Added more functionality

This commit is contained in:
2026-08-26 23:34:22 +03:00
parent 2ade668e3e
commit d1807a424e
6 changed files with 212 additions and 45 deletions
+82 -20
View File
@@ -1,4 +1,4 @@
const state = { orders: [], users: [] };
const state = { orders: [], users: [], vehicles: [] };
const $ = (selector) => document.querySelector(selector);
async function request(url, options) {
@@ -22,7 +22,8 @@ async function loadOverview() {
$('#customer-count').textContent = summary.customers;
$('#vehicle-count').textContent = summary.vehicles;
$('#appointments-list').innerHTML = appointments.length ? appointments.map((appointment) => `<div class="list-row"><span class="time">${formatTime(appointment.scheduled_at)}</span><div><div class="item-title">${appointment.customer_name}</div><div class="item-meta">${appointment.make} ${appointment.model} · ${appointment.reason}</div></div><span class="status ${appointment.status}">${titleCase(appointment.status)}</span></div>`).join('') : emptyMessage('No appointments scheduled for today.');
$('#orders-list').innerHTML = orders.length ? orders.slice(0, 5).map((order) => `<div class="list-row"><span class="time">${order.registration_number}</span><div><div class="item-title">${order.make} ${order.model}</div><div class="item-meta">${order.complaint}</div></div><span class="status ${order.status}">${titleCase(order.status)}</span></div>`).join('') : emptyMessage('No active service orders.');
$('#orders-list').innerHTML = orders.length ? orders.slice(0, 5).map((order) => `<div class="list-row"><span class="time">${escapeHtml(order.registration_number)}</span><div><div class="item-title">${escapeHtml(`${order.make} ${order.model}`)}</div><div class="item-meta">${escapeHtml(order.complaint)}</div></div><button class="text-button light open-order-button" type="button" data-order-id="${order.service_order_id}">Open</button></div>`).join('') : emptyMessage('No active service orders.');
document.querySelectorAll('.open-order-button').forEach((button) => button.addEventListener('click', async () => { switchView('orders'); await loadOrders(); const record = state.orders.find((order) => order.service_order_id === button.dataset.orderId); if (record) editRecord(record); }));
} catch (error) { showError(error); }
}
@@ -36,9 +37,31 @@ async function loadCustomers() {
async function loadOrders() {
try {
state.orders = await request('/api/service-records');
$('#orders-table').innerHTML = state.orders.length ? state.orders.map((order) => `<tr><td>${escapeHtml(order.service_date)}</td><td>${escapeHtml(order.vin || '—')}</td><td><strong>${escapeHtml(order.license_plate)}</strong></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="9">${emptyMessage('No service records yet.')}</td></tr>`;
renderOrders();
} catch (error) { showError(error); }
}
function renderOrders() {
const filters = Object.fromEntries(new FormData($('#order-filters')));
const vehicleFilter = filters.vehicle.trim().toLowerCase();
const customerFilter = filters.customer.trim().toLowerCase();
const technicianFilter = filters.technician.trim().toLowerCase();
const visibleOrders = state.orders.filter((order) => {
const vehicleText = `${order.vin || ''} ${order.license_plate || ''} ${order.make || ''} ${order.model || ''}`.toLowerCase();
const customerText = `${order.customer_name || ''} ${order.customer_email || ''}`.toLowerCase();
const technicianText = (order.technician || '').toLowerCase();
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>`;
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))));
}
async function loadVehicles() {
try {
state.vehicles = await request('/api/vehicles');
$('#vehicles-table').innerHTML = state.vehicles.length ? state.vehicles.map((vehicle) => `<tr><td><strong>${escapeHtml(`${vehicle.make || ''} ${vehicle.model || ''}`.trim())}</strong><br />${vehicle.model_year || 'Year not set'}</td><td><strong>${escapeHtml(vehicle.license_plate)}</strong><br />${escapeHtml(vehicle.vin || 'VIN not set')}</td><td>${escapeHtml([vehicle.engine, vehicle.transmission, vehicle.fuel_type, vehicle.color].filter(Boolean).join(' · ') || 'Specifications not set')}</td><td>${escapeHtml(vehicle.customer_name)}</td><td>${Number(vehicle.mileage).toLocaleString()} km</td><td>${vehicle.service_record_count}</td></tr>`).join('') : `<tr><td colspan="6">${emptyMessage('No vehicles registered yet.')}</td></tr>`;
} catch (error) { showError(error); }
}
@@ -51,22 +74,21 @@ async function loadUsers() {
} catch (error) { showError(error); }
}
function editUser(user) {
const form = $('#user-form'); form.dataset.editId = user.user_id;
form.elements.username.value = user.username; form.elements.username.disabled = true;
form.elements.displayName.value = user.display_name; form.elements.email.value = user.email || '';
form.elements.role.value = user.role; form.elements.canRead.checked = user.can_read; form.elements.canAdd.checked = user.can_add; form.elements.canDelete.checked = user.can_delete;
$('#user-dialog-title').textContent = 'Edit user'; $('#user-submit-button').textContent = 'Save changes'; $('#user-dialog').showModal();
}
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] || '';
$('#record-dialog-title').textContent = 'Edit service record'; $('#record-submit-button').textContent = 'Save changes'; $('#record-dialog').showModal();
}
async function deleteUser(event) {
function editUser(user) {
const form = $('#user-form'); form.dataset.editId = user.user_id;
form.elements.username.value = user.username; form.elements.username.disabled = true;
form.elements.displayName.value = user.display_name; form.elements.email.value = user.email || '';
form.elements.role.value = user.role; form.elements.canRead.checked = user.can_read; form.elements.canAdd.checked = user.can_add; form.elements.canDelete.checked = user.can_delete;
$('#user-dialog-title').textContent = 'Edit user'; $('#user-submit-button').textContent = 'Save changes'; $('#user-dialog').showModal();
}
function editRecord(record) {
const form = $('#record-form'); form.dataset.editId = record.service_order_id;
form.elements.serviceDate.value = record.service_date.slice(0, 10); form.elements.customerName.value = record.customer_name; form.elements.customerEmail.value = record.customer_email || '';
form.elements.vin.value = record.vin || ''; form.elements.licensePlate.value = record.license_plate; form.elements.mileage.value = record.mileage; form.elements.technicianName.value = record.technician; form.elements.complaint.value = record.complaint;
$('#record-dialog-title').textContent = 'Edit service record'; $('#record-submit-button').textContent = 'Save changes'; $('#record-dialog').showModal();
}
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); }
}
@@ -78,25 +100,65 @@ async function updateOrderStatus(event) {
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));
$('#page-title').textContent = viewName === 'overview' ? 'Good morning, workshop.' : viewName === 'customers' ? 'Your customer directory.' : viewName === 'orders' ? 'Keep the bays moving.' : 'Manage workshop access.';
$('#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.';
if (viewName === 'customers') loadCustomers();
if (viewName === 'vehicles') loadVehicles();
if (viewName === 'orders') loadOrders();
if (viewName === 'users') loadUsers();
}
document.querySelectorAll('[data-view]').forEach((button) => button.addEventListener('click', () => switchView(button.dataset.view)));
document.querySelectorAll('#new-customer-button, #new-customer-button-secondary').forEach((button) => button.addEventListener('click', () => $('#customer-dialog').showModal()));
$('#cancel-customer-button').addEventListener('click', () => $('#customer-dialog').close());
$('#close-customer-button').addEventListener('click', () => $('#customer-dialog').close());
$('#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', () => $('#record-dialog').showModal());
$('#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-vehicle-button').addEventListener('click', () => $('#vehicle-dialog').showModal());
$('#cancel-vehicle-button').addEventListener('click', () => $('#vehicle-dialog').close());
$('#close-vehicle-button').addEventListener('click', () => $('#vehicle-dialog').close());
$('#cancel-record-button').addEventListener('click', () => $('#record-dialog').close());
$('#close-record-button').addEventListener('click', () => $('#record-dialog').close());
$('#order-filters').addEventListener('input', renderOrders);
$('#order-filters').addEventListener('change', renderOrders);
['vin', 'licensePlate'].forEach((name) => $('#record-form').elements[name].addEventListener('blur', async (event) => {
const identifier = event.target.value.trim();
if (!identifier) return;
try {
const vehicle = await request(`/api/vehicles/lookup?identifier=${encodeURIComponent(identifier)}`);
const form = $('#record-form');
for (const field of ['vin', 'licensePlate', 'make', 'model', 'modelYear', 'engine', 'transmission', 'fuelType', 'color', 'customerName', 'customerEmail', 'customerPhone', 'mileage']) {
const value = vehicle[field === 'licensePlate' ? 'license_plate' : field === 'modelYear' ? 'model_year' : field === 'customerName' ? 'customer_name' : field === 'customerEmail' ? 'customer_email' : field === 'customerPhone' ? 'customer_phone' : field] || '';
if (value) form.elements[field].value = value;
}
$('#notice').textContent = `Vehicle found: ${vehicle.make} ${vehicle.model}.`;
} catch (error) { if (!error.message.startsWith('No vehicle found')) showError(error); }
}));
['customerName', 'customerEmail'].forEach((name) => $('#record-form').elements[name].addEventListener('blur', async (event) => {
const value = event.target.value.trim();
if (!value) return;
const parameter = name === 'customerEmail' ? 'email' : 'name';
try {
const customer = await request(`/api/customers/lookup?${parameter}=${encodeURIComponent(value)}`);
const form = $('#record-form');
form.elements.customerName.value = `${customer.first_name} ${customer.last_name}`;
form.elements.customerEmail.value = customer.email || '';
form.elements.customerPhone.value = customer.phone || '';
$('#notice').textContent = `Customer found: ${customer.first_name} ${customer.last_name}.`;
} catch (error) { if (!error.message.startsWith('No customer found') && !error.message.startsWith('More than one customer')) showError(error); }
}));
$('#customer-form').addEventListener('submit', async (event) => {
event.preventDefault();
try { await request('/api/customers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.fromEntries(new FormData(event.target))) }); event.target.reset(); $('#customer-dialog').close(); await loadOverview(); await loadCustomers(); } catch (error) { showError(error); }
});
$('#vehicle-form').addEventListener('submit', async (event) => {
event.preventDefault();
try { await request('/api/vehicles', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.fromEntries(new FormData(event.target))) }); event.target.reset(); $('#vehicle-dialog').close(); await loadVehicles(); await loadOverview(); } catch (error) { showError(error); }
});
$('#user-form').addEventListener('submit', async (event) => {
event.preventDefault();
try {
+11 -7
View File
@@ -3,18 +3,19 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bayline Service Desk</title>
<title>eternityproject.fi | Service Desk</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<div class="app-shell">
<aside class="sidebar">
<div class="brand"><span class="brand-mark">B</span><span>Bayline<br /><strong>Service Desk</strong></span></div>
<div class="brand"><span class="brand-mark">E</span><span>eternityproject.fi<br /><strong>Service Desk</strong></span></div>
<nav aria-label="Main navigation">
<button class="nav-item active" data-view="overview"><span>Overview</span><small>01</small></button>
<button class="nav-item" data-view="customers"><span>Customers</span><small>02</small></button>
<button class="nav-item" data-view="orders"><span>Service records</span><small>03</small></button>
<button class="nav-item" data-view="users"><span>Accounts</span><small>04</small></button>
<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>
</nav>
<div class="sidebar-footer"><span class="status-dot"></span><span>Database online</span><small>PostgreSQL 16</small></div>
</aside>
@@ -38,14 +39,17 @@
<section id="customers-view" class="view"><div class="section-heading"><div><p class="eyebrow">Directory</p><h2>Customers</h2><p class="subheading">People and vehicles connected to your workshop.</p></div><button class="primary-button" id="new-customer-button-secondary">+ New customer</button></div><div class="panel table-panel"><div class="table-wrap"><table><thead><tr><th>Customer</th><th>Contact</th><th>Vehicles</th><th>Added</th></tr></thead><tbody id="customers-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, mileage, date, customer, and technician in one view.</p></div><button class="primary-button" id="new-record-button">+ Add service record</button></div><div class="panel table-panel"><div class="table-wrap"><table><thead><tr><th>Service date</th><th>VIN</th><th>License plate</th><th>Mileage</th><th>Customer</th><th>Technician</th><th>Complaint</th><th>Status</th></tr></thead><tbody id="orders-table"></tbody></table></div></div></section>
<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="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>
<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" value="cancel" 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><button class="primary-button" value="default">Save customer</button></form></dialog>
<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><label>Service date<input name="serviceDate" type="date" required /></label><label>Customer name<input name="customerName" placeholder="First and last name" required /></label><label>Customer email<input name="customerEmail" type="email" /></label><label>VIN<input name="vin" minlength="17" maxlength="17" required /></label><label>License plate number<input name="licensePlate" required /></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><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>
+7 -2
View File
@@ -15,10 +15,15 @@ nav { display: grid; gap: 8px; margin-top: 76px; }
.nav-item small { color: #6d8c7e; }.nav-item.active, .nav-item:hover { background: #294542; border-left-color: var(--mint); color: white; }
.sidebar-footer { margin-top: auto; border-top: 1px solid #34504a; padding: 18px 4px 0; color: #a9c0b6; font-size: 12px; display: grid; grid-template-columns: 10px 1fr; gap: 4px 7px; }.sidebar-footer small { grid-column: 2; color: #6d8c7e; }.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--mint-deep); margin-top: 4px; }
.main-content { max-width: 1450px; width: 100%; padding: 48px 5.5%; margin: auto; }.topbar, .section-heading, .panel-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; }.eyebrow { color: #84918d; font-size: 11px; font-weight: 700; letter-spacing: .12em; margin: 0 0 7px; text-transform: uppercase; }.topbar h1, h2 { font: 600 32px 'Space Grotesk', sans-serif; letter-spacing: -.055em; margin: 0; }.section-heading h2, .panel-heading h2 { font-size: 23px; }.subheading { color: var(--muted); margin: 8px 0 0; }
.primary-button { background: var(--ink); border: 0; color: white; padding: 12px 17px; font-weight: 700; }.primary-button:hover { background: #30423f; }.text-button { background: transparent; border: 0; color: #71847b; padding: 5px 0; }.text-button.light { color: #bad9cc; }.notice { min-height: 20px; color: #b15b27; padding-top: 20px; }.notice:empty { padding-top: 0; }
.primary-button { background: var(--ink); border: 0; color: white; padding: 12px 17px; font-weight: 700; }.primary-button:hover { background: #30423f; }.text-button { background: transparent; border: 0; color: #71847b; padding: 5px 0; }.text-button.light { color: var(--mint); }.open-order-button { border: 1px solid var(--mint); padding: 7px 12px; font-weight: 700; white-space: nowrap; }.open-order-button:hover { background: var(--mint); color: var(--navy); }.notice { min-height: 20px; color: #b15b27; padding-top: 20px; }.notice:empty { padding-top: 0; }
.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 42px 0 38px; }.metric-card { background: var(--white); border: 1px solid var(--line); min-height: 155px; padding: 20px; display: flex; flex-direction: column; }.metric-card.accent { background: var(--mint); border-color: var(--mint); }.metric-label { color: #6c7b75; font-size: 12px; font-weight: 700; }.metric-card strong { font: 600 44px 'Space Grotesk', sans-serif; letter-spacing: -.08em; margin: 18px 0 10px; }.metric-note { color: #85918d; font-size: 12px; }.content-grid { display: grid; grid-template-columns: 1.05fr .95fr; gap: 16px; }.panel { background: var(--white); border: 1px solid var(--line); padding: 25px; }.dark-panel { background: var(--navy); border-color: var(--navy); color: white; }.panel-heading { align-items: flex-start; }.list-container { margin-top: 22px; }.list-row { border-top: 1px solid var(--line); display: grid; grid-template-columns: 74px 1fr auto; gap: 14px; padding: 15px 0; align-items: center; }.dark-panel .list-row { border-color: #34504a; }.time { color: #71847b; font-weight: 700; font-size: 12px; }.dark-panel .time { color: var(--mint); }.item-title { font-weight: 700; }.item-meta { color: var(--muted); font-size: 12px; margin-top: 4px; }.dark-panel .item-meta { color: #9bb3aa; }.status { display: inline-block; background: #edf2ee; color: #60706a; font-size: 11px; font-weight: 700; padding: 5px 8px; text-transform: capitalize; white-space: nowrap; }.status.ready, .status.confirmed { background: #dff5e8; color: #27784a; }.status.in_progress { background: #fff0dc; color: #a36218; }.empty-state { color: var(--muted); padding: 30px 0; text-align: center; }.view { display: none; }.active-view { display: block; }.section-heading { align-items: flex-start; margin: 25px 0 32px; }.table-panel { padding: 0; }.table-wrap { overflow-x: auto; }table { border-collapse: collapse; width: 100%; }th, td { border-bottom: 1px solid var(--line); padding: 17px 20px; text-align: left; white-space: nowrap; }th { color: #8a9691; font-size: 11px; letter-spacing: .1em; text-transform: uppercase; }td { color: #465550; }td strong { color: var(--ink); display: block; }.dialog-heading { display: flex; justify-content: space-between; margin-bottom: 25px; }.close-button { background: transparent; border: 0; font-size: 28px; line-height: 1; color: var(--muted); }.dialog-heading h2 { font-size: 24px; }dialog { border: 0; padding: 0; width: min(420px, calc(100% - 32px)); box-shadow: 0 18px 60px #13272533; }dialog::backdrop { background: #122b2a99; }.dialog-heading, dialog form { padding: 26px; }dialog form { display: grid; gap: 15px; }dialog form .dialog-heading { margin: -26px -26px 0; }label { color: #64726d; font-size: 12px; font-weight: 700; display: grid; gap: 7px; }input { border: 1px solid var(--line); padding: 11px; color: var(--ink); outline-color: var(--mint-deep); }
@media (max-width: 900px) { .app-shell { grid-template-columns: 1fr; }.sidebar { padding: 18px; }.sidebar nav { display: flex; margin-top: 24px; overflow-x: auto; }.nav-item { flex: 1; min-width: 145px; }.sidebar-footer { display: none; }.main-content { padding: 30px 20px; }.metric-grid { grid-template-columns: repeat(2, 1fr); margin-top: 30px; }.content-grid { grid-template-columns: 1fr; } }
@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); }
.edit-button { background: transparent; border: 1px solid var(--line); color: var(--ink); padding: 7px 10px; }.edit-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; }
.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; } }
@media (max-width: 520px) { .filter-bar { grid-template-columns: 1fr; } }