Add user accounting

This commit is contained in:
2026-08-27 00:10:53 +03:00
parent ce70b7322f
commit b3ea30e807
6 changed files with 105 additions and 16 deletions
+24 -6
View File
@@ -1,4 +1,4 @@
const state = { orders: [], users: [], vehicles: [], recordItems: [] };
const state = { orders: [], users: [], vehicles: [], recordItems: [], user: null };
const $ = (selector) => document.querySelector(selector);
async function request(url, options) {
@@ -13,6 +13,10 @@ function formatTime(value) { return new Date(value).toLocaleTimeString([], { hou
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[character])); }
function canModify() { return ['admin', 'technician', 'service_advisor'].includes(state.user?.role) || state.user?.can_add === true; }
function canDelete() { return state.user?.role === 'admin'; }
async function loadAuth() { try { state.user = (await request('/api/auth/me')).user; updateAuthUi(); } catch (error) { showError(error); } }
function updateAuthUi() { const signedIn = Boolean(state.user); $('#current-user').textContent = signedIn ? `${state.user.display_name} · ${state.user.role.replaceAll('_', ' ')}` : 'Read-only mode'; $('#login-button').hidden = signedIn; $('#logout-button').hidden = !signedIn; document.querySelectorAll('#new-customer-button, #new-customer-button-secondary, #new-vehicle-button, #new-record-button').forEach((button) => { button.hidden = !canModify(); }); $('#new-user-button').hidden = !canDelete(); if (state.users.length) renderUsers(); renderOrders(); }
async function loadOverview() {
try {
@@ -53,7 +57,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>€${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>`;
$('#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}" ${canModify() ? '' : 'disabled'}><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>${canModify() ? `<button class="edit-button" data-record-id="${order.service_order_id}">Edit</button>` : '<span class="read-only-label">Read only</span>'}</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))));
}
@@ -68,12 +72,16 @@ async function loadVehicles() {
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))));
renderUsers();
} catch (error) { showError(error); }
}
function renderUsers() {
$('#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>${canDelete() ? `<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>` : '<span class="read-only-label">Admin only</span>'}</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))));
}
function editUser(user) {
const form = $('#user-form'); form.dataset.editId = user.user_id;
form.elements.username.value = user.username; form.elements.username.disabled = true;
@@ -122,6 +130,10 @@ function switchView(viewName) {
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()));
$('#login-button').addEventListener('click', () => $('#login-dialog').showModal());
$('#cancel-login-button').addEventListener('click', () => $('#login-dialog').close());
$('#close-login-button').addEventListener('click', () => $('#login-dialog').close());
$('#logout-button').addEventListener('click', async () => { await request('/api/auth/logout', { method: 'POST' }); state.user = null; updateAuthUi(); });
$('#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(); });
@@ -193,11 +205,16 @@ $('#user-form').addEventListener('submit', async (event) => {
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') }) });
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'), password: form.get('password'), 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); }
});
$('#login-form').addEventListener('submit', async (event) => {
event.preventDefault();
try { state.user = (await request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.fromEntries(new FormData(event.target))) })).user; event.target.reset(); $('#login-dialog').close(); updateAuthUi(); } catch (error) { showError(error); }
});
$('#record-form').addEventListener('submit', async (event) => {
event.preventDefault();
try {
@@ -209,4 +226,5 @@ $('#record-form').addEventListener('submit', async (event) => {
} catch (error) { showError(error); }
});
loadAuth();
loadOverview();