Added more functionality

This commit is contained in:
2026-08-26 23:40:31 +03:00
parent d1807a424e
commit c37f7bb6ef
6 changed files with 53 additions and 6 deletions
+19 -4
View File
@@ -1,4 +1,4 @@
const state = { orders: [], users: [], vehicles: [] };
const state = { orders: [], users: [], vehicles: [], recordItems: [] };
const $ = (selector) => document.querySelector(selector);
async function request(url, options) {
@@ -53,7 +53,7 @@ function renderOrders() {
const serviceDate = String(order.service_date || '').slice(0, 10);
return (!vehicleFilter || vehicleText.includes(vehicleFilter)) && (!customerFilter || customerText.includes(customerFilter)) && (!technicianFilter || technicianText.includes(technicianFilter)) && (!filters.dateFrom || serviceDate >= filters.dateFrom) && (!filters.dateTo || serviceDate <= filters.dateTo) && (!filters.status || order.status === filters.status);
});
$('#orders-table').innerHTML = visibleOrders.length ? visibleOrders.map((order) => `<tr><td>${escapeHtml(order.service_date)}</td><td>${escapeHtml(order.vin || '—')}</td><td><strong>${escapeHtml(order.license_plate)}</strong></td><td>${escapeHtml(`${order.make || ''} ${order.model || ''}`.trim())}</td><td>${Number(order.mileage).toLocaleString()} km</td><td>${escapeHtml(order.customer_name)}</td><td>${escapeHtml(order.technician)}</td><td>${escapeHtml(order.complaint)}</td><td><select class="status-select" data-order-id="${order.service_order_id}"><option value="${order.status}" selected>${titleCase(order.status)}</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></select></td><td><button class="edit-button" data-record-id="${order.service_order_id}">Edit</button></td></tr>`).join('') : `<tr><td colspan="10">${emptyMessage(state.orders.length ? 'No service records match these filters.' : 'No service records yet.')}</td></tr>`;
$('#orders-table').innerHTML = visibleOrders.length ? visibleOrders.map((order) => `<tr><td>${escapeHtml(order.service_date)}</td><td>${escapeHtml(order.vin || '—')}</td><td><strong>${escapeHtml(order.license_plate)}</strong></td><td>${escapeHtml(`${order.make || ''} ${order.model || ''}`.trim())}</td><td>${Number(order.mileage).toLocaleString()} km</td><td>${escapeHtml(order.customer_name)}</td><td>${escapeHtml(order.technician)}</td><td>${escapeHtml(order.complaint)}</td><td>€${Number(order.total || 0).toFixed(2)}</td><td><select class="status-select" data-order-id="${order.service_order_id}"><option value="${order.status}" selected>${titleCase(order.status)}</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></select></td><td><button class="edit-button" data-record-id="${order.service_order_id}">Edit</button></td></tr>`).join('') : `<tr><td colspan="11">${emptyMessage(state.orders.length ? 'No service records match these filters.' : 'No service records yet.')}</td></tr>`;
document.querySelectorAll('.status-select').forEach((select) => select.addEventListener('change', updateOrderStatus));
document.querySelectorAll('.edit-button[data-record-id]').forEach((button) => button.addEventListener('click', () => editRecord(state.orders.find((order) => order.service_order_id === button.dataset.recordId))));
}
@@ -85,9 +85,21 @@ function editUser(user) {
function editRecord(record) {
const form = $('#record-form'); form.dataset.editId = record.service_order_id;
for (const name of ['serviceDate', 'vin', 'licensePlate', 'make', 'model', 'modelYear', 'engine', 'transmission', 'fuelType', 'color', 'customerName', 'customerEmail', 'customerPhone', 'mileage', 'technicianName', 'complaint']) form.elements[name].value = record[name === 'licensePlate' ? 'license_plate' : name === 'modelYear' ? 'model_year' : name === 'customerName' ? 'customer_name' : name === 'customerEmail' ? 'customer_email' : name === 'customerPhone' ? 'customer_phone' : name === 'technicianName' ? 'technician' : name] || '';
state.recordItems = (record.items || []).map((item) => ({ itemType: item.item_type, description: item.description, partNumber: item.part_number || '', usedTimeHours: item.labor_time_minutes ? Number(item.labor_time_minutes) / 60 : '', quantity: item.quantity || 1, unitPrice: item.unit_price || 0 }));
renderServiceItems();
$('#record-dialog-title').textContent = 'Edit service record'; $('#record-submit-button').textContent = 'Save changes'; $('#record-dialog').showModal();
}
function renderServiceItems() {
$('#service-items').innerHTML = state.recordItems.map((item, index) => `<div class="service-item" data-item-index="${index}"><div class="item-row-heading"><strong>${item.itemType === 'part' ? 'Part' : 'Labor'}</strong><button class="remove-item-button" type="button" data-item-index="${index}">Remove</button></div><label>Description<input data-item-field="description" value="${escapeHtml(item.description)}" /></label>${item.itemType === 'part' ? `<label>Part number<input data-item-field="partNumber" value="${escapeHtml(item.partNumber)}" /></label><label>Quantity<input data-item-field="quantity" type="number" min="0.01" step="0.01" value="${item.quantity}" /></label>` : `<label>Used time (hours)<input data-item-field="usedTimeHours" type="number" min="0.01" step="0.25" value="${item.usedTimeHours}" /></label>`}<label>${item.itemType === 'part' ? 'Price each' : 'Labor price per hour'}<input data-item-field="unitPrice" type="number" min="0" step="0.01" value="${item.unitPrice}" /></label><span class="item-line-total">Line total: €${calculateItemTotal(item).toFixed(2)}</span></div>`).join('');
$('#service-items-total').textContent = `€${state.recordItems.reduce((total, item) => total + calculateItemTotal(item), 0).toFixed(2)}`;
document.querySelectorAll('.service-item input').forEach((input) => input.addEventListener('input', (event) => { const row = event.target.closest('.service-item'); state.recordItems[row.dataset.itemIndex][event.target.dataset.itemField] = event.target.value; updateServiceItemTotals(); }));
document.querySelectorAll('.remove-item-button').forEach((button) => button.addEventListener('click', () => { state.recordItems.splice(Number(button.dataset.itemIndex), 1); renderServiceItems(); }));
}
function calculateItemTotal(item) { return item.itemType === 'labor' ? (Number(item.usedTimeHours) || 0) * (Number(item.unitPrice) || 0) : (Number(item.quantity) || 0) * (Number(item.unitPrice) || 0); }
function updateServiceItemTotals() { document.querySelectorAll('.item-line-total').forEach((total, index) => { total.textContent = `Line total: €${calculateItemTotal(state.recordItems[index]).toFixed(2)}`; }); $('#service-items-total').textContent = `€${state.recordItems.reduce((total, item) => total + calculateItemTotal(item), 0).toFixed(2)}`; }
async function deleteUser(event) {
if (!window.confirm(`Delete ${event.target.dataset.userName}?`)) return;
try { await request(`/api/users/${event.target.dataset.userId}`, { method: 'DELETE' }); await loadUsers(); } catch (error) { showError(error); }
@@ -115,7 +127,9 @@ $('#close-customer-button').addEventListener('click', () => $('#customer-dialog'
$('#new-user-button').addEventListener('click', () => { const form = $('#user-form'); delete form.dataset.editId; form.reset(); form.elements.username.disabled = false; $('#user-dialog-title').textContent = 'Add user'; $('#user-submit-button').textContent = 'Create user'; $('#user-dialog').showModal(); });
$('#cancel-user-button').addEventListener('click', () => $('#user-dialog').close());
$('#close-user-button').addEventListener('click', () => $('#user-dialog').close());
$('#new-record-button').addEventListener('click', () => { const form = $('#record-form'); delete form.dataset.editId; form.reset(); $('#record-dialog-title').textContent = 'Add service record'; $('#record-submit-button').textContent = 'Create service record'; $('#record-dialog').showModal(); });
$('#new-record-button').addEventListener('click', () => { const form = $('#record-form'); delete form.dataset.editId; form.reset(); state.recordItems = []; renderServiceItems(); $('#record-dialog-title').textContent = 'Add service record'; $('#record-submit-button').textContent = 'Create service record'; $('#record-dialog').showModal(); });
$('#add-part-button').addEventListener('click', () => { state.recordItems.push({ itemType: 'part', description: '', partNumber: '', quantity: 1, unitPrice: 0 }); renderServiceItems(); });
$('#add-labor-button').addEventListener('click', () => { state.recordItems.push({ itemType: 'labor', description: '', usedTimeHours: 1, unitPrice: 0 }); renderServiceItems(); });
$('#new-vehicle-button').addEventListener('click', () => $('#vehicle-dialog').showModal());
$('#cancel-vehicle-button').addEventListener('click', () => $('#vehicle-dialog').close());
$('#close-vehicle-button').addEventListener('click', () => $('#vehicle-dialog').close());
@@ -173,9 +187,10 @@ $('#record-form').addEventListener('submit', async (event) => {
event.preventDefault();
try {
const form = Object.fromEntries(new FormData(event.target));
form.items = state.recordItems;
const editId = event.target.dataset.editId;
await request(editId ? `/api/service-records/${editId}` : '/api/service-records', { method: editId ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) });
event.target.reset(); delete event.target.dataset.editId; $('#record-dialog-title').textContent = 'Add service record'; $('#record-submit-button').textContent = 'Create service record'; $('#record-dialog').close(); await loadOrders(); await loadOverview();
event.target.reset(); state.recordItems = []; delete event.target.dataset.editId; $('#record-dialog-title').textContent = 'Add service record'; $('#record-submit-button').textContent = 'Create service record'; $('#record-dialog').close(); await loadOrders(); await loadOverview();
} catch (error) { showError(error); }
});
+2 -2
View File
@@ -41,7 +41,7 @@
<section id="vehicles-view" class="view"><div class="section-heading"><div><p class="eyebrow">Vehicle register</p><h2>Vehicles</h2><p class="subheading">Find a vehicle by plate or VIN and keep its specifications together.</p></div><button class="primary-button" id="new-vehicle-button">+ Add vehicle</button></div><div class="panel table-panel"><div class="table-wrap"><table><thead><tr><th>Vehicle</th><th>Identifiers</th><th>Specifications</th><th>Owner</th><th>Mileage</th><th>Records</th></tr></thead><tbody id="vehicles-table"></tbody></table></div></div></section>
<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>Status</th><th>Actions</th></tr></thead><tbody id="orders-table"></tbody></table></div></div></section>
<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>
</main>
</div>
@@ -49,7 +49,7 @@
<dialog id="customer-dialog"><form method="dialog" id="customer-form"><div class="dialog-heading"><div><p class="eyebrow">Directory</p><h2>Add customer</h2></div><button class="close-button" type="button" id="close-customer-button" aria-label="Close">×</button></div><label>First name<input name="firstName" required /></label><label>Last name<input name="lastName" required /></label><label>Email<input name="email" type="email" /></label><label>Phone<input name="phone" type="tel" /></label><div class="dialog-actions"><button class="secondary-button" type="button" id="cancel-customer-button">Cancel</button><button class="primary-button" value="default">Save customer</button></div></form></dialog>
<dialog id="vehicle-dialog"><form method="dialog" id="vehicle-form"><div class="dialog-heading"><div><p class="eyebrow">Vehicle register</p><h2>Add vehicle</h2></div><button class="close-button" type="button" id="close-vehicle-button" aria-label="Close">×</button></div><p class="form-hint">Use an existing customer email to link this vehicle to that customer.</p><label>Customer name<input name="customerName" required /></label><label>Customer email<input name="customerEmail" type="email" /></label><label>Customer phone<input name="customerPhone" type="tel" /></label><label>VIN<input name="vin" minlength="17" maxlength="17" required /></label><label>License plate number<input name="licensePlate" required /></label><label>Make<input name="make" required /></label><label>Model<input name="model" required /></label><label>Model year<input name="modelYear" type="number" min="1886" max="2100" /></label><label>Engine<input name="engine" /></label><label>Transmission<input name="transmission" /></label><label>Fuel type<input name="fuelType" /></label><label>Color<input name="color" /></label><label>Car mileage<input name="mileage" type="number" min="0" step="1" value="0" required /></label><div class="dialog-actions"><button class="secondary-button" type="button" id="cancel-vehicle-button">Cancel</button><button class="primary-button" value="default">Create vehicle</button></div></form></dialog>
<dialog id="user-dialog"><form method="dialog" id="user-form"><div class="dialog-heading"><div><p class="eyebrow">Access control</p><h2 id="user-dialog-title">Add user</h2></div><button class="close-button" type="button" id="close-user-button" aria-label="Close">×</button></div><label>Username<input name="username" required /></label><label>Display name<input name="displayName" required /></label><label>Email<input name="email" type="email" /></label><label>Role<select name="role"><option value="viewer">Viewer</option><option value="technician">Technician</option><option value="service_advisor">Service advisor</option><option value="admin">Administrator</option></select></label><fieldset><legend>Rights</legend><label class="check-label"><input name="canRead" type="checkbox" checked /> Read records</label><label class="check-label"><input name="canAdd" type="checkbox" /> Add records</label><label class="check-label"><input name="canDelete" type="checkbox" /> Delete records</label></fieldset><div class="dialog-actions"><button class="secondary-button" type="button" id="cancel-user-button">Cancel</button><button class="primary-button" id="user-submit-button" value="default">Create user</button></div></form></dialog>
<dialog id="record-dialog"><form method="dialog" id="record-form"><div class="dialog-heading"><div><p class="eyebrow">Workshop floor</p><h2 id="record-dialog-title">Add service record</h2></div><button class="close-button" type="button" id="close-record-button" aria-label="Close">×</button></div><p class="form-hint">Enter a known VIN or license plate to autofill the vehicle and customer. Use customer email when names are shared.</p><label>Service date<input name="serviceDate" type="date" required /></label><label>VIN<input name="vin" minlength="17" maxlength="17" /></label><label>License plate number<input name="licensePlate" /></label><label>Make<input name="make" required /></label><label>Model<input name="model" required /></label><label>Model year<input name="modelYear" type="number" min="1886" max="2100" /></label><label>Engine<input name="engine" placeholder="e.g. 2.0 TDI" /></label><label>Transmission<input name="transmission" placeholder="e.g. Automatic" /></label><label>Fuel type<input name="fuelType" placeholder="e.g. Petrol, diesel, electric" /></label><label>Color<input name="color" /></label><label>Customer name<input name="customerName" placeholder="First and last name" required /></label><label>Customer email<input name="customerEmail" type="email" /></label><label>Customer phone<input name="customerPhone" type="tel" /></label><label>Car mileage<input name="mileage" type="number" min="0" step="1" required /></label><label>Technician<input name="technicianName" required /></label><label>Complaint or service requested<textarea name="complaint" rows="3" required></textarea></label><div class="dialog-actions"><button class="secondary-button" type="button" id="cancel-record-button">Cancel</button><button class="primary-button" id="record-submit-button" value="default">Create service record</button></div></form></dialog>
<dialog id="record-dialog"><form method="dialog" id="record-form"><div class="dialog-heading"><div><p class="eyebrow">Workshop floor</p><h2 id="record-dialog-title">Add service record</h2></div><button class="close-button" type="button" id="close-record-button" aria-label="Close">×</button></div><p class="form-hint">Enter a known VIN or license plate to autofill the vehicle and customer. Use customer email when names are shared.</p><label>Service date<input name="serviceDate" type="date" required /></label><label>VIN<input name="vin" minlength="17" maxlength="17" /></label><label>License plate number<input name="licensePlate" /></label><label>Make<input name="make" required /></label><label>Model<input name="model" required /></label><label>Model year<input name="modelYear" type="number" min="1886" max="2100" /></label><label>Engine<input name="engine" placeholder="e.g. 2.0 TDI" /></label><label>Transmission<input name="transmission" placeholder="e.g. Automatic" /></label><label>Fuel type<input name="fuelType" placeholder="e.g. Petrol, diesel, electric" /></label><label>Color<input name="color" /></label><label>Customer name<input name="customerName" placeholder="First and last name" required /></label><label>Customer email<input name="customerEmail" type="email" /></label><label>Customer phone<input name="customerPhone" type="tel" /></label><label>Car mileage<input name="mileage" type="number" min="0" step="1" required /></label><label>Technician<input name="technicianName" required /></label><label>Complaint or service requested<textarea name="complaint" rows="3" required></textarea></label><section class="items-section"><div class="items-heading"><div><p class="eyebrow">Optional billing</p><h3>Used parts and labor</h3></div><div class="item-add-actions"><button class="secondary-button" type="button" id="add-part-button">+ Part</button><button class="secondary-button" type="button" id="add-labor-button">+ Labor</button></div></div><div id="service-items"></div><strong class="items-total">Total: <span id="service-items-total">€0.00</span></strong></section><div class="dialog-actions"><button class="secondary-button" type="button" id="cancel-record-button">Cancel</button><button class="primary-button" id="record-submit-button" value="default">Create service record</button></div></form></dialog>
<script src="/app.js"></script>
</body>
</html>
+1
View File
@@ -23,6 +23,7 @@ nav { display: grid; gap: 8px; margin-top: 76px; }
.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); }
.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; }
.filter-bar { display: grid; grid-template-columns: minmax(180px, 1.5fr) minmax(160px, 1.2fr) minmax(150px, 1fr) repeat(2, minmax(130px, .8fr)) minmax(145px, .9fr) auto; gap: 10px; align-items: end; margin-bottom: 16px; }
.filter-bar label { font-size: 11px; }.filter-bar input, .filter-bar select { width: 100%; padding: 10px; }.filter-clear { white-space: nowrap; }
@media (max-width: 1100px) { .filter-bar { grid-template-columns: repeat(3, 1fr); }.filter-clear { justify-self: start; } }