SOP and Work Package localStorage keys are now namespaced by the active project via ProjectData.key(base) -> base+'__'+<projectId> (SK() in the suite, wpKey() in the creator), so switching projects shows that project's own SOP and packages. project-data.js runs a one-time discard of the legacy un-namespaced keys (guarded by wp_ns_migrated_v1), per the chosen approach. Active project is resolved before the store loads in both the suite and the creator so namespaced keys resolve correctly, including standalone deep-links. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
4.5 KiB
JavaScript
97 lines
4.5 KiB
JavaScript
/* 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, '<').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 — INC Construction Work Packages', 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; }
|
|
};
|
|
|
|
// 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);
|