Add multi-project support: projects entity, picker home page, project context
Projects become the top-level container; SOPs and Work Packages belong to one. Backend: - New projects table + CRUD (/api/projects). - sops.project_id (FK, cascade) and work_packages.project_id added; list/latest/metrics endpoints accept a project_id filter. Front end (now under html/): - project-data.js: shared API-first ProjectData adapter with localStorage fallback + active-project helpers. - Home page: removed "About This Suite"; added a Project picker (create / use sample / select). Tool cards stay hidden until a project is active and carry &project=<id>; hero shows the active project. - Suite reads ?project, resolves it, shows it in the header, and prefills the SOP project fields; passes &project into the WP-creator iframe. - WP creator stamps projectId onto saved packages. SOP/WP localStorage is not yet namespaced per project (next step). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
80
html/project-data.js
Normal file
80
html/project-data.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/* 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) {}
|
||||
}
|
||||
};
|
||||
|
||||
global.ProjectData = ProjectData;
|
||||
})(window);
|
||||
Reference in New Issue
Block a user