Reintegrates C-West8's "storing data in DB instead of client only"
(commit e5102446 on shared-data) on top of the BIM / per-package work.
localStorage becomes a per-browser cache; the server is authoritative.
- project-data.js: pullProject() hydrates the apps' existing localStorage
keys from the API on load; pushSOP()/pushWP()/removeWP() write through
on save/delete. WPs store the whole flat object in `data`, so BIM
fields, kind, and projectLinks round-trip intact.
- index.html: home pulls the project before showing SOP status; feedback
loads from /api/comments (server-authoritative, local fallback).
- work-package-suite-app.js: pull-then-restore on boot; completeSOP
pushes the SOP to the server.
- wp-creation-app.js: save/duplicate/issue/setStatus push; delete/clear
remove; boot pulls from the server first, then boots off the cache.
- server/app.py: /api/sops and /api/wps take full=true to return the
data JSON for one-request hydration (list stays lean by default).
Co-Authored-By: C-West8 <125926137+C-West8@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
743 lines
26 KiB
HTML
743 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Work Package Suite — Prime Controls</title>
|
|
<script src="auth-guard.js"></script>
|
|
<link rel="icon" href="favicon.ico" sizes="any">
|
|
<link rel="stylesheet" href="theme-light.css">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--cds-background);
|
|
color: var(--cds-text-primary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* HEADER */
|
|
.header {
|
|
background: var(--cds-layer);
|
|
padding: 1.5rem 2rem;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
border-bottom: 1px solid var(--cds-border-subtle);
|
|
}
|
|
|
|
.header-content {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
text-decoration: none;
|
|
color: var(--cds-text-primary);
|
|
background: white;
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.logo img {
|
|
height: 32px;
|
|
width: auto;
|
|
}
|
|
|
|
.logo:hover { opacity: 0.9; }
|
|
|
|
.header-spacer { flex: 1; }
|
|
|
|
.header-nav {
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.header-nav a {
|
|
color: var(--cds-text-secondary);
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.header-nav a:hover { color: var(--cds-text-primary); }
|
|
|
|
/* CONTAINER */
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 3rem 2rem;
|
|
}
|
|
|
|
/* HERO */
|
|
.hero {
|
|
text-align: center;
|
|
margin-bottom: 4rem;
|
|
}
|
|
|
|
.hero h1 {
|
|
font-size: 2.625rem;
|
|
font-weight: 300;
|
|
margin-bottom: 1rem;
|
|
color: var(--cds-text-primary);
|
|
}
|
|
|
|
.hero p {
|
|
font-size: 1.125rem;
|
|
color: var(--cds-text-secondary);
|
|
margin-bottom: 2rem;
|
|
max-width: 700px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
|
|
/* CARDS */
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.card {
|
|
background: var(--cds-layer);
|
|
border: 1px solid var(--cds-border-subtle);
|
|
border-radius: 4px;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
transition: all 0.2s;
|
|
text-decoration: none;
|
|
color: var(--cds-text-primary);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.card:hover {
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
|
|
transform: translateY(-2px);
|
|
border-color: var(--cds-button-primary);
|
|
}
|
|
|
|
.card-badge {
|
|
display: inline-block;
|
|
background: var(--cds-button-primary);
|
|
color: white;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
width: fit-content;
|
|
}
|
|
|
|
.card h3 {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.card p {
|
|
color: var(--cds-text-secondary);
|
|
margin-bottom: 1.5rem;
|
|
flex: 1;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.card-button {
|
|
display: inline-block;
|
|
background: var(--cds-button-primary);
|
|
color: white;
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 3px;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
transition: background 0.2s;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.card-button:hover {
|
|
background: var(--cds-hover-primary);
|
|
}
|
|
|
|
/* COMPLETE STATE (SOP done) */
|
|
.card.complete {
|
|
background: #ecfdf5;
|
|
border-color: #16a34a;
|
|
}
|
|
.card.complete .card-button { background: #16a34a; }
|
|
.card.complete .card-button:hover { background: #15803d; }
|
|
.card-status {
|
|
display: inline-block;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #16a34a;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.card.disabled {
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* SECTION */
|
|
.section {
|
|
background: var(--cds-layer);
|
|
border-radius: 4px;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
border: 1px solid var(--cds-border-subtle);
|
|
}
|
|
|
|
.section h2 {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1.5rem;
|
|
color: var(--cds-text-primary);
|
|
}
|
|
|
|
.section h3 {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.section p {
|
|
color: var(--cds-text-secondary);
|
|
margin-bottom: 1rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.quick-start {
|
|
background: var(--cds-button-primary);
|
|
color: white;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.quick-start h2 { color: white; }
|
|
.quick-start ol { margin-left: 1.5rem; line-height: 2; }
|
|
.quick-start li { margin-bottom: 0.5rem; }
|
|
|
|
/* FOOTER */
|
|
.footer {
|
|
background: var(--cds-ui-01);
|
|
color: var(--cds-text-secondary);
|
|
padding: 2rem;
|
|
text-align: center;
|
|
font-size: 12px;
|
|
border-top: 1px solid var(--cds-border-subtle);
|
|
}
|
|
|
|
.footer a {
|
|
color: var(--cds-link-primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.footer a:hover { text-decoration: underline; }
|
|
|
|
/* COMMENTS SECTION */
|
|
.comments-section {
|
|
background: var(--cds-layer);
|
|
border-radius: 4px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
border: 1px solid var(--cds-border-subtle);
|
|
}
|
|
|
|
.comments-toggle {
|
|
padding: 0.75rem 1.5rem;
|
|
background: var(--cds-button-primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 3px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.comments-toggle:hover { background: var(--cds-hover-primary); }
|
|
|
|
.comments-panel {
|
|
display: none;
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
background: var(--cds-ui-01);
|
|
border-radius: 3px;
|
|
border: 1px solid var(--cds-border-subtle);
|
|
}
|
|
|
|
.comments-panel.open { display: block; }
|
|
|
|
.comments-panel input,
|
|
.comments-panel textarea {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid var(--cds-border-subtle);
|
|
border-radius: 3px;
|
|
background: var(--cds-ui-02);
|
|
color: var(--cds-text-primary);
|
|
font-family: inherit;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.comments-panel textarea {
|
|
resize: vertical;
|
|
min-height: 80px;
|
|
}
|
|
|
|
.comment-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.comment-buttons button {
|
|
padding: 0.5rem 1rem;
|
|
border: none;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.submit-btn {
|
|
background: var(--cds-button-primary);
|
|
color: white;
|
|
}
|
|
|
|
.submit-btn:hover { background: var(--cds-hover-primary); }
|
|
|
|
.close-btn {
|
|
background: var(--cds-border-subtle);
|
|
color: var(--cds-text-primary);
|
|
}
|
|
|
|
.close-btn:hover { background: var(--cds-hover-ui); }
|
|
|
|
.comments-list {
|
|
margin-top: 1rem;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.comment-item {
|
|
padding: 0.75rem;
|
|
background: var(--cds-background);
|
|
border: 1px solid var(--cds-border-subtle);
|
|
border-radius: 3px;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.comment-meta {
|
|
font-size: 11px;
|
|
color: var(--cds-text-secondary);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.comment-text {
|
|
color: var(--cds-text-primary);
|
|
}
|
|
|
|
/* PROJECT PICKER */
|
|
.proj-loading { color: var(--cds-text-secondary); font-style: italic; font-size: 13px; }
|
|
.proj-row { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
|
|
.proj-row select { flex: 1; min-width: 240px; padding: 0.6rem 0.7rem; font-size: 14px;
|
|
border: 1px solid var(--cds-border-strong, #8d8d8d); border-radius: 4px; background: #fff; }
|
|
.proj-empty { background: var(--cds-ui-01, #fff); border: 1px dashed var(--cds-border-strong, #8d8d8d);
|
|
border-radius: 6px; padding: 1.25rem; }
|
|
.proj-empty p { margin: 0 0 0.9rem; color: var(--cds-text-secondary); }
|
|
.proj-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
|
.proj-form { margin-top: 1rem; padding: 1rem; border: 1px solid var(--cds-ui-03, #e0e0e0); border-radius: 6px; background: var(--cds-ui-01, #fff); }
|
|
.proj-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.75rem; margin-bottom: 0.9rem; }
|
|
.proj-form-grid label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 12px; font-weight: 600; color: var(--cds-text-secondary); }
|
|
.proj-form-grid input { padding: 0.55rem 0.65rem; font-size: 14px; border: 1px solid var(--cds-border-strong, #8d8d8d); border-radius: 4px; }
|
|
.proj-active { margin-top: 0.85rem; font-size: 13px; color: var(--cds-text-primary); }
|
|
.link-like { background: none; border: none; color: var(--cds-link-01, #0f62fe); cursor: pointer; font-size: 13px; padding: 0; text-decoration: underline; }
|
|
|
|
/* RESPONSIVE */
|
|
@media (max-width: 768px) {
|
|
.header-content { flex-direction: column; text-align: center; }
|
|
.header-spacer { display: none; }
|
|
.hero h1 { font-size: 1.75rem; }
|
|
.cards-grid { grid-template-columns: 1fr; }
|
|
.container { padding: 1.5rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- HEADER -->
|
|
<header class="header">
|
|
<div class="header-content">
|
|
<a href="index.html" class="logo">
|
|
<img src="prime-controls-logo.jpg" alt="Prime Controls">
|
|
<div>Work Package Suite</div>
|
|
</a>
|
|
<div class="header-spacer"></div>
|
|
<nav class="header-nav">
|
|
<a href="#overview">Overview</a>
|
|
<a href="#comments">Feedback</a>
|
|
<a href="#" onclick="openHelp();return false;">Help</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- MAIN CONTENT -->
|
|
<div class="container">
|
|
|
|
<!-- HERO -->
|
|
<div class="hero">
|
|
<h1 id="hero-title">Work Package Suite</h1>
|
|
<p id="hero-sub">Standardized Work Package creation for Prime Controls construction projects. Select a project to begin — or create one.</p>
|
|
</div>
|
|
|
|
<!-- PROJECT SELECTION -->
|
|
<div class="section" id="project-section">
|
|
<h2>Project</h2>
|
|
<p style="color:var(--cds-text-secondary);font-size:13px;margin:-.25rem 0 1rem">Projects are stored centrally. Pick the project you're working on, or set up a new one.</p>
|
|
<div id="project-picker"><div class="proj-loading">Loading projects…</div></div>
|
|
</div>
|
|
|
|
<!-- TOOL CARDS (shown once a project is active) -->
|
|
<div class="cards-grid" id="overview" style="display:none">
|
|
|
|
<!-- SOP CONFIG -->
|
|
<a href="work-package-suite.html?tab=sop" class="card" id="card-sop">
|
|
<h3>SOP Configuration</h3>
|
|
<p>Define the project baseline in 10 steps — team, sign-offs, WP types, governance, quality, platforms, sequence, constraints, and sources. Every Work Package inherits these defaults.</p>
|
|
<button class="card-button" id="card-sop-btn">Open Tool</button>
|
|
</a>
|
|
|
|
<!-- WP CREATOR -->
|
|
<a href="work-package-suite.html?tab=wp" class="card" id="card-wp">
|
|
<h3>Work Package Creator</h3>
|
|
<p>Author individual Work Packages against the project SOP — pre-populated defaults, constraint checklists, and exportable IWPs. Complete the SOP first to unlock.</p>
|
|
<button class="card-button" id="card-wp-btn">Open Tool</button>
|
|
</a>
|
|
|
|
<!-- WP DASHBOARD -->
|
|
<a href="work-package-suite.html?view=dashboard" class="card" id="card-dash">
|
|
<h3>Work Package Dashboard</h3>
|
|
<p>Track status and gating across every Work Package — release-readiness, on-hold packages, overdue work, hours, and breakdowns by status and discipline. Issue release-ready packages in one click.</p>
|
|
<button class="card-button" id="card-dash-btn">Open Dashboard</button>
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<!-- QUICK START -->
|
|
<div class="section quick-start">
|
|
<h2>Getting Started</h2>
|
|
<ol>
|
|
<li><strong>Open "SOP Configuration"</strong> and complete the 10 steps for your project (~15 minutes)</li>
|
|
<li><strong>Finish the SOP</strong> — this card turns green and unlocks the Work Package Creator</li>
|
|
<li><strong>Open "Work Package Creator"</strong> to author Work Packages with your SOP defaults pre-populated</li>
|
|
<li><strong>Leave feedback</strong> on any page using the feedback button below</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<!-- COMMENTS SECTION -->
|
|
<div class="comments-section" id="comments">
|
|
<button class="comments-toggle" onclick="toggleComments()">Leave Feedback</button>
|
|
<div class="comments-panel" id="comments-panel">
|
|
<div style="margin-bottom: 1rem;">
|
|
<label style="font-weight: 600; font-size: 13px; color: var(--cds-text-primary);">Name (optional)</label>
|
|
<input type="text" id="commenter-name" placeholder="Your name">
|
|
</div>
|
|
<div style="margin-bottom: 1rem;">
|
|
<label style="font-weight: 600; font-size: 13px; color: var(--cds-text-primary);">Feedback</label>
|
|
<textarea id="comment-text" placeholder="Your feedback here..."></textarea>
|
|
</div>
|
|
<div class="comment-buttons">
|
|
<button class="submit-btn" onclick="submitComment()">Submit</button>
|
|
<button class="close-btn" onclick="exportFeedback()">⤓ Export</button>
|
|
<button class="close-btn" onclick="document.getElementById('feedback-import').click()">⤒ Import</button>
|
|
<button class="close-btn" onclick="toggleComments()">Close</button>
|
|
<input type="file" id="feedback-import" accept="application/json" style="display:none" onchange="importFeedback(event)">
|
|
</div>
|
|
<div class="comments-list" id="comments-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- FOOTER -->
|
|
<footer class="footer">
|
|
<p>Work Package Suite v1.0 | Prime Controls - Business Technology Group | Pilot Use Only</p>
|
|
</footer>
|
|
|
|
<script src="feedback-config.js"></script>
|
|
<script src="project-data.js"></script>
|
|
<script src="help.js"></script>
|
|
<script>
|
|
// ── PROJECT SELECTION ─────────────────────────────────────────────────────
|
|
const esc = ProjectData.esc;
|
|
let _projects = [];
|
|
|
|
function initProjects(){
|
|
ProjectData.list().then(list => {
|
|
_projects = list || [];
|
|
// Reconcile the active project against the list; clear if it's gone.
|
|
const active = ProjectData.getActive();
|
|
if(active && !_projects.some(p => p.id === active.id)) ProjectData.setActive(null);
|
|
renderProjectPicker();
|
|
applyActiveProject();
|
|
});
|
|
}
|
|
|
|
function createFormHtml(){
|
|
return `<div class="proj-form" id="proj-form" style="display:none">
|
|
<div class="proj-form-grid">
|
|
<label>Project Name *<input type="text" id="np_name" placeholder="e.g. Micron — INC Construction"></label>
|
|
<label>Project Number<input type="text" id="np_number" placeholder="e.g. 26-67-008"></label>
|
|
<label>Client<input type="text" id="np_client" placeholder="e.g. Micron Technology, Inc."></label>
|
|
<label>Division<input type="text" id="np_division" placeholder="e.g. Semiconductor"></label>
|
|
<label>Site / Location<input type="text" id="np_site" placeholder="e.g. Boise, ID — Fab"></label>
|
|
</div>
|
|
<div class="proj-actions">
|
|
<button class="card-button" onclick="saveNewProject()">Create & Select</button>
|
|
<button class="close-btn" onclick="hideCreateProject()">Cancel</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
function renderProjectPicker(){
|
|
const box = document.getElementById('project-picker');
|
|
const activeId = ProjectData.getActiveId();
|
|
if(!_projects.length){
|
|
box.innerHTML = `<div class="proj-empty">
|
|
<p>No projects yet. Create your first project, or start from a sample.</p>
|
|
<div class="proj-actions">
|
|
<button class="card-button" onclick="showCreateProject()">+ Create Project</button>
|
|
<button class="close-btn" onclick="useSampleProject()">Use Sample Project</button>
|
|
</div>
|
|
</div>` + createFormHtml();
|
|
return;
|
|
}
|
|
const opts = _projects.map(p =>
|
|
`<option value="${esc(p.id)}" ${p.id===activeId?'selected':''}>${esc(p.name||'(unnamed)')}${p.number?' — '+esc(p.number):''}${p.sample?' [sample]':''}</option>`
|
|
).join('');
|
|
box.innerHTML = `<div class="proj-row">
|
|
<select id="project-select" onchange="selectProject(this.value)">
|
|
<option value="">Select a project…</option>${opts}
|
|
</select>
|
|
<button class="card-button" onclick="showCreateProject()">+ New</button>
|
|
<button class="close-btn" onclick="useSampleProject()">Sample</button>
|
|
</div>
|
|
<div id="active-project-info"></div>` + createFormHtml();
|
|
}
|
|
|
|
function showCreateProject(){ const f=document.getElementById('proj-form'); if(f){ f.style.display=''; const n=document.getElementById('np_name'); if(n) n.focus(); } }
|
|
function hideCreateProject(){ const f=document.getElementById('proj-form'); if(f) f.style.display='none'; }
|
|
|
|
function saveNewProject(){
|
|
const v = id => (document.getElementById(id)?.value || '').trim();
|
|
const name = v('np_name');
|
|
if(!name){ alert('Project name is required.'); return; }
|
|
const p = { name, number:v('np_number'), client:v('np_client'), division:v('np_division'), site:v('np_site'), sample:false };
|
|
ProjectData.save(p).then(saved => { afterProjectChosen(saved); });
|
|
}
|
|
|
|
function useSampleProject(){
|
|
const existing = _projects.find(p => p.sample);
|
|
if(existing){ afterProjectChosen(existing); return; }
|
|
ProjectData.save(Object.assign({}, ProjectData.SAMPLE)).then(saved => { afterProjectChosen(saved); });
|
|
}
|
|
|
|
function selectProject(id){
|
|
if(!id){ ProjectData.setActive(null); applyActiveProject(); return; }
|
|
const p = _projects.find(x => x.id === id);
|
|
if(p){ ProjectData.setActive(p); applyActiveProject(); }
|
|
}
|
|
|
|
function afterProjectChosen(p){
|
|
if(!_projects.some(x => x.id === p.id)) _projects.unshift(p);
|
|
ProjectData.setActive(p);
|
|
renderProjectPicker();
|
|
applyActiveProject();
|
|
document.getElementById('overview').scrollIntoView({ behavior:'smooth', block:'start' });
|
|
}
|
|
|
|
// Show/hide the tool cards and stamp the active project into their links.
|
|
function applyActiveProject(){
|
|
const active = ProjectData.getActive();
|
|
const cards = document.getElementById('overview');
|
|
const heroTitle = document.getElementById('hero-title');
|
|
const heroSub = document.getElementById('hero-sub');
|
|
const info = document.getElementById('active-project-info');
|
|
|
|
if(!active){
|
|
cards.style.display = 'none';
|
|
heroTitle.textContent = 'Work Package Suite';
|
|
heroSub.textContent = 'Select a project to begin — or create one.';
|
|
if(info) info.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const q = '&project=' + encodeURIComponent(active.id);
|
|
const setHref = (id, base) => { const el=document.getElementById(id); if(el) el.href = base + q; };
|
|
setHref('card-sop', 'work-package-suite.html?tab=sop');
|
|
setHref('card-wp', 'work-package-suite.html?tab=wp');
|
|
setHref('card-dash', 'work-package-suite.html?view=dashboard');
|
|
|
|
cards.style.display = '';
|
|
heroTitle.textContent = active.name || 'Work Package Suite';
|
|
heroSub.textContent = [active.number, active.client, active.site].filter(Boolean).join(' · ') || 'Active project';
|
|
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>`;
|
|
|
|
// Pull the project's shared SOP/WPs from the server into the local cache
|
|
// first, so the SOP "Complete / Review" status reflects what other users did.
|
|
if(ProjectData.pullProject){ ProjectData.pullProject(active.id).then(()=>reflectSOPStatus(active)).catch(()=>reflectSOPStatus(active)); }
|
|
else reflectSOPStatus(active);
|
|
}
|
|
|
|
function clearActiveProject(){ ProjectData.setActive(null); renderProjectPicker(); applyActiveProject(); }
|
|
|
|
// Reflect SOP completion on the tool cards (scoped to the active project).
|
|
function reflectSOPStatus(active){
|
|
let complete = false, projName = '';
|
|
try {
|
|
// Storage is namespaced per project, so these already scope to `active`.
|
|
complete = localStorage.getItem(ProjectData.key('wp_suite_sop_complete')) === '1';
|
|
const sop = JSON.parse(localStorage.getItem(ProjectData.key('wp_suite_sop')) || 'null');
|
|
projName = sop && sop.project && sop.project.name || '';
|
|
} catch(e){}
|
|
|
|
const sopCard = document.getElementById('card-sop');
|
|
const sopBtn = document.getElementById('card-sop-btn');
|
|
const wpCard = document.getElementById('card-wp');
|
|
const wpBtn = document.getElementById('card-wp-btn');
|
|
if(!sopCard) return;
|
|
|
|
// reset (re-render can run multiple times)
|
|
sopCard.classList.remove('complete');
|
|
wpCard && wpCard.classList.remove('disabled');
|
|
const oldStatus = sopCard.querySelector('.card-status'); if(oldStatus) oldStatus.remove();
|
|
|
|
if(complete){
|
|
sopCard.classList.add('complete');
|
|
sopBtn.textContent = 'Review';
|
|
const status = document.createElement('div');
|
|
status.className = 'card-status';
|
|
status.textContent = '✓ SOP Complete' + (projName ? ' — ' + projName : '');
|
|
sopCard.insertBefore(status, sopCard.firstChild);
|
|
if(wpBtn) wpBtn.textContent = 'Open Creator';
|
|
} else {
|
|
if(wpCard) wpCard.classList.add('disabled');
|
|
if(wpBtn) wpBtn.textContent = 'Complete SOP first';
|
|
}
|
|
}
|
|
|
|
initProjects();
|
|
|
|
let allComments = [];
|
|
|
|
function toggleComments() {
|
|
const panel = document.getElementById('comments-panel');
|
|
panel.classList.toggle('open');
|
|
if (panel.classList.contains('open')) loadComments();
|
|
}
|
|
|
|
function submitComment() {
|
|
const name = document.getElementById('commenter-name').value || 'Anonymous';
|
|
const text = document.getElementById('comment-text').value.trim();
|
|
|
|
if (!text) {
|
|
alert('Please enter feedback.');
|
|
return;
|
|
}
|
|
|
|
const comment = {
|
|
name,
|
|
text,
|
|
timestamp: new Date().toLocaleString()
|
|
};
|
|
|
|
allComments.push(comment);
|
|
localStorage.setItem('wp_suite_index_comments', JSON.stringify(allComments));
|
|
if (window.postFeedback) window.postFeedback({ type: 'home_feedback', ...comment });
|
|
|
|
document.getElementById('comment-text').value = '';
|
|
loadComments();
|
|
}
|
|
|
|
function exportFeedback() {
|
|
const saved = localStorage.getItem('wp_suite_index_comments');
|
|
const data = saved ? JSON.parse(saved) : [];
|
|
if (!data.length) { alert('No feedback to export yet.'); return; }
|
|
const payload = { app: 'Work Package Suite', source: 'home', exportedAt: new Date().toISOString(), comments: data };
|
|
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = 'wp-suite-feedback-home-' + new Date().toISOString().slice(0, 10) + '.json';
|
|
a.click();
|
|
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
|
}
|
|
|
|
function importFeedback(ev) {
|
|
const f = ev.target.files && ev.target.files[0];
|
|
if (!f) return;
|
|
const r = new FileReader();
|
|
r.onload = () => {
|
|
try {
|
|
const inc = JSON.parse(r.result);
|
|
const incoming = Array.isArray(inc) ? inc : (inc.comments || []);
|
|
if (!incoming.length) { alert('No feedback found in that file.'); return; }
|
|
const saved = localStorage.getItem('wp_suite_index_comments');
|
|
allComments = saved ? JSON.parse(saved) : [];
|
|
const seen = new Set(allComments.map(c => c.timestamp + '|' + c.text));
|
|
let added = 0;
|
|
incoming.forEach(c => { const k = c.timestamp + '|' + c.text; if (c.text && !seen.has(k)) { allComments.push(c); seen.add(k); added++; } });
|
|
localStorage.setItem('wp_suite_index_comments', JSON.stringify(allComments));
|
|
loadComments();
|
|
alert('Imported ' + added + ' feedback item' + (added === 1 ? '' : 's') + '.');
|
|
} catch (e) { alert('Could not read that file.'); }
|
|
ev.target.value = '';
|
|
};
|
|
r.readAsText(f);
|
|
}
|
|
|
|
function renderComments() {
|
|
const list = document.getElementById('comments-list');
|
|
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>';
|
|
} else {
|
|
list.innerHTML = allComments.map(c => `
|
|
<div class="comment-item">
|
|
<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>
|
|
`).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>
|
|
</body>
|
|
</html>
|