synch correction and db update

This commit is contained in:
Stan
2026-04-26 16:00:43 +02:00
parent c10e259ae8
commit bfd812747e
18 changed files with 3934 additions and 767 deletions
+922 -334
View File
File diff suppressed because it is too large Load Diff
+170 -12
View File
@@ -36,7 +36,10 @@ const userState = {
taskSettings: { projects: [], processes: [] },
taskData: {},
imageRules: null,
appConfig: {},
currentTaskId: null,
currentUserId: null,
currentUserName: '',
language: 'EN',
activeCategory: null,
searchQuery: '',
@@ -86,6 +89,7 @@ async function loadFromServer() {
userState.clRecords = data.clRecords || [];
userState.clTemplates = data.clTemplates || [];
userState.tasks = data.tasks || [];
userState.appConfig = data.appConfig || {};
} catch (err) {
/* Network failure (offline, DNS, etc.) — fall back to cache. */
console.warn('Failed to load data from server, using IndexedDB cache', err);
@@ -108,6 +112,7 @@ async function loadFromCache() {
userState.clRecords = data.clRecords || [];
userState.clTemplates = data.clTemplates || [];
userState.tasks = data.tasks || [];
userState.appConfig = data.appConfig || {};
} catch { /* ignore */ }
}
@@ -118,6 +123,23 @@ async function loadFromCache() {
export async function initUser() {
userState.language = localStorage.getItem('user_language') || 'EN';
await openUserDB();
/* Identify the logged-in user from the server session cookie */
try {
const checkResp = await fetch('/api/v1/auth/check');
if (checkResp.ok) {
const checkData = await checkResp.json();
if (checkData.authenticated && checkData.session?.type === 'user') {
userState.currentUserId = checkData.session.id;
userState.currentUserName = `${checkData.session.name || ''} ${checkData.session.familyName || ''}`.trim();
}
}
} catch (e) { /* non-blocking */ }
/* Display logged-in user name in sidebar */
const nameEl = document.getElementById('userDisplayName');
if (nameEl && userState.currentUserName) nameEl.textContent = userState.currentUserName;
/* Load admin entity data from server before reading IndexedDB cache. */
if (navigator.onLine) {
await loadFromServer();
@@ -165,6 +187,13 @@ async function forceSyncWithServer() {
filterTasksByUser();
renderTaskListView();
renderSidebarTasks();
/* If a task detail is open, also refresh its record data from the server so
* changes made on another device become visible immediately after a manual sync. */
if (userState.currentTaskId) {
await maybeHydrateFromServer(userState.currentTaskId);
await maybeDownloadImages(userState.currentTaskId);
renderTaskDetail();
}
} finally {
if (btn) {
btn.disabled = false;
@@ -174,10 +203,11 @@ async function forceSyncWithServer() {
}
function filterTasksByUser() {
/* Prefer session-based user id; fall back to URL param for compatibility */
const params = new URLSearchParams(window.location.search);
const userId = params.get('userId');
if (userId) {
const uid = Number(userId);
const urlUserId = params.get('userId');
const uid = userState.currentUserId || (urlUserId ? Number(urlUserId) : null);
if (uid) {
userState.tasks = userState.tasks.filter(t => t.userId === uid);
}
}
@@ -186,6 +216,9 @@ function bindEvents() {
document.getElementById('backToListBtn')?.addEventListener('click', showListView);
document.getElementById('saveDraftBtn')?.addEventListener('click', saveDraft);
document.getElementById('saveFinalBtn')?.addEventListener('click', saveFinal);
document.getElementById('mobileSaveDraftBtn')?.addEventListener('click', saveDraft);
document.getElementById('mobileSaveFinalBtn')?.addEventListener('click', saveFinal);
document.getElementById('logoutBtn')?.addEventListener('click', logoutUser);
document.getElementById('showSettingsBtn')?.addEventListener('click', showSettingsView);
document.getElementById('syncBtn')?.addEventListener('click', forceSyncWithServer);
document.getElementById('closeSettingsBtn')?.addEventListener('click', closeSettingsView);
@@ -227,12 +260,14 @@ function hideAllViews() {
function showSettingsView() {
hideAllViews();
document.getElementById('settingsView').classList.add('workspace-view-active');
document.getElementById('desktopSaveActions')?.classList.remove('is-visible');
}
function closeSettingsView() {
hideAllViews();
if (userState.currentTaskId) {
document.getElementById('taskDetailView').classList.add('workspace-view-active');
document.getElementById('desktopSaveActions')?.classList.add('is-visible');
} else {
document.getElementById('taskListView').classList.add('workspace-view-active');
}
@@ -242,6 +277,9 @@ async function showListView() {
userState.currentTaskId = null;
hideAllViews();
document.getElementById('taskListView').classList.add('workspace-view-active');
const msavHide = document.getElementById('mobileSaveActions');
if (msavHide) msavHide.style.display = 'none';
document.getElementById('desktopSaveActions')?.classList.remove('is-visible');
await loadAllData();
filterTasksByUser();
renderTaskListView();
@@ -260,6 +298,9 @@ function showDetailView(taskId) {
if (searchInput) searchInput.value = '';
hideAllViews();
document.getElementById('taskDetailView').classList.add('workspace-view-active');
const msav = document.getElementById('mobileSaveActions');
if (msav) msav.style.display = '';
document.getElementById('desktopSaveActions')?.classList.add('is-visible');
/* If task was reopened and images were stripped, try to re-download from server.
Chain: hydrate values first → then fetch image blobs → then render. */
maybeHydrateFromServer(id)
@@ -279,6 +320,8 @@ function renderTaskListView() {
container.innerHTML = '<div class="p-4 text-center text-muted"><h5>No tasks assigned</h5><p>Tasks will appear here once an administrator assigns them to you.</p></div>';
return;
}
/* Desktop: table layout */
const rows = userState.tasks.map((task) => {
const site = userState.sites.find((s) => s.id === task.siteId);
const tpl = userState.clTemplates.find((t) => t.id === task.templateId);
@@ -292,9 +335,30 @@ function renderTaskListView() {
<td><button class="btn btn-primary btn-sm" data-open-task="${task.id}">Open</button></td>
</tr>`;
}).join('');
container.innerHTML = `<table class="table table-hover table-sm mb-0"><thead><tr>
<th>Site</th><th>Template</th><th>Project</th><th>Process</th><th>Status</th><th></th>
</tr></thead><tbody>${rows}</tbody></table>`;
/* Mobile: card layout — fields expand horizontally with flex-wrap */
const cards = userState.tasks.map((task) => {
const site = userState.sites.find((s) => s.id === task.siteId);
const tpl = userState.clTemplates.find((t) => t.id === task.templateId);
const badgeCls = task.status === 'final' ? 'bg-success' : task.status === 'draft' ? 'bg-warning text-dark' : 'bg-secondary';
return `<div class="task-list-card">
<div class="task-list-card-fields">
<span class="task-list-field"><small>Site</small><strong>${esc(site?.siteCode || '-')}</strong></span>
<span class="task-list-field"><small>Template</small><strong>${esc(tpl?.name || '-')}</strong></span>
<span class="task-list-field"><small>Project</small><strong>${esc(task.project || '-')}</strong></span>
<span class="task-list-field"><small>Process</small><strong>${esc(task.process || '-')}</strong></span>
<span class="badge ${badgeCls} align-self-center">${esc(task.status || 'pending')}</span>
</div>
<button class="btn btn-primary btn-sm flex-shrink-0" data-open-task="${task.id}">Open</button>
</div>`;
}).join('');
container.innerHTML =
`<div class="d-none d-md-block"><table class="table table-hover table-sm mb-0"><thead><tr>
<th>Site</th><th>Template</th><th>Project</th><th>Process</th><th>Status</th><th></th>
</tr></thead><tbody>${rows}</tbody></table></div>` +
`<div class="d-md-none">${cards}</div>`;
container.querySelectorAll('[data-open-task]').forEach((b) => {
b.addEventListener('click', () => showDetailView(b.dataset.openTask));
});
@@ -432,6 +496,49 @@ function renderCategoryTabs(task, tpl) {
renderTaskRecords(taskObj, tplObj, getTaskData(userState.currentTaskId));
});
});
/* Scroll-arrow setup — runs after the DOM is painted so scrollWidth is correct */
requestAnimationFrame(() => initTabsScrollArrows(tabsContainer));
}
/**
* Attaches left/right scroll-arrow logic to the category tabs wrapper.
* Arrows are shown only when the tab list overflows its container,
* and each arrow scrolls by roughly the width of three tabs.
* Called via requestAnimationFrame so layout is complete before measuring.
*/
function initTabsScrollArrows(tabsEl) {
const wrapper = tabsEl?.closest('.tabs-scroll-wrapper');
const btnLeft = document.getElementById('tabsScrollLeft');
const btnRight = document.getElementById('tabsScrollRight');
if (!wrapper || !btnLeft || !btnRight) return;
function updateArrows() {
const overflows = tabsEl.scrollWidth > tabsEl.clientWidth;
wrapper.classList.toggle('has-overflow', overflows);
wrapper.classList.toggle('at-start', tabsEl.scrollLeft <= 2);
wrapper.classList.toggle('at-end',
tabsEl.scrollLeft >= tabsEl.scrollWidth - tabsEl.clientWidth - 2);
}
/* Scroll by ~3 average tab widths on each click */
const scrollStep = () => Math.max(tabsEl.clientWidth * 0.45, 120);
btnLeft.addEventListener('click', () => {
tabsEl.scrollBy({ left: -scrollStep(), behavior: 'smooth' });
});
btnRight.addEventListener('click', () => {
tabsEl.scrollBy({ left: scrollStep(), behavior: 'smooth' });
});
tabsEl.addEventListener('scroll', updateArrows, { passive: true });
/* Re-evaluate when the container is resized (e.g. sidebar open/close) */
if (window._tabsResizeObs) window._tabsResizeObs.disconnect();
window._tabsResizeObs = new ResizeObserver(updateArrows);
window._tabsResizeObs.observe(tabsEl);
updateArrows();
}
function onSearchInput(e) {
@@ -560,6 +667,10 @@ function renderTaskRecords(task, tpl, data) {
const category = rec.category || '-';
const subCategory = rec.subCategory || '-';
const severity = rec.severity || '-';
const sevColor = getSevColor(rec.severityId);
const sevBadge = sevColor
? `<span class="badge" style="background:${sevColor};color:${colorContrast(sevColor)}">${esc(severity)}</span>`
: esc(severity);
html += `<div class="task-record-card" data-record-id="${rec.id}" data-sub-cat="${esc(subCat)}">
<div class="task-record-header">
@@ -570,7 +681,7 @@ function renderTaskRecords(task, tpl, data) {
<div class="d-flex gap-3 mb-2 small text-muted">
<span><strong>Category:</strong> ${esc(category)}</span>
<span><strong>Sub:</strong> ${esc(subCategory)}</span>
<span><strong>Severity:</strong> ${esc(severity)}</span>
<span><strong>Severity:</strong> ${sevBadge}</span>
</div>
<div class="row g-2">
<div class="col-md-4">
@@ -611,6 +722,9 @@ function renderTaskRecords(task, tpl, data) {
container.innerHTML = html;
/* Apply colour coding to status selects based on current value */
applyStatusColors(container);
/* Bind change events */
container.querySelectorAll('.rec-status').forEach((el) => el.addEventListener('change', onRecordFieldChange));
container.querySelectorAll('.rec-handled').forEach((el) => el.addEventListener('change', onRecordFieldChange));
@@ -717,8 +831,11 @@ function onRecordFieldChange(e) {
const data = getTaskData(userState.currentTaskId);
if (!data.records[recId]) data.records[recId] = { status: '', handledBy: '', comment: '', images: [] };
if (e.target.classList.contains('rec-status')) data.records[recId].status = e.target.value;
else if (e.target.classList.contains('rec-handled')) data.records[recId].handledBy = e.target.value;
if (e.target.classList.contains('rec-status')) {
data.records[recId].status = e.target.value;
const color = getStatColor(e.target.value);
e.target.style.borderLeft = color ? `4px solid ${color}` : '';
} else if (e.target.classList.contains('rec-handled')) data.records[recId].handledBy = e.target.value;
else if (e.target.classList.contains('rec-comment')) data.records[recId].comment = e.target.value;
data.visitDate = document.getElementById('visitDate').value;
@@ -1059,6 +1176,13 @@ function showValidationErrors(errors) {
* Helpers
* ═══════════════════════════════════════════════════════════════════════════ */
async function logoutUser() {
try {
await fetch('/api/v1/auth/logout', { method: 'POST' });
} catch (e) { /* ignore */ }
window.location.href = '/';
}
function updateConnectionBadge() {
const badge = document.getElementById('connectionBadge');
if (!badge) return;
@@ -1097,6 +1221,34 @@ function esc(text) {
return d.innerHTML;
}
/* Returns white or black depending on background luminance */
function colorContrast(hex) {
if (!hex || hex.length < 7) return '#000000';
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return lum > 0.5 ? '#000000' : '#ffffff';
}
function getSevColor(severityId) {
const sev = userState.templateSettings.severities.find(s => s.id === severityId);
return sev?.color || '';
}
function getStatColor(statusValue) {
const stat = userState.templateSettings.statuses.find(s => s.value === statusValue);
return stat?.color || '';
}
/* Applies a colored left border to each status select based on the selected value */
function applyStatusColors(container) {
container.querySelectorAll('.rec-status').forEach(sel => {
const color = getStatColor(sel.value);
sel.style.borderLeft = color ? `4px solid ${color}` : '';
});
}
function formatFileSizeUser(bytes) {
if (!bytes) return '-';
if (bytes < 1024) return `${bytes} B`;
@@ -1280,6 +1432,10 @@ function applyTaskLock(task) {
if (visitDate) visitDate.disabled = locked;
if (saveDraftBtn) saveDraftBtn.disabled = locked;
if (saveFinalBtn) saveFinalBtn.disabled = locked;
const mobileSaveDraftBtn = document.getElementById('mobileSaveDraftBtn');
const mobileSaveFinalBtn = document.getElementById('mobileSaveFinalBtn');
if (mobileSaveDraftBtn) mobileSaveDraftBtn.disabled = locked;
if (mobileSaveFinalBtn) mobileSaveFinalBtn.disabled = locked;
if (container && locked) {
container.querySelectorAll('select, textarea, input').forEach(el => { el.disabled = true; });
@@ -1344,9 +1500,11 @@ function stripImageDataFromStorage(taskId) {
async function maybeHydrateFromServer(taskId) {
const id = Number(taskId);
/* Skip if IndexedDB already has record data for this task. */
const existing = userState.taskData[id];
if (existing && Object.keys(existing.records || {}).length > 0) return;
/* Skip if the user has unsaved local changes — don't overwrite their in-progress work.
* unsavedChanges is set by markUnsaved() on every field edit and cleared by markSaved()
* after an explicit Save Draft / Save Final action. It resets to false on page reload,
* so opening the task on a fresh device (or a new tab) always fetches from the server. */
if (userState.unsavedChanges) return;
if (!navigator.onLine) return;