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:
2026-06-15 15:39:45 -07:00
parent 4d111d608d
commit 3c40b58ff8
9 changed files with 437 additions and 39 deletions

View File

@@ -130,8 +130,9 @@ window.addEventListener('DOMContentLoaded',()=>{
updateStepUI();
updateProjectDisplay();
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard from the home page cards.
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard | ?project=<id> from the home page.
const params = new URLSearchParams(window.location.search);
applyProjectContext(params.get('project'));
const tab = params.get('tab');
if(params.get('view') === 'dashboard') switchTool('wp');
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
@@ -284,8 +285,11 @@ function renderWPTab(){
gate.style.display = 'none';
frame.style.display = 'block';
// Reload each time so the creator picks up the latest SOP from localStorage.
const wantDash = new URLSearchParams(window.location.search).get('view') === 'dashboard';
frame.src = 'wp-creation-index.html?embedded=1' + (wantDash ? '&view=dashboard' : '') + '&t=' + Date.now();
const sp = new URLSearchParams(window.location.search);
const wantDash = sp.get('view') === 'dashboard';
const projId = sp.get('project') || (activeProject && activeProject.id) || '';
frame.src = 'wp-creation-index.html?embedded=1' + (wantDash ? '&view=dashboard' : '')
+ (projId ? '&project=' + encodeURIComponent(projId) : '') + '&t=' + Date.now();
}else{
gate.style.display = 'block';
frame.style.display = 'none';
@@ -297,8 +301,40 @@ function onSOPReady(){
if(currentTool === 'wp') renderWPTab();
}
// Active project comes from the home page (?project=<id> + ProjectData.getActive()).
// When the SOP's project fields are still empty, prefill them from the project
// record so the SOP is authored against the chosen project.
let activeProject = null;
function applyProjectContext(projectId){
try { if(typeof ProjectData !== 'undefined') activeProject = ProjectData.getActive(); } catch(e){}
if(projectId && (!activeProject || activeProject.id !== projectId)){
// Deep-linked to a project we don't have cached yet — resolve it, then prefill.
try {
if(typeof ProjectData !== 'undefined'){
ProjectData.get(projectId).then(p => { if(p){ activeProject = p; ProjectData.setActive(p); prefillProjectFields(); updateProjectDisplay(); } });
}
} catch(e){}
}
prefillProjectFields();
}
function prefillProjectFields(){
if(!activeProject) return;
const set = (id,v)=>{ const el=document.getElementById(id); if(el && !el.value && v) el.value = v; };
set('proj_name', activeProject.name);
set('proj_number', activeProject.number);
set('proj_client', activeProject.client);
set('proj_division', activeProject.division);
set('proj_site', activeProject.site);
if(typeof state !== 'undefined' && state.project){
state.project.name = state.project.name || activeProject.name || '';
state.project.number = state.project.number || activeProject.number || '';
state.project.client = state.project.client || activeProject.client || '';
state.project.division = state.project.division || activeProject.division || '';
state.project.site = state.project.site || activeProject.site || '';
}
}
function updateProjectDisplay(){
const projName = document.getElementById('proj_name')?.value || 'Project';
const projName = document.getElementById('proj_name')?.value || (activeProject && activeProject.name) || 'Project';
const display = document.getElementById('project-display');
if(display) display.textContent = sopComplete ? `${projName} (SOP Ready)` : projName;
}