modified version
This commit is contained in:
+122
-8
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user