'use strict'; (function initPotholeBackend(globalScope) { const API_PREFIX = '/api'; function isPlainObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function deepMerge(baseValue, incomingValue) { if (!isPlainObject(baseValue) || !isPlainObject(incomingValue)) { return incomingValue === undefined ? baseValue : incomingValue; } const result = { ...baseValue }; for (const [key, incoming] of Object.entries(incomingValue)) { const existing = result[key]; if (Array.isArray(incoming)) { result[key] = incoming.slice(); continue; } if (isPlainObject(existing) && isPlainObject(incoming)) { result[key] = deepMerge(existing, incoming); continue; } if (incoming !== undefined) { result[key] = incoming; } } return result; } function stripPhotoDataUrl(value) { if (Array.isArray(value)) { return value.map((item) => stripPhotoDataUrl(item)); } if (!isPlainObject(value)) { return value; } const clone = {}; for (const [key, child] of Object.entries(value)) { if (key === 'photoDataUrl') continue; clone[key] = stripPhotoDataUrl(child); } return clone; } function toTimestamp(value) { const parsed = Date.parse(String(value || '')); return Number.isFinite(parsed) ? parsed : 0; } function caseFreshness(record) { if (!record || typeof record !== 'object') return 0; const report = record.report || {}; return Math.max( toTimestamp(record.updatedAt), toTimestamp(record.statusUpdatedAt), toTimestamp(record.ts), toTimestamp(record.timestamp), toTimestamp(report.workflowLastUpdatedAt), toTimestamp(report.resolvedAt), toTimestamp(report.supervisor?.at), toTimestamp(report.enrichment?.enrichedAt), toTimestamp(report.submittedAt) ); } function mergeCaseLists(localCases, remoteCases) { const merged = new Map(); const allCases = [...(Array.isArray(localCases) ? localCases : []), ...(Array.isArray(remoteCases) ? remoteCases : [])]; allCases.forEach((record) => { if (!record || !record.caseId) return; const existing = merged.get(record.caseId); if (!existing) { merged.set(record.caseId, record); return; } const existingFreshness = caseFreshness(existing); const nextFreshness = caseFreshness(record); const nextPreferred = nextFreshness >= existingFreshness; merged.set( record.caseId, nextPreferred ? deepMerge(existing, record) : deepMerge(record, existing) ); }); return Array.from(merged.values()).sort((a, b) => { const aTs = caseFreshness(a); const bTs = caseFreshness(b); if (aTs !== bTs) return aTs - bTs; return String(a.caseId || '').localeCompare(String(b.caseId || '')); }); } async function request(path, options = {}) { const response = await fetch(`${API_PREFIX}${path}`, { headers: { 'Content-Type': 'application/json', ...(options.headers || {}), }, ...options, }); let payload = null; try { payload = await response.json(); } catch { payload = null; } if (!response.ok) { const errorMessage = payload?.error || `Request failed with HTTP ${response.status}`; throw new Error(errorMessage); } return payload; } async function health() { const payload = await request('/health', { method: 'GET' }); return payload?.ok === true; } async function getCases() { const payload = await request('/cases', { method: 'GET' }); return Array.isArray(payload?.cases) ? payload.cases : []; } async function getCase(caseId) { if (!caseId) return null; const payload = await request(`/cases/${encodeURIComponent(caseId)}`, { method: 'GET' }); return payload?.case || null; } async function upsertCase(record) { const payload = await request('/cases/upsert', { method: 'POST', body: JSON.stringify(stripPhotoDataUrl(record || {})), }); return payload?.case || null; } async function bulkUpsertCases(cases) { const payload = await request('/cases/bulk-upsert', { method: 'POST', body: JSON.stringify({ cases: Array.isArray(cases) ? cases.map((record) => stripPhotoDataUrl(record || {})) : [], }), }); return Array.isArray(payload?.cases) ? payload.cases : []; } async function patchCase(caseId, patch) { if (!caseId) throw new Error('caseId is required.'); const payload = await request(`/cases/${encodeURIComponent(caseId)}/patch`, { method: 'POST', body: JSON.stringify(stripPhotoDataUrl(patch || {})), }); return payload?.case || null; } async function submitSupervisorDecision(caseId, decisionPayload = {}) { if (!caseId) throw new Error('caseId is required.'); const payload = await request('/workflow/supervisor-decision', { method: 'POST', body: JSON.stringify(stripPhotoDataUrl({ caseId, ...decisionPayload, })), }); return payload || null; } async function submitCrewCloseout(caseId, closeoutPayload = {}) { if (!caseId) throw new Error('caseId is required.'); const payload = await request('/workflow/crew-closeout', { method: 'POST', body: JSON.stringify(stripPhotoDataUrl({ caseId, ...closeoutPayload, })), }); return payload || null; } async function submitDispatchPlan(dispatchPayload = {}) { const payload = await request('/workflow/dispatch-plan', { method: 'POST', body: JSON.stringify(stripPhotoDataUrl(dispatchPayload || {})), }); return payload || null; } async function resolveCase(caseId, resolvePayload = {}) { if (!caseId) throw new Error('caseId is required.'); const payload = await request('/workflow/resolve-case', { method: 'POST', body: JSON.stringify(stripPhotoDataUrl({ caseId, ...resolvePayload, })), }); return payload || null; } async function uploadCaseFile(caseId, fileName, fileBlob, contentType = '') { if (!caseId) throw new Error('caseId is required.'); if (!fileName) throw new Error('fileName is required.'); if (!(fileBlob instanceof Blob)) { throw new Error('fileBlob must be a Blob or File.'); } const response = await fetch( `${API_PREFIX}/box/upload-case-file?caseId=${encodeURIComponent(caseId)}&fileName=${encodeURIComponent(fileName)}`, { method: 'POST', headers: contentType ? { 'Content-Type': contentType } : {}, body: fileBlob, } ); let payload = null; try { payload = await response.json(); } catch { payload = null; } if (!response.ok) { throw new Error(payload?.error || `Request failed with HTTP ${response.status}`); } return payload?.upload || null; } async function hydrateCasesIntoLocalStorage(storageKey = 'submitted_cases') { if (typeof localStorage === 'undefined') return null; let localCases = []; try { localCases = JSON.parse(localStorage.getItem(storageKey) || '[]'); } catch { localCases = []; } const remoteCases = await getCases(); const mergedCases = mergeCaseLists(localCases, remoteCases); localStorage.setItem(storageKey, JSON.stringify(mergedCases)); const remoteIds = new Set(remoteCases.map((item) => item?.caseId).filter(Boolean)); const unsyncedLocal = localCases.filter((item) => item?.caseId && !remoteIds.has(item.caseId)); if (unsyncedLocal.length) { bulkUpsertCases(unsyncedLocal).catch((err) => { console.warn('[PotholeBackend] could not sync local-only cases:', err); }); } return mergedCases; } async function syncLocalStorageCases(storageKey = 'submitted_cases') { if (typeof localStorage === 'undefined') return []; try { const cases = JSON.parse(localStorage.getItem(storageKey) || '[]'); return bulkUpsertCases(cases); } catch (err) { console.warn('[PotholeBackend] could not read local cases for sync:', err); return []; } } globalScope.PotholeBackend = { health, getCases, getCase, upsertCase, bulkUpsertCases, patchCase, submitSupervisorDecision, submitDispatchPlan, submitCrewCloseout, resolveCase, uploadCaseFile, hydrateCasesIntoLocalStorage, syncLocalStorageCases, mergeCaseLists, }; })(window);