Compare commits
1 Commits
Micron-Ass
...
shared-dat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e51024466b |
@@ -595,7 +595,10 @@
|
|||||||
if(info) info.innerHTML = `<div class="proj-active">✓ Active project: <strong>${esc(active.name||'')}</strong>${active.number?' ('+esc(active.number)+')':''}
|
if(info) info.innerHTML = `<div class="proj-active">✓ Active project: <strong>${esc(active.name||'')}</strong>${active.number?' ('+esc(active.number)+')':''}
|
||||||
<button class="link-like" onclick="clearActiveProject()">change</button></div>`;
|
<button class="link-like" onclick="clearActiveProject()">change</button></div>`;
|
||||||
|
|
||||||
reflectSOPStatus(active);
|
// Pull the project's shared SOP from the server into the local cache first,
|
||||||
|
// so the SOP "Complete / Review" status reflects what other users have done.
|
||||||
|
if(ProjectData.pullProject){ ProjectData.pullProject(active.id).then(()=>reflectSOPStatus(active)).catch(()=>reflectSOPStatus(active)); }
|
||||||
|
else reflectSOPStatus(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearActiveProject(){ ProjectData.setActive(null); renderProjectPicker(); applyActiveProject(); }
|
function clearActiveProject(){ ProjectData.setActive(null); renderProjectPicker(); applyActiveProject(); }
|
||||||
@@ -704,22 +707,36 @@
|
|||||||
r.readAsText(f);
|
r.readAsText(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadComments() {
|
function renderComments() {
|
||||||
const saved = localStorage.getItem('wp_suite_index_comments');
|
|
||||||
if (saved) allComments = JSON.parse(saved);
|
|
||||||
|
|
||||||
const list = document.getElementById('comments-list');
|
const list = document.getElementById('comments-list');
|
||||||
if (allComments.length === 0) {
|
if (allComments.length === 0) {
|
||||||
list.innerHTML = '<div style="color: var(--cds-text-secondary); font-style: italic; font-size: 12px;">No feedback yet. Be the first to share!</div>';
|
list.innerHTML = '<div style="color: var(--cds-text-secondary); font-style: italic; font-size: 12px;">No feedback yet. Be the first to share!</div>';
|
||||||
} else {
|
} else {
|
||||||
list.innerHTML = allComments.map(c => `
|
list.innerHTML = allComments.map(c => `
|
||||||
<div class="comment-item">
|
<div class="comment-item">
|
||||||
<div class="comment-meta"><strong>${c.name}</strong> • ${c.timestamp}</div>
|
<div class="comment-meta"><strong>${(c.name||'Anonymous').replace(/</g,'<')}</strong> • ${c.timestamp||''}</div>
|
||||||
<div class="comment-text">${c.text.replace(/</g,'<').replace(/>/g,'>')}</div>
|
<div class="comment-text">${(c.text||'').replace(/</g,'<').replace(/>/g,'>')}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadComments() {
|
||||||
|
// Server is authoritative (so feedback is shared across users); fall back to
|
||||||
|
// the local cache if the API is unreachable.
|
||||||
|
const saved = localStorage.getItem('wp_suite_index_comments');
|
||||||
|
if (saved) { try { allComments = JSON.parse(saved) || []; } catch(e) { allComments = []; } }
|
||||||
|
renderComments();
|
||||||
|
fetch('/api/comments?source=home_feedback', { headers: { 'Accept': 'application/json' } })
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(rows => {
|
||||||
|
if (Array.isArray(rows)) {
|
||||||
|
allComments = rows.map(c => ({ name: c.author, text: c.text, timestamp: c.created_at ? new Date(c.created_at).toLocaleString() : '' }));
|
||||||
|
renderComments();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -81,6 +81,107 @@
|
|||||||
key: function (base) { var id = this.getActiveId(); return id ? base + '__' + id : base; }
|
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
|
// 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.)
|
// global state can't leak across projects. (User chose: discard, don't migrate.)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -148,15 +148,25 @@ window.addEventListener('DOMContentLoaded',()=>{
|
|||||||
// Resolve the active project FIRST so per-project storage keys are correct
|
// Resolve the active project FIRST so per-project storage keys are correct
|
||||||
// before we restore this project's SOP.
|
// before we restore this project's SOP.
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const projId = params.get('project') || (typeof ProjectData!=='undefined' && ProjectData.getActiveId && ProjectData.getActiveId()) || '';
|
||||||
applyProjectContext(params.get('project'));
|
applyProjectContext(params.get('project'));
|
||||||
restoreSavedSOP();
|
|
||||||
updateStepUI();
|
|
||||||
updateProjectDisplay();
|
|
||||||
|
|
||||||
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard from the home page.
|
// Pull the project's shared SOP from the server into the local cache, THEN
|
||||||
const tab = params.get('tab');
|
// restore it. Falls back to the local cache if offline.
|
||||||
if(params.get('view') === 'dashboard') switchTool('dashboard');
|
function afterPull(){
|
||||||
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
restoreSavedSOP();
|
||||||
|
updateStepUI();
|
||||||
|
updateProjectDisplay();
|
||||||
|
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard from the home page.
|
||||||
|
const tab = params.get('tab');
|
||||||
|
if(params.get('view') === 'dashboard') switchTool('dashboard');
|
||||||
|
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
||||||
|
}
|
||||||
|
if(projId && typeof ProjectData!=='undefined' && ProjectData.pullProject){
|
||||||
|
ProjectData.pullProject(projId).then(afterPull).catch(afterPull);
|
||||||
|
} else {
|
||||||
|
afterPull();
|
||||||
|
}
|
||||||
|
|
||||||
track('app_open');
|
track('app_open');
|
||||||
|
|
||||||
@@ -842,6 +852,12 @@ function completeSOP(){
|
|||||||
localStorage.setItem(SK('wp_suite_sop_complete'), '1');
|
localStorage.setItem(SK('wp_suite_sop_complete'), '1');
|
||||||
} catch(e){}
|
} catch(e){}
|
||||||
|
|
||||||
|
// Share the SOP to the server so every user of this project gets it.
|
||||||
|
try {
|
||||||
|
const pid = (typeof ProjectData!=='undefined' && ProjectData.getActiveId && ProjectData.getActiveId()) || sop.projectId || '';
|
||||||
|
if(pid && ProjectData.pushSOP) ProjectData.pushSOP(pid, sop, state);
|
||||||
|
} catch(e){}
|
||||||
|
|
||||||
track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length});
|
track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length});
|
||||||
|
|
||||||
// Hand the SOP to the embedded Work Package Creator and unlock its tab (in case
|
// Hand the SOP to the embedded Work Package Creator and unlock its tab (in case
|
||||||
|
|||||||
@@ -687,6 +687,7 @@ function savePackage(view){
|
|||||||
const ix=savedPackages.findIndex(p=>p.id===pkg.id);
|
const ix=savedPackages.findIndex(p=>p.id===pkg.id);
|
||||||
if(ix>=0) savedPackages[ix]=pkg; else savedPackages.push(pkg);
|
if(ix>=0) savedPackages[ix]=pkg; else savedPackages.push(pkg);
|
||||||
editingId=pkg.id; saveStore(); renderSavedList(); track('package_saved',{status:pkg.status});
|
editingId=pkg.id; saveStore(); renderSavedList(); track('package_saved',{status:pkg.status});
|
||||||
|
if(typeof ProjectData!=='undefined' && ProjectData.pushWP) ProjectData.pushWP(pkg, activeProjectId); // share to server
|
||||||
document.getElementById('loadingOverlay').classList.add('active');
|
document.getElementById('loadingOverlay').classList.add('active');
|
||||||
setTimeout(()=>{ document.getElementById('loadingOverlay').classList.remove('active'); if(view) renderPackage(pkg); }, 400);
|
setTimeout(()=>{ document.getElementById('loadingOverlay').classList.remove('active'); if(view) renderPackage(pkg); }, 400);
|
||||||
}
|
}
|
||||||
@@ -846,8 +847,8 @@ function renderSavedList(){
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
function viewPackage(i){ if(savedPackages[i]) renderPackage(savedPackages[i]); }
|
function viewPackage(i){ if(savedPackages[i]) renderPackage(savedPackages[i]); }
|
||||||
function deletePackage(i){ const p=savedPackages[i]; if(!p) return; if(!confirm('Delete work package "'+(p.number||p.subject||'untitled')+'"? This cannot be undone.')) return; savedPackages.splice(i,1); saveStore(); renderSavedList(); track('package_deleted'); }
|
function deletePackage(i){ const p=savedPackages[i]; if(!p) return; if(!confirm('Delete work package "'+(p.number||p.subject||'untitled')+'"? This cannot be undone.')) return; const delId=p.id; savedPackages.splice(i,1); saveStore(); renderSavedList(); track('package_deleted'); if(typeof ProjectData!=='undefined' && ProjectData.removeWP) ProjectData.removeWP(delId); }
|
||||||
function clearSaved(){ if(!savedPackages.length) return; if(!confirm('Delete all '+savedPackages.length+' saved packages on this device?')) return; savedPackages=[]; saveStore(); renderSavedList(); }
|
function clearSaved(){ if(!savedPackages.length) return; if(!confirm('Delete all '+savedPackages.length+' saved packages?')) return; const ids=savedPackages.map(p=>p.id); savedPackages=[]; saveStore(); renderSavedList(); if(typeof ProjectData!=='undefined' && ProjectData.removeWP) ids.forEach(id=>ProjectData.removeWP(id)); }
|
||||||
function editPackage(i){ const p=savedPackages[i]; if(!p) return; editingId=p.id; loadPackageIntoForm(p); }
|
function editPackage(i){ const p=savedPackages[i]; if(!p) return; editingId=p.id; loadPackageIntoForm(p); }
|
||||||
function loadExample(){ loadPackageIntoForm(JSON.parse(JSON.stringify(EXAMPLE_PKG))); editingId=null; toast('Example work package loaded'); track('example_loaded'); }
|
function loadExample(){ loadPackageIntoForm(JSON.parse(JSON.stringify(EXAMPLE_PKG))); editingId=null; toast('Example work package loaded'); track('example_loaded'); }
|
||||||
function loadPackageIntoForm(p){
|
function loadPackageIntoForm(p){
|
||||||
@@ -916,6 +917,7 @@ function duplicateWP(){
|
|||||||
savedPackages.push(c); made.push(c);
|
savedPackages.push(c); made.push(c);
|
||||||
}
|
}
|
||||||
editingId=null; saveStore(); renderSavedList();
|
editingId=null; saveStore(); renderSavedList();
|
||||||
|
if(typeof ProjectData!=='undefined' && ProjectData.pushWP) made.forEach(m=>ProjectData.pushWP(m, activeProjectId)); // share to server
|
||||||
toast('Created '+n+' duplicate'+(n>1?'s':''));
|
toast('Created '+n+' duplicate'+(n>1?'s':''));
|
||||||
track('wp_duplicated',{count:n});
|
track('wp_duplicated',{count:n});
|
||||||
alert('Created '+n+' duplicate'+(n>1?'s':'')+':\n\n• '+made.map(m=>m.number).join('\n• ')+'\n\nThey are in the Saved Work Packages list — edit each as needed.');
|
alert('Created '+n+' duplicate'+(n>1?'s':'')+':\n\n• '+made.map(m=>m.number).join('\n• ')+'\n\nThey are in the Saved Work Packages list — edit each as needed.');
|
||||||
@@ -951,12 +953,14 @@ function exportPackages(){
|
|||||||
// Data adapter: localStorage today. In Phase 2 swap list()/issue()/setStatus()
|
// Data adapter: localStorage today. In Phase 2 swap list()/issue()/setStatus()
|
||||||
// bodies for fetch() calls to /api/wps — the dashboard UI doesn't change.
|
// bodies for fetch() calls to /api/wps — the dashboard UI doesn't change.
|
||||||
const WPData = {
|
const WPData = {
|
||||||
list(){ return savedPackages.slice(); }, // → GET /api/wps
|
list(){ return savedPackages.slice(); }, // hydrated from GET /api/wps on boot
|
||||||
get(id){ return savedPackages.find(p=>p.id===id); }, // → GET /api/wps/{id}
|
get(id){ return savedPackages.find(p=>p.id===id); },
|
||||||
issue(id){ const p=savedPackages.find(x=>x.id===id); if(!p) return false;
|
issue(id){ const p=savedPackages.find(x=>x.id===id); if(!p) return false;
|
||||||
p.status='Issued'; p.issuedAt=new Date().toISOString(); p.updatedAt=p.issuedAt; saveStore(); return true; }, // → POST /api/wps/{id}/issue
|
p.status='Issued'; p.issuedAt=new Date().toISOString(); p.updatedAt=p.issuedAt; saveStore();
|
||||||
|
if(typeof ProjectData!=='undefined' && ProjectData.pushWP) ProjectData.pushWP(p, activeProjectId); return true; },
|
||||||
setStatus(id,status){ const p=savedPackages.find(x=>x.id===id); if(!p) return false;
|
setStatus(id,status){ const p=savedPackages.find(x=>x.id===id); if(!p) return false;
|
||||||
p.status=status; p.updatedAt=new Date().toISOString(); saveStore(); return true; }, // → POST /api/wps/{id}/status
|
p.status=status; p.updatedAt=new Date().toISOString(); saveStore();
|
||||||
|
if(typeof ProjectData!=='undefined' && ProjectData.pushWP) ProjectData.pushWP(p, activeProjectId); return true; },
|
||||||
};
|
};
|
||||||
|
|
||||||
let dashFilter={status:'',discipline:'',q:'',flag:''};
|
let dashFilter={status:'',discipline:'',q:'',flag:''};
|
||||||
@@ -1144,10 +1148,10 @@ document.querySelectorAll('#status-group .radio-pill').forEach(p=>p.addEventList
|
|||||||
ProjectData.setActive(cached && cached.id===activeProjectId ? cached : { id: activeProjectId });
|
ProjectData.setActive(cached && cached.id===activeProjectId ? cached : { id: activeProjectId });
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
loadStore();
|
function bootSOP(){
|
||||||
(function bootSOP(){
|
|
||||||
// When embedded in the Suite, hide the SOP import/sample controls (SOP is injected)
|
// When embedded in the Suite, hide the SOP import/sample controls (SOP is injected)
|
||||||
// and prefer the SOP the Suite just completed (persisted to localStorage).
|
// and prefer the SOP the Suite just completed (persisted to localStorage, which
|
||||||
|
// we've already hydrated from the server for this project).
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
if(params.get('embedded')) document.body.classList.add('embedded');
|
if(params.get('embedded')) document.body.classList.add('embedded');
|
||||||
try {
|
try {
|
||||||
@@ -1162,11 +1166,22 @@ loadStore();
|
|||||||
// state so it's clear the project's SOP must be completed first.
|
// state so it's clear the project's SOP must be completed first.
|
||||||
if(activeProjectId){ SOP=null; renderCtxBar(); newPackage(); }
|
if(activeProjectId){ SOP=null; renderCtxBar(); newPackage(); }
|
||||||
else { loadSampleSOP(); }
|
else { loadSampleSOP(); }
|
||||||
})();
|
}
|
||||||
setRadio('status','Draft');
|
function bootData(){
|
||||||
renderSavedList();
|
loadStore(); // reads the localStorage cache (hydrated from the server below)
|
||||||
cmtInit();
|
bootSOP();
|
||||||
// Deep-link: open straight to the dashboard when requested (?view=dashboard or #dashboard).
|
setRadio('status','Draft');
|
||||||
(function(){ const p=new URLSearchParams(location.search); if(p.get('view')==='dashboard' || location.hash==='#dashboard'){ showDashboard(); } })();
|
renderSavedList();
|
||||||
|
cmtInit();
|
||||||
|
// Deep-link: open straight to the dashboard when requested (?view=dashboard or #dashboard).
|
||||||
|
const p=new URLSearchParams(location.search); if(p.get('view')==='dashboard' || location.hash==='#dashboard'){ showDashboard(); }
|
||||||
|
track('app_open');
|
||||||
|
}
|
||||||
window.addEventListener('hashchange',()=>{ if(location.hash==='#dashboard') showDashboard(); });
|
window.addEventListener('hashchange',()=>{ if(location.hash==='#dashboard') showDashboard(); });
|
||||||
track('app_open');
|
// Pull this project's shared SOP + Work Packages from the server first, then boot
|
||||||
|
// off the refreshed cache. Falls back to whatever is cached locally if offline.
|
||||||
|
if(activeProjectId && typeof ProjectData!=='undefined' && ProjectData.pullProject){
|
||||||
|
ProjectData.pullProject(activeProjectId).then(bootData).catch(bootData);
|
||||||
|
} else {
|
||||||
|
bootData();
|
||||||
|
}
|
||||||
|
|||||||
@@ -407,13 +407,15 @@ def upsert_sop(body: SopIn, user: models.User = Depends(auth.get_current_user),
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/sops")
|
@app.get("/api/sops")
|
||||||
def list_sops(project_id: Optional[str] = Query(None), user: models.User = Depends(auth.get_current_user), db: Session = Depends(get_db)):
|
def list_sops(project_id: Optional[str] = Query(None), full: bool = Query(False), user: models.User = Depends(auth.get_current_user), db: Session = Depends(get_db)):
|
||||||
stmt = select(models.Sop)
|
stmt = select(models.Sop)
|
||||||
if project_id:
|
if project_id:
|
||||||
stmt = stmt.where(models.Sop.project_id == project_id)
|
stmt = stmt.where(models.Sop.project_id == project_id)
|
||||||
stmt = scope_to_access(stmt, models.Sop.project_id, db, user)
|
stmt = scope_to_access(stmt, models.Sop.project_id, db, user)
|
||||||
rows = db.scalars(stmt.order_by(models.Sop.updated_at.desc())).all()
|
rows = db.scalars(stmt.order_by(models.Sop.updated_at.desc())).all()
|
||||||
return [s.summary() for s in rows]
|
# full=true includes the data JSON (the whole SOP document) for hydration;
|
||||||
|
# the default summary view stays lean for listing.
|
||||||
|
return [(s.to_dict() if full else s.summary()) for s in rows]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/sops/latest")
|
@app.get("/api/sops/latest")
|
||||||
@@ -480,6 +482,7 @@ def list_wps(
|
|||||||
sop_id: Optional[str] = Query(None),
|
sop_id: Optional[str] = Query(None),
|
||||||
parent_id: Optional[str] = Query(None),
|
parent_id: Optional[str] = Query(None),
|
||||||
status: Optional[str] = Query(None),
|
status: Optional[str] = Query(None),
|
||||||
|
full: bool = Query(False),
|
||||||
user: models.User = Depends(auth.get_current_user),
|
user: models.User = Depends(auth.get_current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -494,7 +497,9 @@ def list_wps(
|
|||||||
stmt = stmt.where(models.WorkPackage.status == status)
|
stmt = stmt.where(models.WorkPackage.status == status)
|
||||||
stmt = scope_to_access(stmt, models.WorkPackage.project_id, db, user)
|
stmt = scope_to_access(stmt, models.WorkPackage.project_id, db, user)
|
||||||
rows = db.scalars(stmt.order_by(models.WorkPackage.updated_at.desc())).all()
|
rows = db.scalars(stmt.order_by(models.WorkPackage.updated_at.desc())).all()
|
||||||
return [w.summary() for w in rows]
|
# full=true includes the data JSON (full package document) so the creator can
|
||||||
|
# rehydrate everything in one request; default stays lean for listing.
|
||||||
|
return [(w.to_dict() if full else w.summary()) for w in rows]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/wps/metrics")
|
@app.get("/api/wps/metrics")
|
||||||
|
|||||||
Reference in New Issue
Block a user