// Borradores de formularios: valores aun no guardados, persistidos en el navegador para no // perderlos al cambiar de pestaña/menu, al recargar (F5) o al vencer la sesion. // // - Clave = usuario + formulario + entidad -> nunca se mezclan entre usuarios ni registros. // - Nunca se guardan secretos: los campos que parecen contraseña/token/PIN se descartan siempre, // ademas de los que cada formulario declare en `exclude`. // - Son SOLO locales: nunca se mandan al backend como dato definitivo; guardar sigue siendo el // boton Guardar de cada formulario. // - `base` = huella de los valores del servidor desde los que se empezo a editar; si al volver la // huella actual difiere, el registro cambio en el servidor (conflicto) y se avisa. const PREFIX = 'plc_draft:v1:'; const TTL_MS = 7 * 24 * 60 * 60 * 1000; const SECRET_FIELD = /pass(word|wd)?|token|secret|^pin$|pin$/i; const DRAFT_FIELDS = { 'project-new': ['projectTypeId', 'priorityId', 'statusId', 'requestingDepartmentId', 'responsibleDepartmentId', 'startDate', 'estimatedEndDate', 'riskLevel'], 'project-info': ['editVersion', 'projectTypeId', 'priorityId', 'statusId', 'requestingDepartmentId', 'responsibleDepartmentId', 'startDate', 'estimatedEndDate', 'riskLevel'], 'activity-new': ['responsibleUserId', 'priorityId', 'statusId', 'startDate', 'dueDate', 'isMilestone', 'estimatedHours'], 'activity-info': ['editVersion', 'responsibleUserId', 'priorityId', 'statusId', 'startDate', 'dueDate', 'isMilestone', 'estimatedHours', 'progressPct'], 'task-new': [], 'task-edit': ['editVersion'], 'department-new': ['parentId'], 'department-edit': ['editVersion', 'parentId'], 'user-new': ['roleId', 'departmentId'], 'user-edit': ['editVersion', 'roleId', 'departmentId'], 'user-approve': ['editVersion', 'roleId', 'departmentId'], 'recurring-new': ['projectId', 'responsibleUserId', 'intervalUnit', 'intervalValue', 'nextDueDate', 'endDate', 'reminderDaysBefore', 'reminderHour'], 'recurring-edit': ['editVersion', 'projectId', 'responsibleUserId', 'intervalUnit', 'intervalValue', 'nextDueDate', 'endDate', 'reminderDaysBefore', 'reminderHour'], 'device-edit': ['editVersion', 'status', 'assignedUserId', 'departmentId'], 'note-edit': ['editVersion'], 'comment-edit': ['editVersion'], 'smtp-config': ['editVersion'], 'project-member-add': ['userId', 'roleInProject'], 'activity-convert': [], 'task-promote': ['projectId', 'responsibleUserId', 'intervalUnit', 'intervalValue', 'nextDueDate', 'endDate', 'reminderDaysBefore', 'reminderHour'], 'company-settings': ['editVersion'], }; export function draftAllowedFields(formId) { if (formId?.startsWith('catalog-')) return ['editVersion', 'color', 'level', 'sortOrder', 'isClosed']; return DRAFT_FIELDS[formId] || []; } const listeners = new Set(); function emit() { listeners.forEach((fn) => fn()); } export function subscribeDrafts(fn) { listeners.add(fn); return () => listeners.delete(fn); } // Otra pestaña del mismo navegador tambien puede crear/borrar borradores. if (typeof window !== 'undefined') { window.addEventListener('storage', (e) => { if (!e.key || e.key.startsWith(PREFIX)) emit(); }); } export function draftKey(userId, formId, entityId) { return `${PREFIX}${userId}:${formId}:${entityId ?? 'new'}`; } export function sanitizeDraftValues(values, exclude = [], allowed = Object.keys(values || {})) { const out = {}; for (const [k, v] of Object.entries(values || {})) { if (!allowed.includes(k) || exclude.includes(k) || SECRET_FIELD.test(k)) continue; if (v instanceof File || v instanceof Blob) continue; out[k] = v; } return out; } // Huella estable (orden de claves fijo) de los valores sanitizados. export function fingerprint(values, exclude = [], allowed) { const clean = sanitizeDraftValues(values, exclude, allowed); return JSON.stringify(Object.keys(clean).sort().map((k) => [k, clean[k] ?? ''])); } export function readDraft(key) { try { const raw = localStorage.getItem(key); if (!raw) return null; const draft = JSON.parse(raw); if (!draft?.savedAt || Date.now() - draft.savedAt > TTL_MS) { localStorage.removeItem(key); return null; } const formId = key.slice(PREFIX.length).split(':')[1]; const clean = sanitizeDraftValues(draft.values, [], draftAllowedFields(formId)); if (JSON.stringify(clean) !== JSON.stringify(draft.values)) { draft.values = clean; localStorage.setItem(key, JSON.stringify(draft)); } return draft; } catch { return null; } } export function writeDraft(key, draft) { try { localStorage.setItem(key, JSON.stringify({ ...draft, savedAt: Date.now() })); emit(); } catch { /* storage lleno o bloqueado: el formulario sigue funcionando sin borrador */ } } export function removeDraft(key) { try { if (localStorage.getItem(key) === null) return; localStorage.removeItem(key); emit(); } catch { /* nada que limpiar */ } } export function clearUserDrafts(userId) { if (!userId) return; const prefix = `${PREFIX}${userId}:`; try { for (const key of Object.keys(localStorage)) if (key.startsWith(prefix)) localStorage.removeItem(key); emit(); } catch { /* almacenamiento bloqueado */ } } // Todos los borradores vigentes del usuario: [{ key, formId, entityId, ...draft }]. export function listDrafts(userId) { if (!userId) return []; const prefix = `${PREFIX}${userId}:`; const out = []; try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (!key?.startsWith(prefix)) continue; const draft = readDraft(key); if (!draft) continue; const [formId, entityId] = key.slice(prefix.length).split(':'); out.push({ key, formId, entityId: entityId === 'new' ? null : entityId, ...draft }); } } catch { /* sin storage */ } return out; } // --- Editores abiertos con cambios (para el aviso al salir de una seccion) --- // key -> { label, leavesSection(nextLocation) } const activeEditors = new Map(); export function setActiveEditor(key, editor) { if (editor) activeEditors.set(key, editor); else activeEditors.delete(key); } export function getActiveEditors() { return [...activeEditors.entries()].map(([key, e]) => ({ key, ...e })); } // Mientras la sesion se cierra/vence, la navegacion a /login no debe pedir confirmacion: los // borradores ya quedaron guardados. let guardBypass = false; export function setGuardBypass(value) { guardBypass = value; } export function isGuardBypassed() { return guardBypass; }