Files
Project-SDE-WP-Suite/html/project-data.js
n.siegfried 5ad3ffa58e Per-project access control + UI/feedback/admin refinements
Access control:
- project_members table; non-admins only see/operate on assigned
  projects (enforced across projects, SOPs, work packages — 403 else),
  admins bypass. Creating a project auto-grants its creator access.
- Admin API to get/set a user's project assignments, plus a checkbox
  assignment dialog in the Admin Console user list.

UI / workflow:
- Login page: drop the "Prime Controls" wordmark next to the logo.
- SOP tool: remove emoji icons from buttons and nav tabs.
- Rename "Step Comments" to "Feedback"; the author auto-populates
  (read-only) from the signed-in user.
- Move usage-log viewing to the Admin Console; add an admin card that
  lists all feedback/comments (who, what, page + step, when).
- Sample project name -> "Micron FMCS Install (sample)".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:38:56 -07:00

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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; }
};
// 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);