120 lines
11 KiB
JavaScript
120 lines
11 KiB
JavaScript
const state = { orders: [], users: [] };
|
|
const $ = (selector) => document.querySelector(selector);
|
|
|
|
async function request(url, options) {
|
|
const response = await fetch(url, options);
|
|
const data = await response.json();
|
|
if (!response.ok) throw new Error(data.error || 'Request failed.');
|
|
return data;
|
|
}
|
|
|
|
function showError(error) { $('#notice').textContent = error.message; }
|
|
function formatTime(value) { return new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
|
|
function titleCase(value) { return value.replaceAll('_', ' '); }
|
|
function emptyMessage(message) { return `<div class="empty-state">${message}</div>`; }
|
|
function escapeHtml(value) { return String(value ?? '').replace(/[&<>'"]/g, (character) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[character])); }
|
|
|
|
async function loadOverview() {
|
|
try {
|
|
const [summary, appointments, orders] = await Promise.all([request('/api/summary'), request('/api/appointments'), request('/api/orders')]);
|
|
$('#today-appointments').textContent = summary.today_appointments;
|
|
$('#active-orders').textContent = summary.active_orders;
|
|
$('#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.');
|
|
} catch (error) { showError(error); }
|
|
}
|
|
|
|
async function loadCustomers() {
|
|
try {
|
|
const customers = await request('/api/customers');
|
|
$('#customers-table').innerHTML = customers.length ? customers.map((customer) => `<tr><td><strong>${customer.first_name} ${customer.last_name}</strong></td><td>${customer.email || '—'}<br />${customer.phone || ''}</td><td>${customer.vehicle_count}</td><td>${new Date(customer.created_at || Date.now()).toLocaleDateString()}</td></tr>`).join('') : `<tr><td colspan="4">${emptyMessage('No customers yet.')}</td></tr>`;
|
|
} catch (error) { showError(error); }
|
|
}
|
|
|
|
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>`;
|
|
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))));
|
|
} catch (error) { showError(error); }
|
|
}
|
|
|
|
async function loadUsers() {
|
|
try {
|
|
state.users = await request('/api/users');
|
|
$('#users-table').innerHTML = state.users.map((user) => `<tr><td><strong>${escapeHtml(user.display_name)}</strong>${escapeHtml(user.username)}<br />${escapeHtml(user.email || '')}</td><td>${escapeHtml(titleCase(user.role))}</td><td><span class="permission ${user.can_read ? 'allowed' : ''}">${user.can_read ? 'Yes' : 'No'}</span></td><td><span class="permission ${user.can_add ? 'allowed' : ''}">${user.can_add ? 'Yes' : 'No'}</span></td><td><span class="permission ${user.can_delete ? 'allowed' : ''}">${user.can_delete ? 'Yes' : 'No'}</span></td><td><button class="edit-button" data-user-id="${user.user_id}">Edit</button> <button class="delete-button" data-user-id="${user.user_id}" data-user-name="${escapeHtml(user.display_name)}">Delete</button></td></tr>`).join('');
|
|
document.querySelectorAll('.delete-button').forEach((button) => button.addEventListener('click', deleteUser));
|
|
document.querySelectorAll('.edit-button[data-user-id]').forEach((button) => button.addEventListener('click', () => editUser(state.users.find((user) => user.user_id === button.dataset.userId))));
|
|
} catch (error) { showError(error); }
|
|
}
|
|
|
|
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); }
|
|
}
|
|
|
|
async function updateOrderStatus(event) {
|
|
try { await request(`/api/orders/${event.target.dataset.orderId}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: event.target.value }) }); await loadOrders(); await loadOverview(); } catch (error) { showError(error); }
|
|
}
|
|
|
|
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.';
|
|
if (viewName === 'customers') loadCustomers();
|
|
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()));
|
|
$('#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());
|
|
$('#cancel-record-button').addEventListener('click', () => $('#record-dialog').close());
|
|
$('#close-record-button').addEventListener('click', () => $('#record-dialog').close());
|
|
$('#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); }
|
|
});
|
|
|
|
$('#user-form').addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
try {
|
|
const form = new FormData(event.target);
|
|
const editId = event.target.dataset.editId;
|
|
await request(editId ? `/api/users/${editId}` : '/api/users', { method: editId ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: form.get('username'), displayName: form.get('displayName'), email: form.get('email'), role: form.get('role'), canRead: form.has('canRead'), canAdd: form.has('canAdd'), canDelete: form.has('canDelete') }) });
|
|
event.target.reset(); delete event.target.dataset.editId; event.target.elements.username.disabled = false; $('#user-dialog-title').textContent = 'Add user'; $('#user-submit-button').textContent = 'Create user'; $('#user-dialog').close(); await loadUsers();
|
|
} catch (error) { showError(error); }
|
|
});
|
|
|
|
$('#record-form').addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
try {
|
|
const form = Object.fromEntries(new FormData(event.target));
|
|
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();
|
|
} catch (error) { showError(error); }
|
|
});
|
|
|
|
loadOverview(); |