Add user accounting
This commit is contained in:
+24
-6
@@ -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) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[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();
|
||||
Reference in New Issue
Block a user