Add data import export functions

This commit is contained in:
2026-08-26 23:52:34 +03:00
parent 99d6123f13
commit ce70b7322f
5 changed files with 67 additions and 1 deletions
+16 -1
View File
@@ -113,7 +113,7 @@ function switchView(viewName) {
document.querySelectorAll('.view').forEach((view) => view.classList.toggle('active-view', view.id === `${viewName}-view`));
document.querySelectorAll('.nav-item').forEach((item) => item.classList.toggle('active', item.dataset.view === viewName));
$('#new-customer-button').hidden = viewName === 'customers' || viewName === 'vehicles' || viewName === 'orders';
$('#page-title').textContent = viewName === 'overview' ? 'Good morning, workshop.' : viewName === 'customers' ? 'Your customer directory.' : viewName === 'vehicles' ? 'Know every vehicle.' : viewName === 'orders' ? 'Keep the bays moving.' : 'Manage workshop access.';
$('#page-title').textContent = viewName === 'overview' ? 'Good morning, workshop.' : viewName === 'customers' ? 'Your customer directory.' : viewName === 'vehicles' ? 'Know every vehicle.' : viewName === 'orders' ? 'Keep the bays moving.' : viewName === 'users' ? 'Manage workshop access.' : 'Keep your workshop data portable.';
if (viewName === 'customers') loadCustomers();
if (viewName === 'vehicles') loadVehicles();
if (viewName === 'orders') loadOrders();
@@ -137,6 +137,21 @@ $('#cancel-record-button').addEventListener('click', () => $('#record-dialog').c
$('#close-record-button').addEventListener('click', () => $('#record-dialog').close());
$('#order-filters').addEventListener('input', renderOrders);
$('#order-filters').addEventListener('change', renderOrders);
$('#export-data-button').addEventListener('click', async () => {
try {
const response = await fetch('/api/data/export');
if (!response.ok) throw new Error('Unable to export database data.');
const blob = await response.blob();
const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `car-service-backup-${new Date().toISOString().slice(0, 10)}.json`; link.click(); URL.revokeObjectURL(link.href);
$('#notice').textContent = 'Database backup downloaded.';
} catch (error) { showError(error); }
});
$('#import-data-input').addEventListener('change', async (event) => {
const file = event.target.files[0]; if (!file) return;
$('#import-file-name').textContent = file.name;
if (!window.confirm('Importing a backup replaces all current database data. Continue?')) { event.target.value = ''; $('#import-file-name').textContent = 'No file selected.'; return; }
try { const result = await request('/api/data/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: await file.text() }); $('#notice').textContent = `Backup imported: ${result.imported} records restored.`; await loadOverview(); await loadCustomers(); await loadVehicles(); await loadOrders(); await loadUsers(); } catch (error) { showError(error); } finally { event.target.value = ''; }
});
['vin', 'licensePlate'].forEach((name) => $('#record-form').elements[name].addEventListener('blur', async (event) => {
const identifier = event.target.value.trim();
if (!identifier) return;
+2
View File
@@ -16,6 +16,7 @@
<button class="nav-item" data-view="vehicles"><span>Vehicles</span><small>03</small></button>
<button class="nav-item" data-view="orders"><span>Service records</span><small>04</small></button>
<button class="nav-item" data-view="users"><span>Accounts</span><small>05</small></button>
<button class="nav-item" data-view="settings"><span>Settings</span><small>06</small></button>
</nav>
<div class="sidebar-footer"><span class="status-dot"></span><span>Database online</span><small>PostgreSQL 16</small></div>
</aside>
@@ -43,6 +44,7 @@
<section id="orders-view" class="view"><div class="section-heading"><div><p class="eyebrow">Workshop floor</p><h2>Service records</h2><p class="subheading">VIN, plate, vehicle, mileage, date, customer, and technician in one view.</p></div><button class="primary-button" id="new-record-button">+ Add service record</button></div><form class="filter-bar" id="order-filters"><label>Vehicle<input name="vehicle" placeholder="VIN, plate, make, or model" /></label><label>Customer<input name="customer" placeholder="Customer name or email" /></label><label>Technician<input name="technician" placeholder="Technician name" /></label><label>From<input name="dateFrom" type="date" /></label><label>To<input name="dateTo" type="date" /></label><label>Status<select name="status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="waiting_for_parts">Waiting for parts</option><option value="ready">Ready</option><option value="closed">Closed</option><option value="cancelled">Cancelled</option></select></label><button class="secondary-button filter-clear" type="reset">Clear filters</button></form><div class="panel table-panel"><div class="table-wrap"><table><thead><tr><th>Service date</th><th>VIN</th><th>License plate</th><th>Vehicle</th><th>Mileage</th><th>Customer</th><th>Technician</th><th>Complaint</th><th>Total</th><th>Status</th><th>Actions</th></tr></thead><tbody id="orders-table"></tbody></table></div></div></section>
<section id="users-view" class="view"><div class="section-heading"><div><p class="eyebrow">Access control</p><h2>User accounts</h2><p class="subheading">Manage workshop staff and their database rights.</p></div><button class="primary-button" id="new-user-button">+ Add user</button></div><div class="panel table-panel"><div class="table-wrap"><table><thead><tr><th>User</th><th>Role</th><th>Read</th><th>Add</th><th>Delete</th><th>Actions</th></tr></thead><tbody id="users-table"></tbody></table></div></div></section>
<section id="settings-view" class="view"><div class="section-heading"><div><p class="eyebrow">Workspace</p><h2>Settings</h2><p class="subheading">Manage the workshop database and move a complete backup between environments.</p></div></div><div class="settings-grid"><section class="panel settings-card"><p class="eyebrow">Backup</p><h3>Export all data</h3><p>Download customers, vehicles, employees, appointments, service records, parts, invoices, and accounts as one JSON file.</p><button class="primary-button" id="export-data-button">Download backup</button></section><section class="panel settings-card"><p class="eyebrow">Restore</p><h3>Import all data</h3><p>Restore a JSON backup. This replaces the current database contents after the file passes validation.</p><label class="file-button">Choose backup file<input id="import-data-input" type="file" accept="application/json,.json" /></label><p id="import-file-name" class="form-hint">No file selected.</p></section></div></section>
</main>
</div>
+2
View File
@@ -21,6 +21,8 @@ nav { display: grid; gap: 8px; margin-top: 76px; }
@media (max-width: 520px) { .topbar { align-items: flex-start; flex-direction: column; }.topbar h1 { font-size: 27px; }.metric-grid { grid-template-columns: 1fr 1fr; gap: 8px; }.metric-card { min-height: 135px; padding: 15px; }.metric-card strong { font-size: 34px; }.panel { padding: 18px; }.list-row { grid-template-columns: 58px 1fr; }.list-row .status { grid-column: 2; justify-self: start; }.section-heading { flex-direction: column; } }
.permission { color: #a15b4e; font-size: 12px; font-weight: 700; }.permission.allowed { color: #27784a; }.delete-button { background: transparent; border: 1px solid #e5c9c4; color: #a15b4e; padding: 7px 10px; }.delete-button:hover { background: #fff1ee; }select { border: 1px solid var(--line); padding: 10px; background: white; color: var(--ink); }fieldset { border: 1px solid var(--line); display: grid; gap: 10px; padding: 12px; }legend { color: var(--muted); font-size: 11px; font-weight: 700; text-transform: uppercase; }.check-label { display: flex; align-items: center; gap: 8px; }.check-label input { accent-color: var(--navy); }
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }.secondary-button { background: transparent; border: 1px solid var(--line); color: var(--ink); padding: 12px 17px; font-weight: 700; }.secondary-button:hover { background: var(--paper); }
.settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }.settings-card { min-height: 235px; }.settings-card h3 { font: 600 21px 'Space Grotesk', sans-serif; margin: 0 0 10px; }.settings-card p:not(.eyebrow):not(.form-hint) { color: var(--muted); line-height: 1.6; margin: 0 0 24px; max-width: 48ch; }.file-button { align-items: center; background: var(--ink); color: white; cursor: pointer; display: inline-flex; font-weight: 700; justify-content: center; padding: 12px 17px; }.file-button input { display: none; }
@media (max-width: 720px) { .settings-grid { grid-template-columns: 1fr; } }
.edit-button { background: transparent; border: 1px solid var(--line); color: var(--ink); padding: 7px 10px; }.edit-button:hover { background: var(--paper); }
.form-hint { margin: -4px 0 4px; color: var(--muted); font-size: 0.85rem; }
.items-section { border-top: 1px solid var(--line); padding-top: 18px; }.items-heading { align-items: flex-start; display: flex; justify-content: space-between; gap: 12px; }.items-heading h3 { font: 600 18px 'Space Grotesk', sans-serif; margin: 0; }.item-add-actions { display: flex; gap: 6px; }.item-add-actions .secondary-button { padding: 8px 10px; }.service-item { background: var(--paper); border: 1px solid var(--line); display: grid; gap: 10px; margin-top: 12px; padding: 14px; }.item-row-heading { display: flex; justify-content: space-between; }.remove-item-button { background: transparent; border: 0; color: #a15b4e; cursor: pointer; font-size: 12px; font-weight: 700; }.item-line-total { color: var(--ink); font-size: 12px; font-weight: 700; }.items-total { display: block; font: 600 18px 'Space Grotesk', sans-serif; margin-top: 16px; text-align: right; }