/* Shared project layer for the Work Package Suite. Projects are the top-level container — every SOP and Work Package belongs to one. Project records live in the SQL database (via /api/projects); this adapter is API-first and falls back to a localStorage mirror so the suite still works in local dev / offline. Included by the home page and the suite. */ (function (global) { 'use strict'; var API = '/api'; var LS_PROJECTS = 'wp_projects'; // local mirror of the project list var LS_ACTIVE = 'wp_active_project'; // active project id var LS_ACTIVE_OBJ = 'wp_active_project_obj'; function uid() { return 'proj_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } function esc(v) { return v == null ? '' : String(v).replace(/&/g, '&').replace(//g, '>'); } function readLocal() { try { return JSON.parse(localStorage.getItem(LS_PROJECTS) || '[]') || []; } catch (e) { return []; } } function writeLocal(list) { try { localStorage.setItem(LS_PROJECTS, JSON.stringify(list)); } catch (e) {} } function cacheUpsert(p) { var list = readLocal(); var ix = list.findIndex(function (x) { return x.id === p.id; }); if (ix >= 0) list[ix] = p; else list.unshift(p); writeLocal(list); } function cacheRemove(id) { writeLocal(readLocal().filter(function (x) { return x.id !== id; })); } var SAMPLE_PROJECT = { name: 'Micron FMCS Install (sample)', number: '26-67-008', client: 'Micron Technology, Inc.', division: 'Semiconductor', site: 'Boise, ID — Fab', sample: true }; var ProjectData = { SAMPLE: SAMPLE_PROJECT, esc: esc, // Returns the project list. Tries the API; falls back to the local mirror. list: function () { return fetch(API + '/projects', { headers: { 'Accept': 'application/json' } }) .then(function (r) { if (!r.ok) throw 0; return r.json(); }) .then(function (rows) { writeLocal(rows); return rows; }) .catch(function () { return readLocal(); }); }, get: function (id) { return fetch(API + '/projects/' + encodeURIComponent(id)) .then(function (r) { if (!r.ok) throw 0; return r.json(); }) .catch(function () { return readLocal().find(function (x) { return x.id === id; }) || null; }); }, // Create or update. Assigns an id when new. Mirrors to localStorage either way. save: function (p) { if (!p.id) p.id = uid(); return fetch(API + '/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(p) }) .then(function (r) { if (!r.ok) throw 0; return r.json(); }) .then(function (saved) { cacheUpsert(saved); return saved; }) .catch(function () { cacheUpsert(p); return p; }); // offline / no API → local only }, remove: function (id) { return fetch(API + '/projects/' + encodeURIComponent(id), { method: 'DELETE' }) .then(function () { cacheRemove(id); }) .catch(function () { cacheRemove(id); }); }, // ── active project context ──────────────────────────────────────────────── getActiveId: function () { try { return localStorage.getItem(LS_ACTIVE) || ''; } catch (e) { return ''; } }, getActive: function () { try { return JSON.parse(localStorage.getItem(LS_ACTIVE_OBJ) || 'null'); } catch (e) { return null; } }, setActive: function (p) { try { if (p) { localStorage.setItem(LS_ACTIVE, p.id); localStorage.setItem(LS_ACTIVE_OBJ, JSON.stringify(p)); } else { localStorage.removeItem(LS_ACTIVE); localStorage.removeItem(LS_ACTIVE_OBJ); } } catch (e) {} }, // Per-project namespacing for the SOP/WP localStorage keys, e.g. // key('wp_iwp_v1') → 'wp_iwp_v1__proj_ab12' // Falls back to the bare key when no project is active. key: function (base) { var id = this.getActiveId(); return id ? base + '__' + id : base; } }; // ── Server sync for SOPs and Work Packages ───────────────────────────────── // SOPs and WPs are authoritative on the server (so every user of a project sees // the same data). To avoid rewriting the two apps, we keep their existing // localStorage keys as a per-browser CACHE: pullProject() hydrates those exact // keys from the API on page load, and the push* helpers write through to the // API whenever the apps save. The apps' own (synchronous) reads are unchanged. function nsKey(base, id) { return id ? base + '__' + id : base; } function currentUser() { try { return (window.WP_USER && (window.WP_USER.username || window.WP_USER.full_name)) || ''; } catch (e) { return ''; } } // A saved Work Package is a flat object in the browser; the API splits it into // promoted columns + a `data` blob. We store the whole flat object in `data` // for perfect round-tripping, and mirror the few fields the API promotes. function pkgToServer(p, projectId) { return { id: p.id, project_id: p.projectId || projectId || null, parent_id: p.instanceOf || null, number: p.number || '', subject: p.subject || '', type: p.type || '', status: p.status || 'Draft', created_by: p.createdBy || currentUser(), data: p }; } function serverToPkg(row) { var p = Object.assign({}, row.data || {}); // full flat object lives in data p.id = row.id; p.projectId = row.project_id || p.projectId || ''; if (row.number) p.number = row.number; if (row.subject != null) p.subject = row.subject; if (row.type != null) p.type = row.type; if (row.status) p.status = row.status; // honor server-side status changes if (row.parent_id) p.instanceOf = row.parent_id; return p; } // Pull this project's SOP + WPs from the API into the localStorage keys the // apps read. Resolves even on failure (offline / no API) so boot continues. ProjectData.pullProject = function (projectId) { if (!projectId) return Promise.resolve(); var jobs = []; jobs.push( fetch(API + '/sops/latest?complete=true&project_id=' + encodeURIComponent(projectId), { headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (sopRow) { if (sopRow && sopRow.data) { var d = sopRow.data; // { sop, state } as written by pushSOP if (d.sop) localStorage.setItem(nsKey('wp_suite_sop', projectId), JSON.stringify(d.sop)); if (d.state) localStorage.setItem(nsKey('wp_suite_state', projectId), JSON.stringify(d.state)); localStorage.setItem(nsKey('wp_suite_sop_complete', projectId), '1'); } }).catch(function () {}) ); jobs.push( fetch(API + '/wps?full=true&project_id=' + encodeURIComponent(projectId), { headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (rows) { if (Array.isArray(rows)) { localStorage.setItem(nsKey('wp_iwp_v1', projectId), JSON.stringify(rows.map(serverToPkg))); } }).catch(function () {}) ); return Promise.all(jobs).then(function () {}); }; // Write a completed SOP (plus the builder's raw state) to the API. Uses a // deterministic id per project so re-completing updates the same row. ProjectData.pushSOP = function (projectId, sop, state) { if (!projectId) return Promise.resolve(null); var body = { id: 'sop__' + projectId, project_id: projectId, name: (sop && sop.project && sop.project.name) || 'SOP', number: (sop && sop.project && sop.project.number) || '', complete: true, created_by: currentUser(), data: { sop: sop, state: state } }; return fetch(API + '/sops', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); }; // Upsert a single Work Package to the API (fire-and-forget from the caller's // perspective; the local cache is the source of truth for immediate rendering). ProjectData.pushWP = function (p, projectId) { if (!p || !p.id) return Promise.resolve(null); return fetch(API + '/wps', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(pkgToServer(p, projectId)) }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); }; ProjectData.removeWP = function (id) { if (!id) return Promise.resolve(); return fetch(API + '/wps/' + encodeURIComponent(id), { method: 'DELETE' }) .then(function () {}).catch(function () {}); }; // One-time discard of pre-multi-project (un-namespaced) SOP/WP data so stale // global state can't leak across projects. (User chose: discard, don't migrate.) try { if (!localStorage.getItem('wp_ns_migrated_v1')) { ['wp_suite_sop', 'wp_suite_state', 'wp_suite_sop_complete', 'wp_iwp_v1'].forEach(function (k) { try { localStorage.removeItem(k); } catch (e) {} }); localStorage.setItem('wp_ns_migrated_v1', '1'); } } catch (e) {} global.ProjectData = ProjectData; })(window);