modified version

This commit is contained in:
Stan
2026-04-21 23:26:13 +02:00
parent e7127f3215
commit bdd06105dd
46 changed files with 1250 additions and 5248 deletions
+122 -8
View File
@@ -53,11 +53,30 @@ const bulkImagesStore = new Map();
/*
* Loads all admin entity data from the server bulk endpoint and caches in IndexedDB.
*
* Failure handling:
* 401 — Session is gone (e.g. server restarted). Redirect to login so the
* user can re-authenticate and return with a fresh session.
* Other non-2xx — Transient server/network problem. Fall back to the last
* successful snapshot stored in IndexedDB so the user can still see
* their assigned tasks.
*/
async function loadFromServer() {
try {
const resp = await fetch('/api/v1/admin/all', { headers: { Accept: 'application/json' } });
if (!resp.ok) return;
if (!resp.ok) {
if (resp.status === 401) {
/* Session expired or server restarted — must re-authenticate. */
window.location.href = '/login-user';
return;
}
/* Other server error — use last cached snapshot so tasks remain visible. */
console.warn('Server returned', resp.status, '— falling back to IndexedDB cache.');
await loadFromCache();
return;
}
const data = await resp.json();
await dbPut(STORE_CONFIG, { key: 'admin_all', value: data });
userState.templateSettings = data.templateSettings || { categories: [], subCategories: [], severities: [], statuses: [], handledBy: [] };
@@ -68,7 +87,9 @@ async function loadFromServer() {
userState.clTemplates = data.clTemplates || [];
userState.tasks = data.tasks || [];
} catch (err) {
/* Network failure (offline, DNS, etc.) — fall back to cache. */
console.warn('Failed to load data from server, using IndexedDB cache', err);
await loadFromCache();
}
}
@@ -128,6 +149,30 @@ async function loadAllData() {
userState.taskData = await loadTaskData();
}
/*
* Manually triggered synchronization with the server.
* Called when the user presses the "Sync" button.
* Fetches fresh data, re-applies the user filter, and re-renders task lists.
*/
async function forceSyncWithServer() {
const btn = document.getElementById('syncBtn');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>Syncing…';
}
try {
await loadFromServer();
filterTasksByUser();
renderTaskListView();
renderSidebarTasks();
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-arrow-repeat me-1"></i>Sync';
}
}
}
function filterTasksByUser() {
const params = new URLSearchParams(window.location.search);
const userId = params.get('userId');
@@ -142,6 +187,7 @@ function bindEvents() {
document.getElementById('saveDraftBtn')?.addEventListener('click', saveDraft);
document.getElementById('saveFinalBtn')?.addEventListener('click', saveFinal);
document.getElementById('showSettingsBtn')?.addEventListener('click', showSettingsView);
document.getElementById('syncBtn')?.addEventListener('click', forceSyncWithServer);
document.getElementById('closeSettingsBtn')?.addEventListener('click', closeSettingsView);
document.getElementById('recordSearchInput')?.addEventListener('input', onSearchInput);
@@ -203,16 +249,23 @@ async function showListView() {
}
function showDetailView(taskId) {
userState.currentTaskId = Number(taskId);
/* Always work with a numeric ID. dataset attributes and sidebar event handlers
may pass either a string or a number depending on the call site. Normalizing
here keeps every downstream function consistent. */
const id = Number(taskId);
userState.currentTaskId = id;
userState.activeCategory = null;
userState.searchQuery = '';
const searchInput = document.getElementById('recordSearchInput');
if (searchInput) searchInput.value = '';
hideAllViews();
document.getElementById('taskDetailView').classList.add('workspace-view-active');
/* If task was reopened and images were stripped, try to re-download from server */
maybeDownloadImages(taskId).then(() => renderTaskDetail());
highlightSidebarTask(taskId);
/* 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)
.then(() => maybeDownloadImages(id))
.then(() => renderTaskDetail());
highlightSidebarTask(id);
}
/* ═══════════════════════════════════════════════════════════════════════════
@@ -1274,15 +1327,76 @@ function stripImageDataFromStorage(taskId) {
updateStorageIndicator();
}
/* ═══════════════════════════════════════════════════════════════════════════
* Hydrate task data from server when IndexedDB has no local copy
* ═══════════════════════════════════════════════════════════════════════════ */
/*
* If the browser's IndexedDB has no record data for this task (cleared storage,
* first access on a new device, or fresh browser profile), fetch the last
* submitted report from the server and seed the local state so previously
* filled values are visible without the user having to re-enter them.
*
* Images are not re-embedded here — they are pointed to the server with
* uploadedToServer:true so that maybeDownloadImages (called next in the chain)
* can fetch the actual blobs.
*/
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;
if (!navigator.onLine) return;
try {
const resp = await fetch(`/api/v1/reports/${id}`, { headers: { Accept: 'application/json' } });
if (!resp.ok) return; /* 404 = task has never been saved — empty form is correct */
const report = await resp.json();
if (!report?.answers?.records) return;
/* Rebuild records, keeping all field values but replacing image dataUrls with
uploadedToServer markers so the image-download step can restore them. */
const hydratedRecords = {};
for (const [recId, rd] of Object.entries(report.answers.records)) {
hydratedRecords[recId] = {
status: rd.status || '',
handledBy: rd.handledBy || '',
comment: rd.comment || '',
images: (rd.images || []).map(img => ({
name: img.name || '',
size: img.size || 0,
uploadedToServer: true
/* dataUrl intentionally omitted — fetched by maybeDownloadImages */
}))
};
}
userState.taskData[id] = {
visitDate: report.answers.visitDate || '',
records: hydratedRecords
};
/* Persist to IndexedDB so future visits within the same browser are instant. */
await saveOneTaskData(id, userState.taskData[id]);
} catch (err) {
console.warn('Could not hydrate task data from server:', err.message);
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Download images from server when task is reopened
* ═══════════════════════════════════════════════════════════════════════════ */
async function maybeDownloadImages(taskId) {
const task = userState.tasks.find(t => t.id === taskId);
/* Normalize to number — callers may pass a string from a dataset attribute. */
const id = Number(taskId);
const task = userState.tasks.find(t => t.id === id);
if (!task) return;
const data = getTaskData(taskId);
const data = getTaskData(id);
if (!data.records) return;
/* Check if any images have uploadedToServer flag but no dataUrl */
@@ -1299,7 +1413,7 @@ async function maybeDownloadImages(taskId) {
/* Fetch images (as dataUrls) from the server */
try {
const resp = await fetch(`/api/v1/reports/${taskId}/images`, {
const resp = await fetch(`/api/v1/reports/${id}/images`, {
headers: { Accept: 'application/json' }
});
if (!resp.ok) return;