Merge branch 'main' into feat/wp-discipline-split-dashboard
This commit is contained in:
BIN
html/favicon.ico
Normal file
BIN
html/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
48
html/feedback-config.js
Normal file
48
html/feedback-config.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/* ──────────────────────────────────────────────────────────────────────────
|
||||
CENTRAL FEEDBACK CONFIGURATION
|
||||
----------------------------------------------------------------------------
|
||||
By default the suite stores all feedback in each user's own browser
|
||||
(localStorage) and reviewers share it via Export / Import. That needs no
|
||||
server and works behind any firewall.
|
||||
|
||||
To ALSO collect feedback automatically into one central place, set
|
||||
FEEDBACK_ENDPOINT below to a URL that accepts an HTTP POST of JSON. This
|
||||
works with either:
|
||||
• a small backend on your host (Node/PHP/Python/ASP.NET) that appends the
|
||||
body to a feedback.json / .csv you can download, or
|
||||
• a Microsoft Power Automate "When an HTTP request is received" trigger
|
||||
that writes to a SharePoint list / Excel table.
|
||||
|
||||
Leave it as an empty string to stay fully local (export/import only).
|
||||
See DEPLOYMENT.md for setup details and sample receivers.
|
||||
|
||||
This is set to the same-origin path '/api/feedback', which the NGINX reverse
|
||||
proxy (see nginx-wp-suite.conf) forwards to the Power Automate HTTP trigger.
|
||||
Keeping it relative means no CORS and the secret trigger URL never appears in
|
||||
client code. Locally (no proxy) the POST simply fails silently and feedback is
|
||||
still saved/exported from the browser.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
window.FEEDBACK_ENDPOINT = '/api/feedback';
|
||||
|
||||
/* Best-effort send to the central endpoint. Never throws and never blocks the
|
||||
UI: feedback is always saved locally first by the caller, so a failed or
|
||||
disabled POST simply means "not centrally collected." Returns a Promise that
|
||||
resolves true on success, false otherwise. */
|
||||
window.postFeedback = function (payload) {
|
||||
if (!window.FEEDBACK_ENDPOINT) return Promise.resolve(false);
|
||||
try {
|
||||
return fetch(window.FEEDBACK_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
app: 'Work Package Suite',
|
||||
page: (typeof location !== 'undefined' && location.pathname) || '',
|
||||
submittedAt: new Date().toISOString(),
|
||||
...payload
|
||||
}),
|
||||
keepalive: true // allow the send to complete even if the page unloads
|
||||
}).then(function (r) { return r.ok; }).catch(function () { return false; });
|
||||
} catch (e) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
};
|
||||
589
html/index.html
Normal file
589
html/index.html
Normal file
@@ -0,0 +1,589 @@
|
||||
<!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>
|
||||
<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);
|
||||
}
|
||||
|
||||
/* 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>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<div class="container">
|
||||
|
||||
<!-- HERO -->
|
||||
<div class="hero">
|
||||
<h1>Work Package Suite</h1>
|
||||
<p>Standardized approach to Work Package creation for Prime Controls construction projects. Configure project parameters, define constraints, and generate compliant work packages.</p>
|
||||
</div>
|
||||
|
||||
<!-- TOOL CARDS -->
|
||||
<div class="cards-grid" id="overview">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- SUPPORT SECTION -->
|
||||
<div class="section">
|
||||
<h2>About This Suite</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;">
|
||||
<div>
|
||||
<h3>Two-Step Workflow</h3>
|
||||
<p>Configure the project SOP once, then author every Work Package against it. The Creator stays locked until the SOP is complete, so packages always inherit a valid baseline.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Leave Feedback</h3>
|
||||
<p>Use the feedback section on this page or within any tool. All comments are stored locally and can be exported for team review and iteration.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Offline & Collaborative</h3>
|
||||
<p>All tools work entirely in your browser. Export SOP and Work Package data as JSON for sharing, version control, and integration.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="footer">
|
||||
<p>Work Package Suite v1.0 | Prime Controls | All files work offline with local browser storage</p>
|
||||
</footer>
|
||||
|
||||
<script src="feedback-config.js"></script>
|
||||
<script>
|
||||
// Reflect SOP completion on the tool cards.
|
||||
(function reflectSOPStatus(){
|
||||
let complete = false, projName = '';
|
||||
try {
|
||||
complete = localStorage.getItem('wp_suite_sop_complete') === '1';
|
||||
const sop = JSON.parse(localStorage.getItem('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(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';
|
||||
}
|
||||
})();
|
||||
|
||||
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 = '';
|
||||
document.getElementById('commenter-name').value = '';
|
||||
loadComments();
|
||||
alert('Thank you! Feedback submitted.');
|
||||
}
|
||||
|
||||
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 loadComments() {
|
||||
const saved = localStorage.getItem('wp_suite_index_comments');
|
||||
if (saved) allComments = JSON.parse(saved);
|
||||
|
||||
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}</strong> • ${c.timestamp}</div>
|
||||
<div class="comment-text">${c.text.replace(/</g,'<').replace(/>/g,'>')}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
html/prime-controls-logo.jpg
Normal file
BIN
html/prime-controls-logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
159
html/theme-light.css
Normal file
159
html/theme-light.css
Normal file
@@ -0,0 +1,159 @@
|
||||
/* IBM Carbon Design System - Light Theme (g10) */
|
||||
:root {
|
||||
--cds-interactive-01: #0f62fe;
|
||||
--cds-interactive-02: #393939;
|
||||
--cds-interactive-03: #0f62fe;
|
||||
--cds-interactive-04: #0f62fe;
|
||||
--cds-ui-background: #f4f4f4;
|
||||
--cds-ui-01: #ffffff;
|
||||
--cds-ui-02: #f4f4f4;
|
||||
--cds-ui-03: #e0e0e0;
|
||||
--cds-ui-04: #8d8d8d;
|
||||
--cds-ui-05: #161616;
|
||||
--cds-text-01: #161616;
|
||||
--cds-text-02: #525252;
|
||||
--cds-text-03: #a8a8a8;
|
||||
--cds-text-04: #ffffff;
|
||||
--cds-text-05: #6f6f6f;
|
||||
--cds-text-error: #da1e28;
|
||||
--cds-icon-01: #161616;
|
||||
--cds-icon-02: #525252;
|
||||
--cds-icon-03: #ffffff;
|
||||
--cds-link-01: #0f62fe;
|
||||
--cds-link-02: #0043ce;
|
||||
--cds-inverse-link: #78a9ff;
|
||||
--cds-field-01: #ffffff;
|
||||
--cds-field-02: #f4f4f4;
|
||||
--cds-inverse-01: #ffffff;
|
||||
--cds-inverse-02: #393939;
|
||||
--cds-support-01: #da1e28;
|
||||
--cds-support-02: #198038;
|
||||
--cds-support-03: #f1c21b;
|
||||
--cds-support-04: #0043ce;
|
||||
--cds-inverse-support-01: #fa4d56;
|
||||
--cds-inverse-support-02: #42be65;
|
||||
--cds-inverse-support-03: #f1c21b;
|
||||
--cds-inverse-support-04: #4589ff;
|
||||
--cds-overlay: rgba(22, 22, 22, .5);
|
||||
--cds-danger: #da1e28;
|
||||
--cds-focus: #0f62fe;
|
||||
--cds-hover-primary: #0353e9;
|
||||
--cds-active-primary: #002d9c;
|
||||
--cds-hover-primary-text: #0043ce;
|
||||
--cds-hover-secondary: #4c4c4c;
|
||||
--cds-active-secondary: #6f6f6f;
|
||||
--cds-hover-tertiary: #0353e9;
|
||||
--cds-active-tertiary: #002d9c;
|
||||
--cds-hover-ui: #e5e5e5;
|
||||
--cds-hover-light-ui: #e5e5e5;
|
||||
--cds-hover-selected-ui: #cacaca;
|
||||
--cds-active-ui: #c6c6c6;
|
||||
--cds-active-light-ui: #cacaca;
|
||||
--cds-selected-ui: #e5e5e5;
|
||||
--cds-selected-light-ui: #e5e5e5;
|
||||
--cds-inverse-hover-ui: #353535;
|
||||
--cds-hover-danger: #ba121b;
|
||||
--cds-active-danger: #750e13;
|
||||
--cds-hover-row: #f4f4f4;
|
||||
--cds-visited-link: #0043ce;
|
||||
--cds-disabled-01: #f4f4f4;
|
||||
--cds-disabled-02: #bdbdbd;
|
||||
--cds-disabled-03: #8d8d8d;
|
||||
--cds-button-primary: #0f62fe;
|
||||
--cds-button-secondary: #393939;
|
||||
--cds-button-tertiary: #0f62fe;
|
||||
--cds-button-danger-primary: #da1e28;
|
||||
--cds-button-danger-secondary: #da1e28;
|
||||
--cds-background: #f4f4f4;
|
||||
--cds-background-inverse: #161616;
|
||||
--cds-background-brand: #0f62fe;
|
||||
--cds-background-active: #e5e5e5;
|
||||
--cds-background-hover: #e8e8e8;
|
||||
--cds-background-selected: #e5e5e5;
|
||||
--cds-background-selected-hover: #cacaca;
|
||||
--cds-layer: #ffffff;
|
||||
--cds-layer-accent: #f4f4f4;
|
||||
--cds-layer-accent-hover: #e8e8e8;
|
||||
--cds-layer-accent-active: #e0e0e0;
|
||||
--cds-layer-hover: #e8e8e8;
|
||||
--cds-layer-active: #e5e5e5;
|
||||
--cds-layer-selected: #e5e5e5;
|
||||
--cds-layer-selected-hover: #cacaca;
|
||||
--cds-layer-selected-inverse: #393939;
|
||||
--cds-field: #ffffff;
|
||||
--cds-field-hover: #e8e8e8;
|
||||
--cds-border-subtle: #e0e0e0;
|
||||
--cds-border-strong: #8d8d8d;
|
||||
--cds-border-inverse: #393939;
|
||||
--cds-border-interactive: #0f62fe;
|
||||
--cds-border-subtle-selected: #c6c6c6;
|
||||
--cds-text-primary: #161616;
|
||||
--cds-text-secondary: #525252;
|
||||
--cds-text-placeholder: #a8a8a8;
|
||||
--cds-text-helper: #6f6f6f;
|
||||
--cds-text-on-color: #ffffff;
|
||||
--cds-text-inverse: #f4f4f4;
|
||||
--cds-text-disabled: #bdbdbd;
|
||||
--cds-link-primary: #0f62fe;
|
||||
--cds-link-secondary: #0043ce;
|
||||
--cds-link-visited: #0043ce;
|
||||
--cds-link-inverse: #78a9ff;
|
||||
--cds-link-primary-hover: #0353e9;
|
||||
--cds-icon-primary: #161616;
|
||||
--cds-icon-secondary: #525252;
|
||||
--cds-icon-on-color: #ffffff;
|
||||
--cds-icon-inverse: #f4f4f4;
|
||||
--cds-icon-disabled: #bdbdbd;
|
||||
--cds-support-error: #da1e28;
|
||||
--cds-support-success: #198038;
|
||||
--cds-support-warning: #f1c21b;
|
||||
--cds-support-info: #0043ce;
|
||||
--cds-support-error-inverse: #fa4d56;
|
||||
--cds-support-success-inverse: #42be65;
|
||||
--cds-support-warning-inverse: #f1c21b;
|
||||
--cds-support-info-inverse: #4589ff;
|
||||
--cds-interactive: #0f62fe;
|
||||
--cds-shadow: rgba(0, 0, 0, .16);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
body {
|
||||
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
||||
color: var(--cds-text-primary);
|
||||
background: var(--cds-background);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.28572;
|
||||
}
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.75rem; }
|
||||
h3 { font-size: 1.25rem; }
|
||||
h4 { font-size: 1rem; }
|
||||
h5 { font-size: .875rem; }
|
||||
h6 { font-size: .75rem; }
|
||||
|
||||
a {
|
||||
color: var(--cds-link-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--cds-link-primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
color: var(--cds-text-primary);
|
||||
}
|
||||
840
html/work-package-suite-app.js
Normal file
840
html/work-package-suite-app.js
Normal file
@@ -0,0 +1,840 @@
|
||||
// ── GLOBAL STATE ──────────────────────────────────────────────────────────────
|
||||
let currentTool = 'sop';
|
||||
let currentStep = 1;
|
||||
let sopComplete = false;
|
||||
let allComments = [];
|
||||
|
||||
let state = {
|
||||
project: {name:'', number:'', client:'', division:'', site:''},
|
||||
team: {pm:'', apm:'', cm:'', qm:''},
|
||||
teamMembers: [],
|
||||
signoffRoles: [{role:'Superintendent',name:''},{role:'Foreman',name:''}],
|
||||
wpTypes: [],
|
||||
governance: {woformat:'', wosize:'', issuance:[], disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:''},
|
||||
quality: {qcreq:'', photo:'', hold:''},
|
||||
platforms: {tracking:'CxAlloy', commissioning:'CxAlloy'},
|
||||
sequence: [],
|
||||
constraints: [],
|
||||
sources: []
|
||||
};
|
||||
|
||||
let sop = null; // Generated SOP for WP tool
|
||||
|
||||
// ── LIBRARIES ─────────────────────────────────────────────────────────────────
|
||||
const DEFAULT_WP_TYPES = [
|
||||
{name:'Rough-In', enabled:true},
|
||||
{name:'Mechanical Install', enabled:true},
|
||||
{name:'Panel Install', enabled:true},
|
||||
{name:'Conduit Install', enabled:true},
|
||||
{name:'Tray Install', enabled:true},
|
||||
{name:'Mechanical Tubing', enabled:false},
|
||||
{name:'Instrument Install', enabled:true},
|
||||
{name:'Wire Pull', enabled:true},
|
||||
{name:'Terminations', enabled:true},
|
||||
{name:'Prefab/Kitting', enabled:false},
|
||||
{name:'Network Cabling', enabled:false},
|
||||
{name:'Fiber', enabled:false},
|
||||
{name:'Calibrations', enabled:false}
|
||||
];
|
||||
|
||||
const STANDARD_10_CONSTRAINTS = [
|
||||
{name:'Safety & Permitting', description:'Permits, safety reviews, environmental clearances'},
|
||||
{name:'Quality Control / Inspection', description:'QC approval, inspection readiness'},
|
||||
{name:'IFC Drawings & Specs', description:'Issued-for-Construction drawings and specifications'},
|
||||
{name:'Schedule', description:'Scheduled sequence timing confirmed'},
|
||||
{name:'Materials (on site, bagged & tagged)', description:'All required materials on site and staged'},
|
||||
{name:'Prefabrication', description:'Prefabricated assemblies complete'},
|
||||
{name:'Work Access & Laydown', description:'Work area accessible and staged'},
|
||||
{name:'Craft Availability', description:'Required craft trades available'},
|
||||
{name:'Construction Equipment & Tools', description:'Special equipment and tools on site'},
|
||||
{name:'Scaffolding / Access Equipment', description:'Temporary access structures in place'}
|
||||
];
|
||||
|
||||
const CONSTRAINT_LIBRARY = [
|
||||
'Utility Clearances',
|
||||
'Third-Party Approvals',
|
||||
'Engineering Change Notices',
|
||||
'Commissioning Sign-Off',
|
||||
'As-Built Documentation',
|
||||
'Labeling & Identification',
|
||||
'Testing & Certification',
|
||||
'Rework Completion',
|
||||
'Coordination with Other Trades',
|
||||
'Environmental Controls',
|
||||
'Critical Path Gate',
|
||||
'Client Walkthrough Approval'
|
||||
];
|
||||
|
||||
const OPTIONAL_ROLES = [
|
||||
'Assistant Project Manager',
|
||||
'General Foreman',
|
||||
'Construction Manager',
|
||||
'HSE Professional',
|
||||
'Quality Representative',
|
||||
'Planner',
|
||||
'Safety Manager',
|
||||
'Project Controls Manager'
|
||||
];
|
||||
|
||||
const LABOR_COST_CODES = [
|
||||
'1000|Project Management',
|
||||
'2000|Design and Development',
|
||||
'2100|Design',
|
||||
'2110|Control System Design',
|
||||
'2120|Instrument Design',
|
||||
'2130|Electrical Design',
|
||||
'2140|Panel Design',
|
||||
'2141|Panel Design Rework',
|
||||
'2150|BIM',
|
||||
'2151|BIM Rework',
|
||||
'2160|Documentation',
|
||||
'2200|Development',
|
||||
'2210|PLC Programming',
|
||||
'2220|OIT Programming',
|
||||
'2230|SCADA Programming',
|
||||
'2240|Simulation Development',
|
||||
'2300|Customer Training',
|
||||
'3000|Operational Technology',
|
||||
'3100|OT Design',
|
||||
'3200|Rack Assembly',
|
||||
'3300|Network Configuration',
|
||||
'3400|Computer Configuration',
|
||||
'4000|Construction',
|
||||
'4010|Instruments Install',
|
||||
'4020|Network & Computers Install',
|
||||
'4040|PLC Install',
|
||||
'4050|Panel Install',
|
||||
'4060|Electrical Install',
|
||||
'4070|Mechanical Install',
|
||||
'4080|Security Install',
|
||||
'4090|Radio Install',
|
||||
'4100|Commissioning',
|
||||
'6000|Production',
|
||||
'7000|Quality',
|
||||
'7100|Panel Quality Control',
|
||||
'7200|Factory Acceptance Testing',
|
||||
'7300|Site Acceptance Testing',
|
||||
'8000|Safety',
|
||||
'9000|Administration'
|
||||
];
|
||||
|
||||
// ── INITIALIZATION ────────────────────────────────────────────────────────────
|
||||
window.addEventListener('DOMContentLoaded',()=>{
|
||||
initializeWPTypes();
|
||||
renderTeamMembers();
|
||||
renderOptionalRoles();
|
||||
renderStandardConstraints();
|
||||
renderSequenceSteps();
|
||||
renderSources();
|
||||
restoreSavedSOP();
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
|
||||
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard from the home page cards.
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const tab = params.get('tab');
|
||||
if(params.get('view') === 'dashboard') switchTool('wp');
|
||||
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
||||
|
||||
track('app_open');
|
||||
let _fieldTimer;
|
||||
document.addEventListener('input', e=>{
|
||||
const t = e.target;
|
||||
if(t && t.id && /^(INPUT|SELECT|TEXTAREA)$/.test(t.tagName)){
|
||||
clearTimeout(_fieldTimer);
|
||||
_fieldTimer = setTimeout(()=> track('field_edit', {field:t.id}), 600);
|
||||
}
|
||||
});
|
||||
});
|
||||
window.addEventListener('beforeunload', trackStepDwell);
|
||||
|
||||
function initializeWPTypes(){
|
||||
state.wpTypes = JSON.parse(JSON.stringify(DEFAULT_WP_TYPES)).map(t=>({...t,notes:'',approval:''}));
|
||||
renderWPTypes();
|
||||
}
|
||||
|
||||
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
|
||||
function loadSampleData(){
|
||||
// Populate Step 1
|
||||
document.getElementById('proj_name').value = 'MICRON_PH1_CUP_HPM_FMCS INSTALL';
|
||||
document.getElementById('proj_number').value = '26-67-008';
|
||||
document.getElementById('proj_client').value = 'Micron Technology, Inc.';
|
||||
document.getElementById('proj_division').value = 'Semiconductor';
|
||||
document.getElementById('proj_site').value = 'Boise, ID — Fab 7';
|
||||
|
||||
// Populate Step 2
|
||||
document.getElementById('proj_pm').value = 'Mariano Sanchez';
|
||||
document.getElementById('proj_apm').value = 'Assistant PM';
|
||||
document.getElementById('proj_cm').value = 'K. Boyd';
|
||||
document.getElementById('proj_qm').value = 'D. Nguyen';
|
||||
|
||||
// Step 3 already has defaults
|
||||
document.getElementById('role_super_name').value = 'John Smith';
|
||||
document.getElementById('role_foreman_name').value = 'Mike Jones';
|
||||
|
||||
// Populate Step 5
|
||||
document.getElementById('gov_woformat').value = 'WP##-[Sector]-[TYPE]';
|
||||
document.getElementById('gov_wosize').value = '3–5 days / 40–80 hours';
|
||||
document.getElementById('gov_disciplines').value = 'Mechanical, Electrical, Tech';
|
||||
document.getElementById('gov_discmode').value = 'choice';
|
||||
document.getElementById('gov_size_hours_max').value = '120';
|
||||
|
||||
// Populate Step 6
|
||||
document.getElementById('qual_qcreq').value = 'Yes — Detailed inspection items';
|
||||
document.getElementById('qual_photo').value = 'Key checkpoints only';
|
||||
document.getElementById('qual_hold').value = 'HOLD: Prime QAQC to inspect rough-in before cover/cover-up.\nWITNESS: Client QC to observe megger test before energization.';
|
||||
|
||||
// Step 7 already has defaults
|
||||
|
||||
// Collect all data
|
||||
collectStepData();
|
||||
track('sample_loaded');
|
||||
|
||||
alert('✓ Sample data loaded!\n\nNavigate through the SOP steps to see example values. You can edit or replace any field.');
|
||||
|
||||
// Switch to step 1
|
||||
currentStep = 1;
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
}
|
||||
|
||||
// ── RESTORE A COMPLETED SOP (for "Review" + WP tab across reloads) ─────────────
|
||||
function restoreSavedSOP(){
|
||||
let savedState = null, savedSop = null, complete = false;
|
||||
try {
|
||||
complete = localStorage.getItem('wp_suite_sop_complete') === '1';
|
||||
savedState = JSON.parse(localStorage.getItem('wp_suite_state') || 'null');
|
||||
savedSop = JSON.parse(localStorage.getItem('wp_suite_sop') || 'null');
|
||||
} catch(e){}
|
||||
if(!complete || !savedState) return;
|
||||
|
||||
state = savedState;
|
||||
sop = savedSop;
|
||||
sopComplete = true;
|
||||
|
||||
// Re-render dynamic lists from restored state.
|
||||
renderWPTypes();
|
||||
renderTeamMembers();
|
||||
renderOptionalRoles();
|
||||
renderStandardConstraints();
|
||||
renderSequenceSteps();
|
||||
renderSources();
|
||||
repopulateForm();
|
||||
|
||||
if(typeof onSOPReady === 'function') onSOPReady(sop);
|
||||
}
|
||||
|
||||
// Push restored state values back into the static form inputs.
|
||||
function repopulateForm(){
|
||||
const set = (id,val)=>{ const el=document.getElementById(id); if(el!=null && val!=null) el.value = val; };
|
||||
set('proj_name', state.project.name);
|
||||
set('proj_number', state.project.number);
|
||||
set('proj_client', state.project.client);
|
||||
set('proj_division', state.project.division);
|
||||
set('proj_site', state.project.site);
|
||||
set('proj_pm', state.team.pm);
|
||||
set('proj_apm', state.team.apm);
|
||||
set('proj_cm', state.team.cm);
|
||||
set('proj_qm', state.team.qm);
|
||||
if(state.signoffRoles[0]) set('role_super_name', state.signoffRoles[0].name);
|
||||
if(state.signoffRoles[1]) set('role_foreman_name', state.signoffRoles[1].name);
|
||||
set('gov_woformat', state.governance.woformat);
|
||||
set('gov_wosize', state.governance.wosize);
|
||||
set('gov_disciplines', (state.governance.disciplines||[]).join(', '));
|
||||
set('gov_discmode', state.governance.discMode);
|
||||
set('gov_size_hours_max', state.governance.sizeHoursMax);
|
||||
set('qual_qcreq', state.quality.qcreq);
|
||||
set('qual_photo', state.quality.photo);
|
||||
set('qual_hold', state.quality.hold);
|
||||
set('plat_tracking', state.platforms.tracking);
|
||||
set('plat_commissioning', state.platforms.commissioning);
|
||||
}
|
||||
|
||||
// ── TOOL SWITCHING ────────────────────────────────────────────────────────────
|
||||
function switchTool(tool){
|
||||
currentTool = tool;
|
||||
|
||||
// Update nav tabs
|
||||
document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active'));
|
||||
document.querySelector(`[data-tab="${tool}"]`).classList.add('active');
|
||||
|
||||
// Update content
|
||||
document.querySelectorAll('.tool').forEach(t=>t.classList.remove('active'));
|
||||
document.getElementById(`tool-${tool}`).classList.add('active');
|
||||
|
||||
// Reset step counter
|
||||
if(tool === 'sop'){
|
||||
document.getElementById('total-steps').textContent = '10';
|
||||
}else{
|
||||
document.getElementById('total-steps').textContent = '—';
|
||||
}
|
||||
|
||||
if(tool === 'wp') renderWPTab();
|
||||
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
}
|
||||
|
||||
// Show the gate or the embedded Work Package Creator depending on SOP status.
|
||||
function renderWPTab(){
|
||||
const gate = document.getElementById('wp-gate');
|
||||
const frame = document.getElementById('wp-frame');
|
||||
if(!gate || !frame) return;
|
||||
if(sopComplete){
|
||||
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();
|
||||
}else{
|
||||
gate.style.display = 'block';
|
||||
frame.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Called from completeSOP / restoreSavedSOP once an SOP is available.
|
||||
function onSOPReady(){
|
||||
if(currentTool === 'wp') renderWPTab();
|
||||
}
|
||||
|
||||
function updateProjectDisplay(){
|
||||
const projName = document.getElementById('proj_name')?.value || 'Project';
|
||||
const display = document.getElementById('project-display');
|
||||
if(display) display.textContent = sopComplete ? `✓ ${projName} (SOP Ready)` : projName;
|
||||
}
|
||||
|
||||
// ── RENDERING (SOP) ────────────────────────────────────────────────────────────
|
||||
function renderWPTypes(){
|
||||
const container = document.getElementById('wp-types-table');
|
||||
container.innerHTML = `<div class="wp-types-header">
|
||||
<div>Work Order Type</div>
|
||||
<div style="text-align:center;">Enabled</div>
|
||||
<div>Special Rules / Notes</div>
|
||||
<div>WO Complete Approval</div>
|
||||
</div>`;
|
||||
state.wpTypes.forEach((t,i)=>{
|
||||
const row = document.createElement('div');
|
||||
row.className = 'wp-type-row';
|
||||
row.innerHTML = `
|
||||
<div style="font-weight:600;">${t.name}</div>
|
||||
<div style="text-align:center;"><input type="checkbox" ${t.enabled?'checked':''} onchange="toggleWPType(${i})" style="width:18px; height:18px; cursor:pointer;"></div>
|
||||
<input type="text" placeholder="Special rules…" value="${(t.notes||'').replace(/"/g,'"')}" onchange="state.wpTypes[${i}].notes=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" placeholder="PM / CM / QC…" value="${(t.approval||'').replace(/"/g,'"')}" onchange="state.wpTypes[${i}].approval=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWPType(i){
|
||||
state.wpTypes[i].enabled = !state.wpTypes[i].enabled;
|
||||
renderWPTypes();
|
||||
}
|
||||
|
||||
function renderTeamMembers(){
|
||||
const container = document.getElementById('team-members-list');
|
||||
if(!container) return;
|
||||
container.innerHTML = state.teamMembers.map((m,i)=>`
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr 30px; gap:1rem; align-items:center; padding:0.75rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
|
||||
<input type="text" placeholder="Role / title (e.g., Scheduler)" value="${(m.role||'').replace(/"/g,'"')}" onchange="state.teamMembers[${i}].role=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" placeholder="Name" value="${(m.name||'').replace(/"/g,'"')}" onchange="state.teamMembers[${i}].name=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="removeTeamMember(${i})">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function addTeamMember(){
|
||||
state.teamMembers.push({role:'',name:''});
|
||||
renderTeamMembers();
|
||||
}
|
||||
|
||||
function removeTeamMember(i){
|
||||
state.teamMembers.splice(i,1);
|
||||
renderTeamMembers();
|
||||
}
|
||||
|
||||
function renderOptionalRoles(){
|
||||
const container = document.getElementById('optional-roles-list');
|
||||
const current = state.signoffRoles.filter(r=>r.role!=='Superintendent'&&r.role!=='Foreman');
|
||||
container.innerHTML = current.map((r,i)=>`
|
||||
<div style="display:grid; grid-template-columns:1fr 200px 30px; gap:1rem; align-items:center; padding:0.75rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
|
||||
<select onchange="state.signoffRoles[${state.signoffRoles.indexOf(r)}].role=this.value">
|
||||
${OPTIONAL_ROLES.map(o=>`<option ${r.role===o?'selected':''}>${o}</option>`).join('')}
|
||||
</select>
|
||||
<input type="text" placeholder="Name (optional)" value="${r.name||''}" onchange="state.signoffRoles[${state.signoffRoles.indexOf(r)}].name=this.value">
|
||||
<button class="row-del" style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer;" onclick="removeRole(${state.signoffRoles.indexOf(r)})">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function addOptionalRole(){
|
||||
state.signoffRoles.push({role:'General Foreman',name:''});
|
||||
renderOptionalRoles();
|
||||
}
|
||||
|
||||
function removeRole(i){
|
||||
state.signoffRoles.splice(i,1);
|
||||
renderOptionalRoles();
|
||||
}
|
||||
|
||||
function renderStandardConstraints(){
|
||||
const container = document.getElementById('standard-constraints');
|
||||
container.innerHTML = STANDARD_10_CONSTRAINTS.map(c=>`
|
||||
<div style="display:flex; align-items:start; gap:0.75rem; padding:0.75rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; margin-bottom:0.5rem;">
|
||||
<input type="checkbox" id="const_${c.name}" checked onchange="toggleConstraint('${c.name}')" style="width:18px; height:18px; cursor:pointer; margin-top:0.2rem;">
|
||||
<div style="flex:1;">
|
||||
<label for="const_${c.name}" style="margin:0; font-weight:600; display:block; cursor:pointer;">${c.name}</label>
|
||||
<div style="font-size:12px; color:var(--text-dim); margin-top:0.25rem;">${c.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
state.constraints = STANDARD_10_CONSTRAINTS.map(c=>({...c}));
|
||||
}
|
||||
|
||||
function toggleConstraint(name){
|
||||
const idx = state.constraints.findIndex(c=>c.name===name);
|
||||
if(idx>=0) state.constraints.splice(idx,1);
|
||||
else state.constraints.push(STANDARD_10_CONSTRAINTS.find(c=>c.name===name));
|
||||
}
|
||||
|
||||
function showConstraintLibrary(){
|
||||
const modal = document.getElementById('constraint-modal');
|
||||
const lib = document.getElementById('constraint-library');
|
||||
lib.innerHTML = CONSTRAINT_LIBRARY.map(c=>`
|
||||
<div class="constraint-option" style="padding:0.75rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; margin-bottom:0.5rem; cursor:pointer; transition:all 0.2s;" onmouseover="this.style.borderColor='var(--primary)'; this.style.background='var(--primary-light)'" onmouseout="this.style.borderColor='var(--border)'; this.style.background='var(--bg)'" onclick="addCustomConstraint('${c}')">
|
||||
<strong>${c}</strong>
|
||||
<div style="font-size:12px; color:var(--text-light); margin-top:0.25rem;">Click to add to this project</div>
|
||||
</div>
|
||||
`).join('');
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeConstraintModal(){
|
||||
document.getElementById('constraint-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
function addCustomConstraint(name){
|
||||
if(!state.constraints.find(c=>c.name===name)){
|
||||
state.constraints.push({name,description:''});
|
||||
}
|
||||
closeConstraintModal();
|
||||
renderStandardConstraints();
|
||||
}
|
||||
|
||||
const DEFAULT_SEQUENCE = ['Layout','Conduit Install','Tray Install','Wire Pull','Device Install','Termination','QC Inspection','Commissioning'];
|
||||
|
||||
let seqDragIndex = null;
|
||||
function renderSequenceSteps(){
|
||||
const container = document.getElementById('sequence-list');
|
||||
if(!container) return;
|
||||
if(!state.sequence.length) state.sequence = DEFAULT_SEQUENCE.map(s=>({label:s,kind:'step'}));
|
||||
container.innerHTML = '';
|
||||
let stepNo = 0;
|
||||
state.sequence.forEach((item,i)=>{
|
||||
const isGate = item.kind==='gate';
|
||||
if(!isGate) stepNo++;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'seq-step' + (isGate?' gate':'');
|
||||
row.draggable = true;
|
||||
row.dataset.idx = i;
|
||||
const badge = isGate ? `<span class="seq-gate-badge" title="QC / hold gate">◆ HOLD</span>`
|
||||
: `<span class="seq-num">${stepNo}</span>`;
|
||||
row.innerHTML = `
|
||||
<span class="seq-handle" title="Drag to reorder">⠿</span>
|
||||
${badge}
|
||||
<input type="text" class="seq-label" value="${(item.label||'').replace(/"/g,'"')}" oninput="state.sequence[${i}].label=this.value">
|
||||
<button class="seq-del" title="Remove" onclick="removeSeqStep(${i})">✕</button>`;
|
||||
row.addEventListener('dragstart',e=>{ seqDragIndex=i; row.classList.add('dragging'); e.dataTransfer.effectAllowed='move'; });
|
||||
row.addEventListener('dragend',()=>{ row.classList.remove('dragging'); document.querySelectorAll('.seq-step').forEach(s=>s.classList.remove('drag-over')); });
|
||||
row.addEventListener('dragover',e=>{ e.preventDefault(); row.classList.add('drag-over'); e.dataTransfer.dropEffect='move'; });
|
||||
row.addEventListener('dragleave',()=>row.classList.remove('drag-over'));
|
||||
row.addEventListener('drop',e=>{
|
||||
e.preventDefault(); row.classList.remove('drag-over');
|
||||
const from=seqDragIndex, to=i;
|
||||
if(from===null||from===to) return;
|
||||
const moved=state.sequence.splice(from,1)[0];
|
||||
state.sequence.splice(to,0,moved);
|
||||
seqDragIndex=null; renderSequenceSteps();
|
||||
});
|
||||
container.appendChild(row);
|
||||
if(i<state.sequence.length-1){ const a=document.createElement('div'); a.className='seq-arrow'; a.textContent='↓'; container.appendChild(a); }
|
||||
});
|
||||
}
|
||||
|
||||
function addSequenceStep(){
|
||||
const inp = document.getElementById('seq-add-input');
|
||||
const v = (inp && inp.value || '').trim();
|
||||
state.sequence.push({label: v || 'New Step', kind:'step'});
|
||||
if(inp) inp.value = '';
|
||||
renderSequenceSteps();
|
||||
}
|
||||
|
||||
function addSequenceGate(){
|
||||
state.sequence.push({label:'QC Hold', kind:'gate'});
|
||||
renderSequenceSteps();
|
||||
}
|
||||
|
||||
function removeSeqStep(i){
|
||||
state.sequence.splice(i,1);
|
||||
renderSequenceSteps();
|
||||
}
|
||||
|
||||
const DEFAULT_SOURCES = [
|
||||
{label:'Design Drawings', ph:'e.g. Procore, Bluebeam'},
|
||||
{label:'Spool Drawings', ph:'e.g. SharePoint, BIM'},
|
||||
{label:'Installation Detail Drawings', ph:'e.g. Prime details + Micron MVDs'},
|
||||
{label:'IO List', ph:'e.g. controls.dev, Excel'},
|
||||
{label:'Cable Schedule', ph:'e.g. Excel on SharePoint'},
|
||||
{label:'Conduit Schedule', ph:'e.g. Excel on SharePoint'},
|
||||
{label:'Tray Schedule', ph:'e.g. Excel on SharePoint'},
|
||||
{label:'Datasheets', ph:'e.g. Procore'},
|
||||
{label:'Specifications', ph:'e.g. client portal'},
|
||||
{label:'Asset Register', ph:'e.g. CxAlloy, Excel'},
|
||||
{label:'RFIs', ph:'e.g. Procore RFI log'},
|
||||
{label:'Safety Documentation', ph:'e.g. site safety binder'}
|
||||
];
|
||||
|
||||
// Escape a value for safe use inside a double-quoted HTML attribute.
|
||||
// SharePoint "Copy Link" URLs contain & (and labels/notes may contain & " < >),
|
||||
// so attribute values must be escaped or a re-render corrupts the field.
|
||||
function escAttr(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
function renderSources(){
|
||||
const container = document.getElementById('sources-list');
|
||||
if(!state.sources.length) state.sources = DEFAULT_SOURCES.map(s=>({label:s.label, system:'', notes:'', link:'', ph:s.ph}));
|
||||
container.innerHTML = state.sources.map((s,i)=>`
|
||||
<div style="display:grid; grid-template-columns:150px 150px 250px 150px 30px; gap:1rem; align-items:center; padding:1rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
|
||||
<input type="text" value="${escAttr(s.label)}" placeholder="Label" onchange="state.sources[${i}].label=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${escAttr(s.system)}" placeholder="${escAttr(s.ph||'System of record')}" onchange="state.sources[${i}].system=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${escAttr(s.link)}" placeholder="Paste SharePoint 'Copy Link' URL" onchange="state.sources[${i}].link=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${escAttr(s.notes)}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="state.sources.splice(${i},1); renderSources()">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function addSource(){
|
||||
state.sources.push({label:'',system:'',notes:'',link:''});
|
||||
renderSources();
|
||||
}
|
||||
|
||||
// ── STEP NAVIGATION ────────────────────────────────────────────────────────────
|
||||
function goToStep(n){
|
||||
if(!validateStep(currentStep)) return;
|
||||
currentStep = n;
|
||||
updateStepUI();
|
||||
}
|
||||
|
||||
function nextStep(){
|
||||
if(!validateStep(currentStep)) return;
|
||||
if(currentStep < 10){
|
||||
currentStep++;
|
||||
updateStepUI();
|
||||
}
|
||||
}
|
||||
|
||||
function previousStep(){
|
||||
if(currentStep > 1){
|
||||
currentStep--;
|
||||
updateStepUI();
|
||||
}
|
||||
}
|
||||
|
||||
function updateStepUI(){
|
||||
trackStepDwell();
|
||||
track('step_view', {step: currentStep});
|
||||
// SOP steps
|
||||
document.querySelectorAll('[id^="sop-step-"]').forEach(s=>s.style.display='none');
|
||||
document.getElementById(`sop-step-${currentStep}`)?.style.display && (document.getElementById(`sop-step-${currentStep}`).style.display='block');
|
||||
|
||||
// Step indicators
|
||||
document.querySelectorAll('.step-item').forEach(s=>s.classList.remove('active'));
|
||||
document.querySelector(`[data-step="${currentStep}"]`)?.classList.add('active');
|
||||
|
||||
// Update counter
|
||||
document.getElementById('current-step').textContent = currentStep;
|
||||
|
||||
// Update buttons
|
||||
document.getElementById('sop-prev-btn').disabled = currentStep === 1;
|
||||
document.getElementById('sop-next-btn').style.display = currentStep < 10 ? 'block' : 'none';
|
||||
document.getElementById('sop-complete-btn').style.display = currentStep === 10 ? 'block' : 'none';
|
||||
|
||||
// Collect form data
|
||||
collectStepData();
|
||||
}
|
||||
|
||||
function collectStepData(){
|
||||
switch(currentStep){
|
||||
case 1:
|
||||
state.project.name = document.getElementById('proj_name').value;
|
||||
state.project.number = document.getElementById('proj_number').value;
|
||||
state.project.client = document.getElementById('proj_client').value;
|
||||
state.project.division = document.getElementById('proj_division').value;
|
||||
state.project.site = document.getElementById('proj_site').value;
|
||||
break;
|
||||
case 2:
|
||||
state.team.pm = document.getElementById('proj_pm').value;
|
||||
state.team.apm = document.getElementById('proj_apm').value;
|
||||
state.team.cm = document.getElementById('proj_cm').value;
|
||||
state.team.qm = document.getElementById('proj_qm').value;
|
||||
break;
|
||||
case 3:
|
||||
state.signoffRoles[0].name = document.getElementById('role_super_name').value;
|
||||
state.signoffRoles[1].name = document.getElementById('role_foreman_name').value;
|
||||
break;
|
||||
case 5:
|
||||
state.governance.woformat = document.getElementById('gov_woformat').value;
|
||||
state.governance.wosize = document.getElementById('gov_wosize').value;
|
||||
const sel = document.getElementById('gov_issuance');
|
||||
state.governance.issuance = Array.from(sel.selectedOptions).map(o=>o.value);
|
||||
state.governance.disciplines = (document.getElementById('gov_disciplines').value||'')
|
||||
.split(',').map(d=>d.trim()).filter(Boolean);
|
||||
state.governance.discMode = document.getElementById('gov_discmode').value;
|
||||
state.governance.sizeHoursMax = document.getElementById('gov_size_hours_max').value;
|
||||
break;
|
||||
case 6:
|
||||
state.quality.qcreq = document.getElementById('qual_qcreq').value;
|
||||
state.quality.photo = document.getElementById('qual_photo').value;
|
||||
state.quality.hold = document.getElementById('qual_hold').value;
|
||||
break;
|
||||
case 7:
|
||||
state.platforms.tracking = document.getElementById('plat_tracking').value;
|
||||
state.platforms.commissioning = document.getElementById('plat_commissioning').value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function validateStep(n){
|
||||
collectStepData();
|
||||
const proj = state.project;
|
||||
|
||||
if(n===1 && (!proj.name || !proj.number || !proj.client || !proj.division || !proj.site)){
|
||||
alert('Please complete all required fields: Project Name, Number, Client, Division, and Site Location.');
|
||||
return false;
|
||||
}
|
||||
if(n===5 && !state.governance.woformat){
|
||||
alert('Please enter a Work Package Number Format.');
|
||||
return false;
|
||||
}
|
||||
if(n===6 && !state.quality.qcreq){
|
||||
alert('Please select a QC requirement.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── SOP COMPLETION ────────────────────────────────────────────────────────────
|
||||
function completeSOP(){
|
||||
if(!validateStep(10)) return;
|
||||
collectStepData();
|
||||
|
||||
sop = {
|
||||
meta: {tool:'Work Package Configuration', sample:false},
|
||||
project: {
|
||||
name: state.project.name,
|
||||
number: state.project.number,
|
||||
client: state.project.client,
|
||||
division: state.project.division,
|
||||
pm: state.team.pm,
|
||||
apm: state.team.apm,
|
||||
cm: state.team.cm,
|
||||
qm: state.team.qm,
|
||||
site: state.project.site,
|
||||
teamMembers: state.teamMembers.filter(m=>(m.role||m.name))
|
||||
},
|
||||
roles: state.signoffRoles.filter(r=>r.role),
|
||||
governance: {
|
||||
issuance: state.governance.issuance.length ? state.governance.issuance : ['By Sector / Area'],
|
||||
woSize: state.governance.wosize,
|
||||
woFormat: state.governance.woformat,
|
||||
disciplines: (state.governance.disciplines && state.governance.disciplines.length)
|
||||
? state.governance.disciplines : ['Mechanical','Electrical','Tech'],
|
||||
discMode: state.governance.discMode || 'choice',
|
||||
instanceSuffix: state.governance.instanceSuffix || 'letter',
|
||||
sizeHoursMax: state.governance.sizeHoursMax || ''
|
||||
},
|
||||
woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({
|
||||
name: t.name,
|
||||
enabled: true,
|
||||
notes: t.notes || '',
|
||||
approval: t.approval || ''
|
||||
})),
|
||||
sources: state.sources.filter(s=>s.label),
|
||||
field: {trackPlatform: state.platforms.tracking},
|
||||
commissioning: {tool: state.platforms.commissioning},
|
||||
quality: {
|
||||
qcReq: state.quality.qcreq,
|
||||
photo: state.quality.photo,
|
||||
holdPoints: state.quality.hold,
|
||||
cxTool: state.platforms.commissioning
|
||||
},
|
||||
sequence: state.sequence.filter(s=>s.label).map(s=>({
|
||||
label: s.label,
|
||||
kind: s.kind || 'step'
|
||||
})),
|
||||
costCodes: LABOR_COST_CODES,
|
||||
constraints: state.constraints.map(c=>({name: c.name, description: c.description || ''}))
|
||||
};
|
||||
|
||||
sopComplete = true;
|
||||
updateProjectDisplay();
|
||||
|
||||
// Persist for the home page (green / "Review") and for the WP Creator tab.
|
||||
try {
|
||||
localStorage.setItem('wp_suite_sop', JSON.stringify(sop));
|
||||
localStorage.setItem('wp_suite_state', JSON.stringify(state));
|
||||
localStorage.setItem('wp_suite_sop_complete', '1');
|
||||
} catch(e){}
|
||||
|
||||
track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length});
|
||||
|
||||
alert('✓ SOP Configuration Complete!\n\nSwitch to "Work Package Creation" to start creating Work Packages.');
|
||||
|
||||
// Hand the SOP to the embedded Work Package Creator and unlock its tab.
|
||||
if(typeof onSOPReady === 'function') onSOPReady(sop);
|
||||
}
|
||||
|
||||
// ── COMMENTS ──────────────────────────────────────────────────────────────────
|
||||
function toggleComments(){
|
||||
const panel = document.getElementById('comments-panel');
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
if(panel.style.display === 'block') loadStepComments();
|
||||
}
|
||||
|
||||
function submitComment(){
|
||||
const name = document.getElementById('commenter-name').value || 'Anonymous';
|
||||
const text = document.getElementById('comment-text').value.trim();
|
||||
|
||||
if(!text){ alert('Please enter a comment.'); return; }
|
||||
|
||||
const comment = {
|
||||
step: currentStep,
|
||||
name: name,
|
||||
text: text,
|
||||
timestamp: new Date().toLocaleString()
|
||||
};
|
||||
|
||||
allComments.push(comment);
|
||||
localStorage.setItem('wp_suite_comments', JSON.stringify(allComments));
|
||||
if(window.postFeedback) window.postFeedback({type:'sop_step_comment', ...comment});
|
||||
|
||||
document.getElementById('comment-text').value = '';
|
||||
document.getElementById('commenter-name').value = '';
|
||||
loadStepComments();
|
||||
alert('✓ Comment submitted!');
|
||||
}
|
||||
|
||||
function exportComments(){
|
||||
const saved = localStorage.getItem('wp_suite_comments');
|
||||
const data = saved ? JSON.parse(saved) : [];
|
||||
if(!data.length){ alert('No comments to export yet.'); return; }
|
||||
const payload = {app:'Work Package Suite', source:'sop', 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-comments-sop-' + new Date().toISOString().slice(0,10) + '.json';
|
||||
a.click();
|
||||
setTimeout(()=>URL.revokeObjectURL(a.href), 1000);
|
||||
}
|
||||
|
||||
function importComments(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 comments found in that file.'); return; }
|
||||
const saved = localStorage.getItem('wp_suite_comments');
|
||||
allComments = saved ? JSON.parse(saved) : [];
|
||||
const seen = new Set(allComments.map(c=>c.step+'|'+c.timestamp+'|'+c.text));
|
||||
let added = 0;
|
||||
incoming.forEach(c=>{ const k=c.step+'|'+c.timestamp+'|'+c.text; if(c.text && !seen.has(k)){ allComments.push(c); seen.add(k); added++; }});
|
||||
localStorage.setItem('wp_suite_comments', JSON.stringify(allComments));
|
||||
loadStepComments();
|
||||
alert('Imported ' + added + ' comment' + (added===1?'':'s') + '.');
|
||||
}catch(e){ alert('Could not read that file.'); }
|
||||
ev.target.value = '';
|
||||
};
|
||||
r.readAsText(f);
|
||||
}
|
||||
|
||||
function loadStepComments(){
|
||||
const saved = localStorage.getItem('wp_suite_comments');
|
||||
if(saved) allComments = JSON.parse(saved);
|
||||
|
||||
const stepComments = allComments.filter(c=>c.step===currentStep);
|
||||
const list = document.getElementById('comments-list');
|
||||
|
||||
if(stepComments.length === 0){
|
||||
list.innerHTML = '<div style="font-size:12px; color:var(--text-dim); font-style:italic;">No comments yet on this step.</div>';
|
||||
}else{
|
||||
list.innerHTML = stepComments.map(c=>`
|
||||
<div style="padding:0.5rem; background:white; border:1px solid var(--border); border-radius:4px; margin-bottom:0.5rem;">
|
||||
<div style="font-size:11px; color:var(--text-dim); margin-bottom:0.25rem;"><strong>${c.name}</strong> • ${c.timestamp}</div>
|
||||
<div style="font-size:12px; color:var(--text);">${c.text.replace(/</g,'<').replace(/>/g,'>')}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ── USAGE ANALYTICS ─────────────────────────────────────────────────────────
|
||||
// Lightweight usage analytics stored in localStorage so the tool owner can review
|
||||
// engagement over time. No field VALUES are stored (field-edit events record only
|
||||
// the field id), keeping captured data non-sensitive.
|
||||
const ANALYTICS_KEY = 'wp_suite_analytics_v1';
|
||||
let _stepEnter = Date.now();
|
||||
const _session = 's_' + Date.now().toString(36) + Math.random().toString(36).slice(2,6);
|
||||
|
||||
function analyticsLoad(){
|
||||
try { return JSON.parse(localStorage.getItem(ANALYTICS_KEY)) || {events:[]}; }
|
||||
catch(e){ return {events:[]}; }
|
||||
}
|
||||
function analyticsSave(data){
|
||||
try { localStorage.setItem(ANALYTICS_KEY, JSON.stringify(data)); }
|
||||
catch(e){ /* storage unavailable — degrade silently */ }
|
||||
}
|
||||
function track(event, detail){
|
||||
try {
|
||||
const data = analyticsLoad();
|
||||
data.events.push({ ts: new Date().toISOString(), session: _session, event, detail: detail||null });
|
||||
if(data.events.length > 5000) data.events = data.events.slice(-5000);
|
||||
analyticsSave(data);
|
||||
} catch(e){}
|
||||
}
|
||||
function trackStepDwell(){
|
||||
const ms = Date.now() - _stepEnter;
|
||||
if(ms > 400 && ms < 1000*60*60) track('step_dwell', {step: currentStep, ms});
|
||||
_stepEnter = Date.now();
|
||||
}
|
||||
function analyticsSummary(){
|
||||
const data = analyticsLoad();
|
||||
const byEvent = {}, byStep = {}, dwell = {}, sessions = new Set();
|
||||
data.events.forEach(e=>{
|
||||
byEvent[e.event] = (byEvent[e.event]||0)+1;
|
||||
sessions.add(e.session);
|
||||
if(e.event==='step_view' && e.detail) byStep[e.detail.step]=(byStep[e.detail.step]||0)+1;
|
||||
if(e.event==='step_dwell' && e.detail){ dwell[e.detail.step]=(dwell[e.detail.step]||0)+e.detail.ms; }
|
||||
});
|
||||
return {total:data.events.length, sessions:sessions.size, byEvent, byStep, dwell, first:data.events[0]?.ts, last:data.events[data.events.length-1]?.ts};
|
||||
}
|
||||
function showAnalytics(){
|
||||
const s = analyticsSummary();
|
||||
const fmtMin = ms => (ms/60000).toFixed(1)+' min';
|
||||
let txt = `USAGE LOGS\n\nSessions: ${s.sessions} Events: ${s.total}\nRange: ${s.first?new Date(s.first).toLocaleString():'—'} → ${s.last?new Date(s.last).toLocaleString():'—'}\n\nStep views:\n`;
|
||||
for(let i=1;i<=10;i++) txt += ` Step ${i}: ${s.byStep[i]||0} views` + (s.dwell[i]?`, ${fmtMin(s.dwell[i])} total`:'') + `\n`;
|
||||
txt += `\nActions:\n`;
|
||||
Object.keys(s.byEvent).filter(k=>!['step_view','step_dwell','field_edit'].includes(k)).forEach(k=> txt += ` ${k}: ${s.byEvent[k]}\n`);
|
||||
txt += ` field edits: ${s.byEvent['field_edit']||0}\n`;
|
||||
txt += `\nDownload full event log as JSON?`;
|
||||
if(confirm(txt)) downloadAnalytics();
|
||||
}
|
||||
function downloadAnalytics(){
|
||||
const data = analyticsLoad();
|
||||
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'wp-suite-usage-' + new Date().toISOString().slice(0,10) + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
track('analytics_exported');
|
||||
}
|
||||
538
html/work-package-suite-styles.css
Normal file
538
html/work-package-suite-styles.css
Normal file
@@ -0,0 +1,538 @@
|
||||
:root {
|
||||
--primary: #2563eb;
|
||||
--primary-light: #dbeafe;
|
||||
--success: #16a34a;
|
||||
--warning: #ea580c;
|
||||
--danger: #dc2626;
|
||||
--text: #1f2937;
|
||||
--text-light: #6b7280;
|
||||
--text-dim: #9ca3af;
|
||||
--border: #e5e7eb;
|
||||
--bg: #f9fafb;
|
||||
--bg-card: #ffffff;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 10px 25px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
.header {
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.logo:hover { opacity: 0.7; }
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--primary-light);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-light);
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg);
|
||||
color: var(--primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-button:hover {
|
||||
background: var(--primary-light);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.step-counter {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-light);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* MAIN NAVIGATION */
|
||||
.main-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 2rem;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.tab-icon { font-size: 16px; }
|
||||
|
||||
/* CONTENT AREA */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tool {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tool.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* STEP NAV */
|
||||
.step-nav {
|
||||
margin-bottom: 2rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.steps-container {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
min-width: min-content;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--border);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.step-item:hover { background: var(--primary-light); border-color: var(--primary); }
|
||||
.step-item.active { background: var(--primary); color: white; border-color: var(--primary); }
|
||||
|
||||
/* STEP CONTENT */
|
||||
.step-content {
|
||||
background: var(--bg-card);
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.step { display: none; }
|
||||
|
||||
.step h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.notice {
|
||||
font-size: 13px;
|
||||
color: var(--text-light);
|
||||
background: var(--primary-light);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: 4px solid var(--primary);
|
||||
}
|
||||
|
||||
/* FIELDS */
|
||||
.field-grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.field-grid.col1 { grid-template-columns: 1fr; }
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select,
|
||||
.field textarea {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field select:focus,
|
||||
.field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--primary-light);
|
||||
}
|
||||
|
||||
.field small {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ROLES */
|
||||
.required-roles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.role-required {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.role-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.role-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.role-checkbox label {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* WP TYPES */
|
||||
#wp-types-table { overflow-x: auto; }
|
||||
|
||||
.wp-type-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 90px 2fr 1.5fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.wp-types-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 90px 2fr 1.5fr;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* BUTTONS */
|
||||
.add-btn {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.add-btn:hover { background: #1d4ed8; }
|
||||
|
||||
.nav-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-btn:hover { border-color: var(--primary); color: var(--primary); }
|
||||
|
||||
.nav-btn.primary {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.nav-btn.primary:hover { background: #15803d; border-color: #15803d; }
|
||||
|
||||
.nav-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
/* NAVIGATION */
|
||||
.step-navigation {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* COMMENTS */
|
||||
.comments-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.comments-toggle {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.comments-toggle:hover { background: var(--primary); color: white; }
|
||||
|
||||
/* CONSTRUCTION SEQUENCE (drag & drop) */
|
||||
#sequence-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.seq-step {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 14px;
|
||||
background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px;
|
||||
box-shadow: var(--shadow); transition: border-color .12s, box-shadow .12s, opacity .12s;
|
||||
}
|
||||
.seq-step:hover { border-color: var(--primary); }
|
||||
.seq-step.dragging { opacity: .35; border-style: dashed; }
|
||||
.seq-step.drag-over { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-light); }
|
||||
.seq-handle { cursor: grab; color: var(--text-dim); font-size: 16px; line-height: 1; user-select: none; flex-shrink: 0; }
|
||||
.seq-handle:active { cursor: grabbing; }
|
||||
.seq-num {
|
||||
width: 24px; height: 24px; flex-shrink: 0; border-radius: 50%; background: var(--primary-light); color: var(--primary);
|
||||
font-size: 11px; font-weight: 600; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.seq-step input.seq-label {
|
||||
border: 1px solid transparent; background: transparent; font-size: 14px; padding: 5px 8px; color: var(--text); flex: 1; border-radius: 4px;
|
||||
}
|
||||
.seq-step input.seq-label:focus { background: var(--bg); border-color: var(--primary); outline: none; }
|
||||
.seq-del {
|
||||
background: var(--danger); color: white; border: none; border-radius: 4px;
|
||||
width: 28px; height: 28px; cursor: pointer; font-weight: 600; flex-shrink: 0;
|
||||
}
|
||||
.seq-arrow { text-align: center; color: var(--text-dim); font-size: 13px; line-height: .4; margin: -2px 0; }
|
||||
.seq-step.gate { border-color: var(--warning); background: #fff7ed; border-style: dashed; }
|
||||
.seq-step.gate .seq-label { color: var(--warning); font-weight: 500; }
|
||||
.seq-gate-badge {
|
||||
flex-shrink: 0; padding: 3px 9px; border-radius: 20px; background: var(--warning); color: #fff;
|
||||
font-size: 9px; font-weight: 600; letter-spacing: .08em; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* STEP COMMENTS DROPDOWN */
|
||||
.comments-dropdown {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 2rem;
|
||||
width: 360px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 1.25rem;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.comments-dropdown-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comments-dropdown-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: var(--text-light);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.comments-dropdown-close:hover { color: var(--text); }
|
||||
|
||||
/* MODAL */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-header h3 { font-size: 18px; margin: 0; }
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.modal-close:hover { color: var(--text); }
|
||||
|
||||
/* UTILITY */
|
||||
.sub-heading {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 768px) {
|
||||
.header { flex-direction: column; text-align: center; gap: 1rem; }
|
||||
.main-nav { flex-wrap: wrap; }
|
||||
.content-area { padding: 1rem; }
|
||||
.step-content { padding: 1rem; }
|
||||
.wp-type-row { grid-template-columns: 1fr; }
|
||||
.wp-types-header { display: none; }
|
||||
.step-navigation { flex-direction: column; }
|
||||
}
|
||||
374
html/work-package-suite.html
Normal file
374
html/work-package-suite.html
Normal file
@@ -0,0 +1,374 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Work Package Suite</title>
|
||||
<link rel="icon" href="favicon.ico" sizes="any">
|
||||
<link rel="stylesheet" href="theme-light.css">
|
||||
<link rel="stylesheet" href="work-package-suite-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<a href="index.html" class="logo" title="Back to Home">
|
||||
<img src="prime-controls-logo.jpg" alt="Prime Controls" style="height: 36px; width: auto;">
|
||||
</a>
|
||||
<div>
|
||||
<div class="header-title">Work Package Suite</div>
|
||||
<div class="header-subtitle" id="project-display"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="load-sample-btn" class="header-button" onclick="loadSampleData()" title="Load example SOP data">⭐ Load Sample</button>
|
||||
<button class="header-button" onclick="toggleComments()" title="View and add comments for the current step">💬 Step Comments</button>
|
||||
<button class="header-button" onclick="showAnalytics()" title="Review usage logs for this tool">📊 Usage Logs</button>
|
||||
<span class="step-counter"><span id="current-step">1</span> / <span id="total-steps">10</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN NAVIGATION -->
|
||||
<div class="main-nav">
|
||||
<button class="nav-tab active" data-tab="sop" onclick="switchTool('sop')">
|
||||
<span class="tab-icon">⚙️</span> SOP Configuration
|
||||
</button>
|
||||
<button class="nav-tab" data-tab="wp" onclick="switchTool('wp')">
|
||||
<span class="tab-icon">📋</span> Work Package Creation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- CONTENT AREA -->
|
||||
<div class="content-area">
|
||||
|
||||
<!-- ═════════════════════════════════════════════════════════════════════ -->
|
||||
<!-- SOP CONFIGURATION TOOL -->
|
||||
<!-- ═════════════════════════════════════════════════════════════════════ -->
|
||||
<div id="tool-sop" class="tool active">
|
||||
|
||||
<!-- SOP STEP INDICATORS -->
|
||||
<div class="step-nav">
|
||||
<div class="steps-container">
|
||||
<div class="step-item active" data-step="1" onclick="goToStep(1)">Project</div>
|
||||
<div class="step-item" data-step="2" onclick="goToStep(2)">Team</div>
|
||||
<div class="step-item" data-step="3" onclick="goToStep(3)">Sign-Offs</div>
|
||||
<div class="step-item" data-step="4" onclick="goToStep(4)">WP Types</div>
|
||||
<div class="step-item" data-step="5" onclick="goToStep(5)">Governance</div>
|
||||
<div class="step-item" data-step="6" onclick="goToStep(6)">Quality</div>
|
||||
<div class="step-item" data-step="7" onclick="goToStep(7)">Platforms</div>
|
||||
<div class="step-item" data-step="8" onclick="goToStep(8)">Sequence</div>
|
||||
<div class="step-item" data-step="9" onclick="goToStep(9)">Constraints</div>
|
||||
<div class="step-item" data-step="10" onclick="goToStep(10)">Sources</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SOP STEP CONTENT -->
|
||||
<div class="step-content">
|
||||
|
||||
<!-- STEP 1: PROJECT BASICS -->
|
||||
<div class="step" id="sop-step-1" style="display: block;">
|
||||
<h2>1. Project Basics</h2>
|
||||
<div class="notice">Define the core project information that will be inherited by all Work Packages.</div>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label>Project Name *</label>
|
||||
<input type="text" id="proj_name" placeholder="e.g., Micron — INC Construction Work Packages" oninput="updateProjectDisplay()">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Project Number *</label>
|
||||
<input type="text" id="proj_number" placeholder="e.g., 26-67-008">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Client Name *</label>
|
||||
<input type="text" id="proj_client" placeholder="e.g., Micron Technology, Inc.">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Division / Sector *</label>
|
||||
<input type="text" id="proj_division" placeholder="e.g., Semiconductor, Oil & Gas, Data Center">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Site Location *</label>
|
||||
<input type="text" id="proj_site" placeholder="e.g., Boise, ID — Fab 7">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2: PROJECT TEAM -->
|
||||
<div class="step" id="sop-step-2" style="display: none;">
|
||||
<h2>2. Project Team Leadership</h2>
|
||||
<div class="notice">Name the key project leaders. These are informational and will appear in SOP exports.</div>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label>Project Manager (PM)</label>
|
||||
<input type="text" id="proj_pm" placeholder="e.g., Mariano Sanchez">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Assistant Project Manager (APM)</label>
|
||||
<input type="text" id="proj_apm" placeholder="e.g., Assistant PM name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Construction Manager (CM)</label>
|
||||
<input type="text" id="proj_cm" placeholder="e.g., K. Boyd">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Quality Manager (QM)</label>
|
||||
<input type="text" id="proj_qm" placeholder="e.g., D. Nguyen">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
|
||||
<div class="sub-heading">Additional Team Members (optional)</div>
|
||||
<div id="team-members-list" style="margin-top: 1rem;"></div>
|
||||
<button class="add-btn" onclick="addTeamMember()">+ Add Team Member</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 3: SIGN-OFF ROLES -->
|
||||
<div class="step" id="sop-step-3" style="display: none;">
|
||||
<h2>3. Required Sign-Off Roles</h2>
|
||||
<div class="notice">Superintendent and Foreman are required. Add other roles as needed for your project structure.</div>
|
||||
<div class="required-roles">
|
||||
<div class="role-required">
|
||||
<div class="role-checkbox">
|
||||
<input type="checkbox" id="role_super" checked disabled>
|
||||
<label>Superintendent *</label>
|
||||
</div>
|
||||
<input type="text" id="role_super_name" placeholder="Name (optional)" style="flex: 1; margin-left: 1rem;">
|
||||
</div>
|
||||
<div class="role-required">
|
||||
<div class="role-checkbox">
|
||||
<input type="checkbox" id="role_foreman" checked disabled>
|
||||
<label>Foreman *</label>
|
||||
</div>
|
||||
<input type="text" id="role_foreman_name" placeholder="Name (optional)" style="flex: 1; margin-left: 1rem;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
|
||||
<div class="sub-heading">Optional Additional Roles</div>
|
||||
<div id="optional-roles-list" style="margin-top: 1rem;"></div>
|
||||
<button class="add-btn" onclick="addOptionalRole()">+ Add Role</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 4: WORK PACKAGE TYPES -->
|
||||
<div class="step" id="sop-step-4" style="display: none;">
|
||||
<h2>4. Work Package Types</h2>
|
||||
<div class="notice">Enable the WP types your project will use. Add any special rules and the roles required to approve WO completion.</div>
|
||||
<div id="wp-types-table" style="margin-top: 1.5rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 5: GOVERNANCE -->
|
||||
<div class="step" id="sop-step-5" style="display: none;">
|
||||
<h2>5. Governance & WP Numbering</h2>
|
||||
<div class="notice">Define how Work Packages are formatted, sized, and issued on this project. The choices here decide how the Work Package Creator behaves for every package.</div>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label>Work Package Number Format *</label>
|
||||
<input type="text" id="gov_woformat" placeholder="e.g., WP##-[Sector]-[TYPE]">
|
||||
<small>Use ## for counter, [Sector] [TYPE] as variables</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Issuance Strategy</label>
|
||||
<select id="gov_issuance" multiple size="3">
|
||||
<option selected>By Sector / Area</option>
|
||||
<option>By Discipline</option>
|
||||
<option>By Phase / Sequence</option>
|
||||
<option>By Resource Availability</option>
|
||||
</select>
|
||||
<small>Hold Ctrl to select multiple</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin:1.4rem 0 .4rem">How will Work Packages use disciplines?</h3>
|
||||
<div class="notice">Decide whether a single package can carry more than one discipline (e.g. a chiller skid needing Mechanical install + Electrical wire-pull + Tech terminations), or whether each discipline gets its own package. This drives whether the Creator shows per-discipline scope sections and the <strong>Split by Discipline</strong> button.</div>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label>Disciplines on this project</label>
|
||||
<input type="text" id="gov_disciplines" placeholder="Mechanical, Electrical, Tech">
|
||||
<small>Comma-separated. These appear as scope sections and instance suffixes in the Creator.</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Discipline strategy *</label>
|
||||
<select id="gov_discmode">
|
||||
<option value="choice">Let the planner choose per package (recommended)</option>
|
||||
<option value="single">One discipline per package (many small packages)</option>
|
||||
<option value="multi">Multiple disciplines per package (scope split by discipline)</option>
|
||||
</select>
|
||||
<small>"Choose per package" lets the planner build a large multi-discipline package and split it later.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin:1.4rem 0 .4rem">Work Package sizing</h3>
|
||||
<div class="notice">A Work Package should be a manageable, trackable chunk of work — typically a 1–2 week assignment. The Creator warns the planner when a package exceeds the ceiling so it can be broken down.</div>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label>Typical WP Size (guidance)</label>
|
||||
<input type="text" id="gov_wosize" placeholder="e.g., 3–5 days or 40–80 hours">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Split threshold — max labor hours</label>
|
||||
<input type="number" id="gov_size_hours_max" min="0" step="1" placeholder="e.g., 120">
|
||||
<small>The Creator flags packages above this so they can be split (by discipline or scope).</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 6: QUALITY -->
|
||||
<div class="step" id="sop-step-6" style="display: none;">
|
||||
<h2>6. Quality & Inspection Strategy</h2>
|
||||
<div class="notice">Establish project-wide quality expectations that cascade to every Work Package.</div>
|
||||
<div class="field-grid col1">
|
||||
<div class="field">
|
||||
<label>QC Required? *</label>
|
||||
<select id="qual_qcreq">
|
||||
<option>Yes</option>
|
||||
<option>Yes — Detailed inspection items</option>
|
||||
<option>Yes — Sample / Spot-check</option>
|
||||
<option>No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Photo / Documentation Standard</label>
|
||||
<select id="qual_photo">
|
||||
<option selected>Key checkpoints only</option>
|
||||
<option>Every step documented</option>
|
||||
<option>As-built final condition only</option>
|
||||
<option>None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Hold Points & Witness Requirements</label>
|
||||
<textarea id="qual_hold" rows="3" placeholder="e.g., HOLD: Prime QAQC to inspect rough-in before cover/cover-up. WITNESS: client QC to observe megger test before energization."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 7: PLATFORMS -->
|
||||
<div class="step" id="sop-step-7" style="display: none;">
|
||||
<h2>7. Tracking & Commissioning Platforms</h2>
|
||||
<div class="notice">Select the tools used for construction tracking and commissioning. These can be the same or different systems.</div>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label>Construction Tracking Platform *</label>
|
||||
<select id="plat_tracking">
|
||||
<option>CxAlloy</option>
|
||||
<option>Procore</option>
|
||||
<option>Autodesk Construction Cloud (ACC)</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Commissioning Tool *</label>
|
||||
<select id="plat_commissioning">
|
||||
<option selected>CxAlloy</option>
|
||||
<option>Procore</option>
|
||||
<option>Autodesk Construction Cloud (ACC)</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 8: SEQUENCE -->
|
||||
<div class="step" id="sop-step-8" style="display: none;">
|
||||
<h2>8. Construction Sequence</h2>
|
||||
<div class="notice">Comes pre-sequenced with a typical install flow. Drag the ⠿ handle to reorder steps, edit labels inline, add a QC hold gate, or remove any step.</div>
|
||||
<div id="sequence-list" style="margin-top: 1rem;"></div>
|
||||
<div style="display:flex; gap:0.5rem; margin-top:1rem; flex-wrap:wrap;">
|
||||
<input type="text" id="seq-add-input" placeholder="New step name" onkeydown="if(event.key==='Enter'){addSequenceStep();}" style="flex:1; min-width:200px; padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
<button class="add-btn" onclick="addSequenceStep()">+ Add Step</button>
|
||||
<button class="add-btn" onclick="addSequenceGate()" style="background:var(--warning);">◆ Add QC Hold</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 9: CONSTRAINTS -->
|
||||
<div class="step" id="sop-step-9" style="display: none;">
|
||||
<h2>9. Release Gate Constraints</h2>
|
||||
<div class="notice">Constraints are readiness items that must be cleared before a WP can be issued.</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<div class="sub-heading">Standard AWP Constraints (Vol II §2.3.2)</div>
|
||||
<div id="standard-constraints" style="margin-top: 1rem;"></div>
|
||||
</div>
|
||||
<div style="border-top: 1px solid var(--border); padding-top: 1.5rem;">
|
||||
<div class="sub-heading">Custom Constraints (Optional)</div>
|
||||
<div id="custom-constraints-list" style="margin-top: 1rem;"></div>
|
||||
<button class="add-btn" onclick="showConstraintLibrary()">+ Add Custom Constraint</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 10: SOURCES -->
|
||||
<div class="step" id="sop-step-10" style="display: none;">
|
||||
<h2>10. Engineering Sources & References</h2>
|
||||
<div class="notice">Link to key documents and systems that WP authors will reference.</div>
|
||||
<div id="sources-list" style="margin-top: 1rem;"></div>
|
||||
<button class="add-btn" onclick="addSource()">+ Add Source</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SOP NAVIGATION -->
|
||||
<div class="step-navigation">
|
||||
<button class="nav-btn" id="sop-prev-btn" onclick="previousStep()">← Back</button>
|
||||
<button class="nav-btn" id="sop-next-btn" onclick="nextStep()">Next →</button>
|
||||
<button class="nav-btn primary" id="sop-complete-btn" onclick="completeSOP()" style="display: none;">✓ SOP Complete</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ═════════════════════════════════════════════════════════════════════ -->
|
||||
<!-- WORK PACKAGE CREATION TOOL -->
|
||||
<!-- ═════════════════════════════════════════════════════════════════════ -->
|
||||
<div id="tool-wp" class="tool">
|
||||
<!-- Shown until the SOP is complete -->
|
||||
<div id="wp-gate" style="padding: 3rem 2rem; text-align: center;">
|
||||
<h2>📋 Work Package Creation</h2>
|
||||
<p style="color: var(--text-light); margin: 1rem 0;">Complete the SOP Configuration first to enable Work Package creation. Once the SOP is finished, the full creator loads here with your project defaults pre-populated.</p>
|
||||
<button class="nav-btn primary" onclick="switchTool('sop')" style="margin-top: 1rem;">← Go to SOP Configuration</button>
|
||||
</div>
|
||||
<!-- The real Work Package Creator, embedded once the SOP is complete -->
|
||||
<iframe id="wp-frame" title="Work Package Creator" style="display:none; width:100%; border:0; min-height: calc(100vh - 200px);"></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- STEP COMMENTS DROPDOWN (toggled from header) -->
|
||||
<div id="comments-panel" class="comments-dropdown" style="display: none;">
|
||||
<div class="comments-dropdown-header">
|
||||
<strong>💬 Step Comments</strong>
|
||||
<button onclick="toggleComments()" class="comments-dropdown-close" title="Close">✕</button>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="font-weight: 600; font-size: 13px;">Your Name (optional)</label>
|
||||
<input type="text" id="commenter-name" placeholder="e.g., Bill Clarida" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem;">
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="font-weight: 600; font-size: 13px;">Feedback</label>
|
||||
<textarea id="comment-text" rows="3" placeholder="Your feedback here..." style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem; font-family: inherit;"></textarea>
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
|
||||
<button onclick="submitComment()" style="background: var(--primary); color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">Submit</button>
|
||||
<button onclick="exportComments()" style="background: var(--bg); color: var(--text); border: 1px solid var(--border); padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-weight: 600;">⤓ Export</button>
|
||||
<button onclick="document.getElementById('sop-comments-import').click()" style="background: var(--bg); color: var(--text); border: 1px solid var(--border); padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-weight: 600;">⤒ Import</button>
|
||||
<input type="file" id="sop-comments-import" accept="application/json" style="display:none" onchange="importComments(event)">
|
||||
</div>
|
||||
<div id="comments-list" style="margin-top: 1rem; max-height: 240px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
|
||||
<!-- CONSTRAINT LIBRARY MODAL -->
|
||||
<div id="constraint-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Add Custom Constraint</h3>
|
||||
<button class="modal-close" onclick="closeConstraintModal()">✕</button>
|
||||
</div>
|
||||
<div id="constraint-library" style="max-height: 400px; overflow-y: auto; margin: 1rem 0;"></div>
|
||||
<button class="nav-btn" onclick="closeConstraintModal()">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="feedback-config.js"></script>
|
||||
<script src="work-package-suite-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1034
html/wp-creation-app.js
Normal file
1034
html/wp-creation-app.js
Normal file
File diff suppressed because it is too large
Load Diff
281
html/wp-creation-index.html
Normal file
281
html/wp-creation-index.html
Normal file
@@ -0,0 +1,281 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Work Package (IWP) — Prime Controls</title>
|
||||
<link rel="icon" href="favicon.ico" sizes="any">
|
||||
<link rel="stylesheet" href="theme-light.css">
|
||||
<link rel="stylesheet" href="wp-creation-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="loading-overlay" id="loadingOverlay"><div class="spinner"></div><div class="loading-text">Saving work package…</div></div>
|
||||
|
||||
<div class="header">
|
||||
<div class="logo-wrap">
|
||||
<div class="header-logo">Prime Controls</div>
|
||||
<button id="dev-toggle" class="dev-toggle" onclick="toggleDevMode()" title="dev mode" aria-label="dev mode"></button>
|
||||
</div>
|
||||
<div class="header-sep">|</div>
|
||||
<div class="header-title">Work Package (IWP)</div>
|
||||
<button class="btn btn-ghost embed-hide" style="margin-left:auto;padding:7px 16px" onclick="document.getElementById('sop-import').click()">⤒ Import SOP</button>
|
||||
<input type="file" id="sop-import" accept="application/json" style="display:none" onchange="importSOP(event)">
|
||||
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="loadSampleSOP()">⤓ Sample SOP</button>
|
||||
<button class="btn btn-ghost embed-first" style="padding:7px 16px" onclick="openSopModal()">👁 View SOP</button>
|
||||
<button class="btn btn-ghost" style="padding:7px 16px" onclick="loadExample()">★ Load Example</button>
|
||||
<button class="btn btn-ghost" style="padding:7px 16px" onclick="showDashboard()">📊 Dashboard</button>
|
||||
<button class="btn btn-ghost" style="padding:7px 16px" onclick="newPackage()">+ New</button>
|
||||
<button class="btn btn-ghost" id="comments-btn" style="padding:7px 16px" onclick="toggleComments()">💬 Comments <span class="cbadge-total" id="cbadge-total" style="display:none">0</span></button>
|
||||
<button class="btn btn-ghost" style="padding:7px 16px" onclick="showAnalytics()">▤ Usage Data</button>
|
||||
</div>
|
||||
|
||||
<div class="dev-banner" id="dev-banner" style="display:none">⚙ DEV MODE — usage tracking paused. This session's actions are not being recorded.</div>
|
||||
|
||||
<div class="ctx-bar" id="ctx-bar"></div>
|
||||
|
||||
<!-- RELEASE READINESS BANNER -->
|
||||
<div class="release-banner" id="release-banner"></div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- GENERAL INFORMATION -->
|
||||
<div class="card">
|
||||
<div class="section-header"><div class="section-title">General Information</div>
|
||||
<div class="section-desc">Parameters in <span style="color:var(--accent)">blue</span> are inherited from the project SOP. Fill the rest for this package.</div></div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>WP Number <span class="auto-tag">auto</span></label><input type="text" id="wp_number" readonly class="locked-field" placeholder="auto-built"><div class="field-hint sop-hint" id="wp_number_hint"></div></div>
|
||||
<div class="field"><label>Status</label>
|
||||
<div class="radio-group" id="status-group" style="margin-bottom:0">
|
||||
<label class="radio-pill" data-val="Draft"><input type="radio" name="status"><span class="dot"></span>Draft</label>
|
||||
<label class="radio-pill" data-val="Scheduled"><input type="radio" name="status"><span class="dot"></span>Scheduled</label>
|
||||
<label class="radio-pill" data-val="Issued"><input type="radio" name="status"><span class="dot"></span>Issued</label>
|
||||
<label class="radio-pill" data-val="In Progress"><input type="radio" name="status"><span class="dot"></span>In Progress</label>
|
||||
<label class="radio-pill pill-hold" data-val="Issue"><input type="radio" name="status"><span class="dot"></span>Issue (Hold)</label>
|
||||
<label class="radio-pill" data-val="QC"><input type="radio" name="status"><span class="dot"></span>QC</label>
|
||||
<label class="radio-pill" data-val="Closed"><input type="radio" name="status"><span class="dot"></span>Closed</label>
|
||||
</div>
|
||||
<div class="field-hint">Cannot move to <strong>Issued</strong> or beyond until all constraints are cleared.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notice">WP number builds automatically from these scope fields + the WP type (per the SOP naming format):</div>
|
||||
<div class="field-grid" id="number-dims"></div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Subject / Title <span class="req">*</span></label><input type="text" id="wp_subject" placeholder="e.g. Utility Level 2P Inert Gas Room Wall Mount Midas/ Rack"></div></div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>WP Type <span class="req">*</span></label><select id="wp_type" onchange="onTypeChange()"></select><div class="field-hint sop-hint">from SOP types</div></div>
|
||||
<div class="field"><label>System / Facility Code / UPN</label><input type="text" id="wp_system" placeholder="ties to controls.dev / COIN"></div>
|
||||
<div class="field"><label>Location</label><input type="text" id="wp_location" placeholder="building / level / sector / room"></div>
|
||||
<div class="field"><label>Cost Code</label><select id="wp_cost"></select><div class="field-hint sop-hint">Acumatica cost codes</div></div>
|
||||
<div class="field"><label>Acumatica Task</label><input type="text" id="wp_wbs" placeholder="Acumatica task no."></div>
|
||||
</div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>Assignees</label><input type="text" id="wp_assignees" placeholder="name (company), name (company)"></div>
|
||||
<div class="field"><label>Distribution</label><input type="text" id="wp_distribution" placeholder="notify list"></div>
|
||||
<div class="field"><label>Due Date</label><input type="date" id="wp_due"></div>
|
||||
<div class="field"><label>Specification Section</label><input type="text" id="wp_spec" placeholder="e.g. 26_05_33_00 - Raceway and Boxes"><div class="field-hint" id="spec-folder-link"></div></div>
|
||||
</div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Description</label><textarea id="wp_desc" rows="2" placeholder="Short summary of the package"></textarea></div></div>
|
||||
</div>
|
||||
|
||||
<!-- ASSETS (controls.dev) -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Assets</div>
|
||||
<div class="notice">Every work package is based on one or more assets managed in <strong>controls.dev</strong>. Paste the controls.dev link for each asset this package covers. <span style="color:var(--text-dim)">A direct integration to pick assets from a list is planned — for now, link them manually.</span></div>
|
||||
<div class="table-wrap"><table><thead><tr><th style="width:200px">Asset Tag / ID</th><th>Description</th><th>controls.dev Link <span class="req">*</span></th><th style="width:44px"></th></tr></thead><tbody id="asset-body"></tbody></table></div>
|
||||
<button class="add-btn" onclick="addAsset()">+ Add Asset</button>
|
||||
</div>
|
||||
|
||||
<!-- DISCIPLINES -->
|
||||
<div class="card" id="discipline-card" style="display:none">
|
||||
<div class="sub-heading">Disciplines</div>
|
||||
<div class="notice" id="discipline-note"></div>
|
||||
<div class="disc-picker" id="discipline-picker"></div>
|
||||
</div>
|
||||
|
||||
<!-- SCOPE & WORK -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Scope & Work</div>
|
||||
<div id="flat-scope">
|
||||
<div class="field"><label>Description of Work (sequenced steps)</label>
|
||||
<div class="notice">Enter the work as ordered steps — added in sequence, the way the crew performs them.</div>
|
||||
<div id="worksteps-body"></div>
|
||||
<button class="add-btn" onclick="addWorkStep()">+ Add Step</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scope-by-discipline" style="display:none"></div>
|
||||
<button class="btn btn-ghost" id="split-disc-btn" style="display:none;margin-top:10px" onclick="splitByDiscipline()" title="Break this multi-discipline package into one numbered instance per discipline">⎘ Split by Discipline</button>
|
||||
<div class="field-grid" style="margin-top:14px">
|
||||
<div class="field"><label>Labor – Est. Hrs.</label><input type="number" id="wp_hours" min="0" step="1" placeholder="e.g. 20" oninput="onHoursChange()"><div class="field-hint" id="size-check"></div></div>
|
||||
<div class="field"><label>Package Predecessor</label><select id="wp_seq"></select><div class="field-hint">The package/step (from the SOP sequence) that must finish before this work can start. Choose "None" if it has no predecessor.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MATERIAL LIST -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Material List</div>
|
||||
<div class="notice">Structured bill of materials. Feeds kitting and the delivery forecast. Unit is from the Acumatica unit list.</div>
|
||||
<div class="table-wrap"><table><thead><tr><th style="width:90px">Qty</th><th style="width:120px">Unit</th><th>Description</th><th id="mat-disc-th" style="width:140px;display:none">Discipline</th><th style="width:44px"></th></tr></thead><tbody id="material-body"></tbody></table></div>
|
||||
<div class="material-actions">
|
||||
<button class="add-btn" onclick="addMaterial()">+ Add Material Line</button>
|
||||
<button class="add-btn" onclick="document.getElementById('material-import').click()">⤒ Import from Excel/CSV</button>
|
||||
<button class="add-btn" onclick="downloadMaterialTemplate()">⤓ Download Template</button>
|
||||
<input type="file" id="material-import" accept=".csv,.xlsx,.xls" style="display:none" onchange="importMaterials(event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DRAWINGS / ATTACHMENTS -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Drawings & Attachments</div>
|
||||
<div id="sop-ref-links" class="sop-ref-links"></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>Document / Drawing</th><th style="width:90px">Rev</th><th>Link / Note</th><th style="width:44px"></th></tr></thead><tbody id="attach-body"></tbody></table></div>
|
||||
<button class="add-btn" onclick="addAttach()">+ Add Document</button>
|
||||
<button class="add-btn" onclick="toggleSopFilePanel()">+ Add files from SOP folder</button>
|
||||
<div id="sop-file-panel" style="display:none; margin-top:0.75rem; padding:0.75rem; border:1px dashed var(--border); border-radius:6px; background:var(--bg);">
|
||||
<div id="sop-file-folders"></div>
|
||||
<div class="field-hint" style="margin-top:0.5rem;">2) Paste the file links here, one per line:</div>
|
||||
<textarea id="sop-file-links" rows="4" placeholder="https://primecontrolsdallas.sharepoint.com/:b:/r/.../Drawings/EE-1YA-2P.pdf?csf=1&web=1&e=..." style="width:100%; font-size:12px; padding:0.5rem; border:1px solid var(--border); border-radius:4px; box-sizing:border-box;"></textarea>
|
||||
<div style="margin-top:0.5rem; display:flex; gap:0.5rem;">
|
||||
<button class="add-btn" onclick="addPastedFileLinks()">Add to attachments</button>
|
||||
<button class="add-btn" onclick="toggleSopFilePanel()" style="background:transparent;">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KITTING & MIMO -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Kitting & Material Movement (MIMO)</div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>Kitting Status</label>
|
||||
<select id="wp_kit_status"><option value="">—</option><option>Open</option><option>In Progress</option><option>Kitted</option><option>Delivered</option></select></div>
|
||||
<div class="field"><label>Warehouse Owner</label><input type="text" id="wp_kit_owner" placeholder="name"></div>
|
||||
<div class="field"><label>Kitting Need Date</label><input type="date" id="wp_kit_date"></div>
|
||||
<div class="field"><label>MIMO Sch. Time</label><input type="datetime-local" id="wp_mimo_time"><div class="field-hint">scheduled material-move date & time</div></div>
|
||||
<div class="field"><label>MIMO Location</label><input type="text" id="wp_mimo_loc" placeholder="staging / move location"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONSTRAINTS / RELEASE READINESS -->
|
||||
<div class="card" id="constraint-card">
|
||||
<div class="sub-heading">Constraints — Release Readiness</div>
|
||||
<div class="notice">Per AWP, a package is not released to the field until every constraint is <strong>Cleared</strong> or <strong>N/A</strong>. If a constraint reopens after release, status drops to <strong>Issue (Hold)</strong>.</div>
|
||||
<div class="table-wrap"><table><thead><tr><th>Constraint</th><th style="width:230px">Status</th><th>Comment</th></tr></thead><tbody id="constraint-body"></tbody></table></div>
|
||||
</div>
|
||||
|
||||
<!-- QUALITY & HOLD POINTS -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Quality, Inspection & Hold Points</div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>QC Required</label><input type="text" id="wp_qc" placeholder="from SOP" readonly>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_qc')">🔒 Edit (reason required)</button><span class="override-note"></span></div></div>
|
||||
<div class="field"><label>Photo Documentation</label><input type="text" id="wp_photo" placeholder="from SOP" readonly>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_photo')">🔒 Edit (reason required)</button><span class="override-note"></span></div></div>
|
||||
</div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Witness / Hold Points</label><textarea id="wp_hold" rows="2" readonly placeholder="from SOP"></textarea>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_hold')">🔒 Edit (reason required)</button><span class="override-note"></span></div>
|
||||
<div class="field-hint">Inherited from the SOP. A <strong>Hold Point</strong> stops work until inspection sign-off; a <strong>Witness Point</strong> is offered for inspection but work may proceed if declined.</div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- SIGN-OFFS -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Approvals & Sign-offs</div>
|
||||
<div class="notice">Per the AWP IWP checklist. A package should be signed by these roles before release.</div>
|
||||
<div class="table-wrap"><table><thead><tr><th style="width:220px">Role</th><th>Name</th><th style="width:150px">Date</th><th style="width:80px;text-align:center">Signed</th></tr></thead><tbody id="signoff-body"></tbody></table></div>
|
||||
</div>
|
||||
|
||||
<!-- CLOSEOUT -->
|
||||
<div class="card" id="closeout-card">
|
||||
<div class="sub-heading">Closeout</div>
|
||||
<div class="notice">Completed at QC / Closed — captures as-built reality and lessons learned.</div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>Actual Hrs.</label><input type="number" id="wp_actual_hrs" min="0" step="1"></div>
|
||||
<div class="field"><label>Installed Quantity</label><input type="text" id="wp_installed_qty" placeholder="e.g. 42 of 42 tags"></div>
|
||||
</div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Redlines / As-Built Notes</label><textarea id="wp_redlines" rows="2"></textarea></div></div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Lessons Learned</label><textarea id="wp_lessons" rows="2"></textarea></div></div>
|
||||
</div>
|
||||
|
||||
<div class="nav-row"><button class="btn btn-ghost" onclick="newPackage()">↺ Clear</button>
|
||||
<div style="display:flex;gap:10px">
|
||||
<button class="btn btn-ghost" onclick="savePackage(false)">Save Draft</button>
|
||||
<button class="btn btn-generate" onclick="savePackage(true)">⚡ Save & View</button>
|
||||
</div></div>
|
||||
|
||||
<!-- DASHBOARD -->
|
||||
<div id="dashboard-view" style="display:none">
|
||||
<div class="output-toolbar">
|
||||
<button class="btn btn-ghost" onclick="showForm()">← Back to Form</button>
|
||||
<div style="font-weight:700;font-size:15px">Work Package Dashboard</div>
|
||||
<div style="display:flex;gap:10px;margin-left:auto">
|
||||
<button class="btn btn-ghost" onclick="newPackage()">+ New WP</button>
|
||||
<button class="btn btn-ghost" onclick="renderDashboard()">↻ Refresh</button>
|
||||
<button class="btn btn-ghost" onclick="exportPackages()">⤓ Export (JSON)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctx-bar" style="margin:0 0 14px"><div class="field-hint">Status, metrics and gating across all saved work packages on this device. <span style="color:var(--text-dim)">Reads local data now; wires to the shared SQL database in Phase 2.</span></div></div>
|
||||
<div id="dash-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- OUTPUT -->
|
||||
<div id="pkg-output" style="display:none">
|
||||
<div class="output-toolbar">
|
||||
<button class="btn btn-ghost" onclick="showForm()">← Back to Form</button>
|
||||
<button class="btn btn-primary" onclick="printPackage()">⎙ Print / Save PDF</button>
|
||||
<button class="btn btn-ghost" onclick="exportPackages()">⤓ Export (JSON)</button>
|
||||
</div>
|
||||
<div class="output-doc" id="pkg-doc"></div>
|
||||
</div>
|
||||
|
||||
<!-- SAVED -->
|
||||
<div class="card" id="saved-card" style="display:none">
|
||||
<div class="sub-heading">Saved Work Packages <span id="saved-count"></span></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>WP #</th><th>Type</th><th>Subject</th><th>Status</th><th style="width:110px">Ready?</th><th style="width:120px"></th></tr></thead><tbody id="saved-body"></tbody></table></div>
|
||||
<button class="add-btn" onclick="exportPackages()">⤓ Export All (JSON)</button>
|
||||
<button class="add-btn" onclick="clearSaved()">Clear All</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- HOLD LOG MODAL (comment 7) -->
|
||||
<div class="modal-overlay" id="hold-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-head"><div class="modal-title">Log Hold — On-Hold Constraint</div><button class="cmt-x" onclick="cancelHold()" title="Cancel">✕</button></div>
|
||||
<div class="modal-body">
|
||||
<div class="notice">Moving a package to <strong>Issue (Hold)</strong> requires logging the constraint that blocked it.</div>
|
||||
<div class="field"><label>Constraint type <span class="req">*</span></label><select id="hold-constraint"></select></div>
|
||||
<div class="field"><label>Details <span class="req">*</span></label><textarea id="hold-details" rows="3" placeholder="What reopened / blocked this package?"></textarea></div>
|
||||
<div class="field"><label>Supporting document link</label><input type="text" id="hold-doclink" placeholder="link to RFI, photo, email, etc. (optional)"></div>
|
||||
<div class="field"><label>Supporting photo</label><input type="file" id="hold-photo" accept="image/*" onchange="holdPhotoChange(event)"><div class="hold-photo-preview" id="hold-photo-preview"></div></div>
|
||||
</div>
|
||||
<div class="modal-foot"><button class="btn btn-ghost" onclick="cancelHold()">Cancel</button><button class="btn btn-generate" onclick="submitHold()">Log Hold</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VIEW SOP MODAL (comment 2) -->
|
||||
<div class="modal-overlay" id="sop-modal">
|
||||
<div class="modal" style="max-width:640px">
|
||||
<div class="modal-head"><div class="modal-title">Project SOP — Reference</div><button class="cmt-x" onclick="closeSopModal()" title="Close">✕</button></div>
|
||||
<div class="modal-body" id="sop-modal-body"></div>
|
||||
<div class="modal-foot"><button class="btn btn-primary" onclick="closeSopModal()">Close</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COMMENTS DRAWER -->
|
||||
<div class="cmt-overlay" id="cmt-overlay" onclick="toggleComments()"></div>
|
||||
<aside class="cmt-drawer" id="cmt-drawer" aria-hidden="true">
|
||||
<div class="cmt-head"><div class="cmt-title">Review Comments</div><button class="cmt-x" onclick="toggleComments()" title="Close">✕</button></div>
|
||||
<div class="cmt-namebar"><label>Your name</label><input type="text" id="cmt-author" placeholder="e.g. J. Park" oninput="cmtSaveAuthor(this.value)"></div>
|
||||
<div class="cmt-compose"><div class="cmt-compose-label">Comment on <strong id="cmt-cur-step">this form</strong></div>
|
||||
<textarea id="cmt-input" rows="3" placeholder="Add feedback…"></textarea>
|
||||
<button class="btn btn-primary cmt-add" onclick="addComment()">Add Comment</button></div>
|
||||
<div class="cmt-list" id="cmt-list"></div>
|
||||
<div class="cmt-foot"><div class="cmt-note">Comments are saved in your browser. Use <strong>Export</strong> to send feedback back; the owner can <strong>Import</strong> each file.</div>
|
||||
<div class="cmt-foot-btns"><button class="btn btn-ghost" onclick="exportComments()">⤓ Export</button>
|
||||
<button class="btn btn-ghost" onclick="document.getElementById('cmt-import').click()">⤒ Import</button>
|
||||
<button class="btn btn-ghost cmt-clear" onclick="clearMyComments()">Clear Mine</button>
|
||||
<input type="file" id="cmt-import" accept="application/json" style="display:none" onchange="importComments(event)"></div></div>
|
||||
</aside>
|
||||
|
||||
<script src="feedback-config.js"></script>
|
||||
<script src="wp-creation-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
601
html/wp-creation-styles.css
Normal file
601
html/wp-creation-styles.css
Normal file
@@ -0,0 +1,601 @@
|
||||
/* Fonts are referenced by name only — no external @import, so the app works
|
||||
fully behind a firewall. If IBM Plex is installed/self-hosted it is used;
|
||||
otherwise it falls back to the system UI fonts. */
|
||||
|
||||
/* Embedded-in-Suite tweaks */
|
||||
body.embedded .embed-hide { display: none !important; }
|
||||
body.embedded .embed-first { margin-left: auto; }
|
||||
|
||||
:root {
|
||||
--bg: #f4f5f7;
|
||||
--surface: #ffffff;
|
||||
--surface2: #f7f8fa;
|
||||
--border: #e3e6ec;
|
||||
--border-strong: #d0d5de;
|
||||
--text: #1a2230;
|
||||
--text-muted: #5a6675;
|
||||
--text-dim: #9aa3b2;
|
||||
--accent: #2563d6;
|
||||
--accent-dim: #e8f0fe;
|
||||
--accent-green: #15924f;
|
||||
--accent-green-dim: #e4f6ec;
|
||||
--accent-amber: #b87100;
|
||||
--accent-amber-dim: #fdf2e0;
|
||||
--red: #cf3b3b;
|
||||
--red-dim: #fbeaea;
|
||||
--radius: 5px;
|
||||
--shadow: 0 1px 2px rgba(20,30,50,.04), 0 1px 3px rgba(20,30,50,.06);
|
||||
--shadow-lg: 0 4px 16px rgba(20,30,50,.08);
|
||||
--mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', 'Segoe UI Mono', Consolas, monospace;
|
||||
--sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── HEADER ── */
|
||||
.header {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 14px 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.header-logo {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .15em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.header-sep { color: var(--border-strong); }
|
||||
.header-title { font-size: 13px; font-weight: 500; color: var(--text-muted); }
|
||||
|
||||
/* ── STEPPER ── */
|
||||
.stepper-wrap { padding: 22px 32px 0; max-width: 1000px; margin: 0 auto; }
|
||||
.stepper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.step-tab {
|
||||
flex: 1 0 10%;
|
||||
padding: 9px 6px;
|
||||
text-align: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 9.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: .04em;
|
||||
color: var(--text-dim);
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
line-height: 1.35;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.step-tab:hover { background: var(--surface2); color: var(--text-muted); }
|
||||
.step-tab.active { background: var(--accent-dim); color: var(--accent); border-bottom-color: var(--accent); }
|
||||
.step-tab.done { color: var(--accent-green); background: var(--accent-green-dim); }
|
||||
.step-num { display: block; font-size: 9px; opacity: .65; margin-bottom: 2px; }
|
||||
|
||||
/* ── MAIN ── */
|
||||
.main { max-width: 1000px; margin: 0 auto; padding: 28px 32px 64px; }
|
||||
|
||||
.section { display: none; }
|
||||
.section.active { display: block; animation: fade .25s ease; }
|
||||
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.section-header { margin-bottom: 24px; padding-bottom: 14px; border-bottom: 1px solid var(--border); }
|
||||
.section-label {
|
||||
font-family: var(--mono); font-size: 10px; font-weight: 600;
|
||||
letter-spacing: .2em; color: var(--accent); text-transform: uppercase; margin-bottom: 5px;
|
||||
}
|
||||
.section-title { font-size: 22px; font-weight: 300; color: var(--text); letter-spacing: -.02em; }
|
||||
.section-desc { margin-top: 5px; font-size: 13px; color: var(--text-muted); font-weight: 400; }
|
||||
|
||||
/* ── FIELDS ── */
|
||||
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 18px; }
|
||||
.field-grid.col3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||
.field-grid.col1 { grid-template-columns: 1fr; }
|
||||
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.field.span2 { grid-column: span 2; }
|
||||
|
||||
label {
|
||||
font-family: var(--mono); font-size: 10px; font-weight: 500;
|
||||
letter-spacing: .1em; color: var(--text-muted); text-transform: uppercase;
|
||||
}
|
||||
label .req { color: var(--accent); margin-left: 3px; }
|
||||
|
||||
input[type=text], input[type=date], input[type=number], textarea, select {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-family: var(--sans);
|
||||
font-size: 14px;
|
||||
padding: 9px 12px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
width: 100%;
|
||||
}
|
||||
input:focus, textarea:focus, select:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-dim);
|
||||
}
|
||||
textarea { resize: vertical; min-height: 70px; line-height: 1.5; }
|
||||
::placeholder { color: var(--text-dim); }
|
||||
|
||||
/* ── PILLS ── */
|
||||
.check-group, .radio-group { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
|
||||
.check-pill, .radio-pill {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
padding: 7px 13px; border: 1px solid var(--border-strong); border-radius: 20px;
|
||||
cursor: pointer; transition: all .12s; user-select: none; font-size: 13px; color: var(--text-muted);
|
||||
background: var(--surface);
|
||||
}
|
||||
.check-pill:hover, .radio-pill:hover { border-color: var(--accent); color: var(--text); }
|
||||
.check-pill input, .radio-pill input { display: none; }
|
||||
.check-pill .dot, .radio-pill .dot {
|
||||
width: 7px; height: 7px; border-radius: 50%; background: var(--border-strong);
|
||||
transition: background .12s; flex-shrink: 0;
|
||||
}
|
||||
.check-pill.checked { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
|
||||
.check-pill.checked .dot { background: var(--accent); }
|
||||
.radio-pill.selected { border-color: var(--accent-amber); background: var(--accent-amber-dim); color: var(--accent-amber); }
|
||||
.radio-pill.selected .dot { background: var(--accent-amber); }
|
||||
|
||||
/* ── TABLES ── */
|
||||
.table-wrap { overflow-x: auto; margin-bottom: 22px; border: 1px solid var(--border); border-radius: var(--radius); }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th {
|
||||
background: var(--surface2); border-bottom: 1px solid var(--border); border-right: 1px solid var(--border);
|
||||
padding: 8px 10px; font-family: var(--mono); font-size: 9px; font-weight: 600;
|
||||
letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted); text-align: left; white-space: nowrap;
|
||||
}
|
||||
td { border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); padding: 4px; vertical-align: middle; }
|
||||
th:last-child, td:last-child { border-right: none; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
td.row-label {
|
||||
padding: 8px 10px; font-family: var(--mono); font-size: 11px; color: var(--text-muted);
|
||||
background: var(--surface2); white-space: nowrap; min-width: 150px;
|
||||
}
|
||||
td input[type=text], td select { border: 1px solid transparent; border-radius: 3px; background: transparent; font-size: 12px; padding: 6px 8px; }
|
||||
td input[type=text]:focus, td select:focus { background: var(--surface); border-color: var(--accent); box-shadow: none; }
|
||||
td.center { text-align: center; }
|
||||
|
||||
.toggle-cell { display: flex; justify-content: center; align-items: center; padding: 6px; }
|
||||
.toggle-btn {
|
||||
width: 28px; height: 28px; border-radius: 4px; border: 1px solid var(--border-strong);
|
||||
background: var(--surface); color: var(--text-dim); font-size: 15px; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center; transition: all .12s; font-family: var(--mono); line-height: 1;
|
||||
}
|
||||
.toggle-btn.enabled { border-color: var(--accent-green); background: var(--accent-green-dim); color: var(--accent-green); }
|
||||
.toggle-btn.disabled { border-color: var(--red); background: var(--red-dim); color: var(--red); text-decoration: line-through; font-size: 11px; }
|
||||
|
||||
.ov-select { width: 58px; font-size: 10px !important; padding: 5px 2px !important; text-align: center; font-family: var(--mono) !important; font-weight: 600; }
|
||||
|
||||
/* ── MISC ── */
|
||||
.divider { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
|
||||
.sub-heading {
|
||||
font-family: var(--mono); font-size: 15px; font-weight: 700; letter-spacing: .12em;
|
||||
text-transform: uppercase; color: var(--text-muted); margin-bottom: 12px; display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.sub-heading::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
||||
|
||||
.notice {
|
||||
background: var(--accent-dim); border: 1px solid #b9d2fb; border-radius: var(--radius);
|
||||
padding: 10px 14px; font-size: 12px; color: #1a4fad; margin-bottom: 18px; font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* ── DELIVERABLES ── */
|
||||
.deliv-group { margin-bottom: 20px; }
|
||||
.deliv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.deliv-item {
|
||||
display: flex; align-items: flex-start; gap: 9px; padding: 9px 12px;
|
||||
border: 1px solid var(--border-strong); border-radius: var(--radius); cursor: pointer;
|
||||
transition: all .12s; user-select: none; background: var(--surface);
|
||||
}
|
||||
.deliv-item:hover { border-color: var(--accent); }
|
||||
.deliv-item.checked { border-color: var(--accent); background: var(--accent-dim); }
|
||||
.deliv-item input { display: none; }
|
||||
.deliv-box {
|
||||
width: 16px; height: 16px; border: 1.5px solid var(--border-strong); border-radius: 3px;
|
||||
flex-shrink: 0; margin-top: 2px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; color: #fff; transition: all .12s;
|
||||
}
|
||||
.deliv-item.checked .deliv-box { background: var(--accent); border-color: var(--accent); }
|
||||
.deliv-text { font-size: 12.5px; line-height: 1.35; color: var(--text); }
|
||||
.deliv-text .dt-sub { display: block; font-size: 11px; color: var(--text-muted); margin-top: 1px; }
|
||||
|
||||
/* ── NAV ── */
|
||||
.nav-row { display: flex; justify-content: space-between; align-items: center; padding-top: 24px; margin-top: 24px; border-top: 1px solid var(--border); }
|
||||
.btn {
|
||||
padding: 10px 22px; border-radius: var(--radius); font-family: var(--mono); font-size: 11px; font-weight: 600;
|
||||
letter-spacing: .08em; text-transform: uppercase; cursor: pointer; border: 1px solid; transition: all .15s;
|
||||
}
|
||||
.btn-ghost { background: var(--surface); border-color: var(--border-strong); color: var(--text-muted); }
|
||||
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: var(--shadow); }
|
||||
.btn-primary:hover { background: #1d52b8; }
|
||||
.btn-generate { background: var(--accent-green); border-color: var(--accent-green); color: #fff; font-weight: 700; box-shadow: var(--shadow); }
|
||||
.btn-generate:hover { background: #117a42; }
|
||||
|
||||
/* ── OUTPUT ── */
|
||||
#output-section { display: none; }
|
||||
#output-section.active { display: block; }
|
||||
.output-toolbar { display: flex; gap: 10px; margin-bottom: 18px; align-items: center; flex-wrap: wrap; }
|
||||
.output-doc {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-lg); padding: 52px 56px; font-family: var(--sans); line-height: 1.7;
|
||||
color: var(--text); font-size: 13px; max-height: 74vh; overflow-y: auto;
|
||||
}
|
||||
.output-doc h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; letter-spacing: -.02em; }
|
||||
.output-doc .doc-subtitle { font-size: 11px; color: var(--text-muted); margin-bottom: 28px; font-family: var(--mono); letter-spacing: .08em; }
|
||||
.output-doc h2 {
|
||||
font-size: 13px; font-weight: 600; font-family: var(--mono); letter-spacing: .08em; text-transform: uppercase;
|
||||
color: var(--accent); margin-top: 30px; margin-bottom: 12px; padding-bottom: 6px; border-bottom: 2px solid var(--accent-dim);
|
||||
}
|
||||
.output-doc h3 { font-size: 11px; font-weight: 600; color: var(--text-muted); margin-top: 16px; margin-bottom: 6px; font-family: var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
||||
.output-doc p { margin-bottom: 10px; }
|
||||
.output-doc ul { padding-left: 20px; margin-bottom: 10px; }
|
||||
.output-doc li { margin-bottom: 3px; }
|
||||
.output-doc table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; border: 1px solid var(--border); }
|
||||
.output-doc th { background: var(--surface2); border: 1px solid var(--border); padding: 6px 10px; font-family: var(--mono); font-size: 9px; letter-spacing: .08em; text-transform: uppercase; text-align: left; color: var(--text-muted); }
|
||||
.output-doc td { border: 1px solid var(--border); padding: 7px 10px; vertical-align: top; }
|
||||
.output-doc .badge { display: inline-block; padding: 1px 8px; border-radius: 3px; font-family: var(--mono); font-size: 10px; font-weight: 600; }
|
||||
.badge-R { background: var(--accent-green-dim); color: var(--accent-green); }
|
||||
.badge-O { background: var(--accent-amber-dim); color: var(--accent-amber); }
|
||||
.badge-NA { background: var(--surface2); color: var(--text-dim); }
|
||||
.badge-Y { background: var(--accent-green-dim); color: var(--accent-green); }
|
||||
.badge-N { background: var(--red-dim); color: var(--red); }
|
||||
|
||||
.info-kv { display: flex; gap: 8px; font-size: 12px; margin-bottom: 6px; }
|
||||
.info-kv .k { font-family: var(--mono); font-size: 10px; color: var(--text-muted); white-space: nowrap; min-width: 130px; }
|
||||
.info-kv .v { color: var(--text); }
|
||||
|
||||
/* ── LOADING ── */
|
||||
.loading-overlay {
|
||||
display: none; position: fixed; inset: 0; background: rgba(244,245,247,.82); z-index: 200;
|
||||
align-items: center; justify-content: center; flex-direction: column; gap: 16px; backdrop-filter: blur(2px);
|
||||
}
|
||||
.loading-overlay.active { display: flex; }
|
||||
.spinner { width: 36px; height: 36px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.loading-text { font-family: var(--mono); font-size: 11px; color: var(--text-muted); letter-spacing: .1em; }
|
||||
|
||||
/* ── VALIDATION ── */
|
||||
.step-tab.invalid { color: var(--red); background: var(--red-dim); border-bottom-color: var(--red); }
|
||||
.step-tab.invalid.active { color: var(--red); background: var(--red-dim); border-bottom-color: var(--red); }
|
||||
.step-tab .step-flag { display: none; }
|
||||
.step-tab.invalid .step-flag { display: inline; margin-left: 4px; }
|
||||
input.input-invalid, textarea.input-invalid, select.input-invalid { border-color: var(--red); box-shadow: 0 0 0 3px var(--red-dim); }
|
||||
.radio-group.group-invalid, .check-group.group-invalid { outline: 1px solid var(--red); outline-offset: 4px; border-radius: var(--radius); }
|
||||
.req-hint { font-family: var(--mono); font-size: 10px; color: var(--red); margin-top: 4px; display: none; }
|
||||
.req-hint.show { display: block; }
|
||||
|
||||
/* ── KEY ROLES ── */
|
||||
.roles-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
|
||||
.role-row { display: grid; grid-template-columns: 220px 1fr 36px; gap: 10px; align-items: center; }
|
||||
.role-row .role-other { display: none; }
|
||||
.role-row.is-other { grid-template-columns: 220px 1fr 1fr 36px; }
|
||||
.role-row.is-other .role-other { display: block; }
|
||||
.row-del {
|
||||
width: 34px; height: 34px; border-radius: var(--radius); border: 1px solid var(--border-strong);
|
||||
background: var(--surface); color: var(--text-dim); cursor: pointer; font-size: 16px; line-height: 1;
|
||||
display: flex; align-items: center; justify-content: center; transition: all .12s; flex-shrink: 0;
|
||||
}
|
||||
.row-del:hover { border-color: var(--red); color: var(--red); background: var(--red-dim); }
|
||||
.add-btn {
|
||||
display: inline-flex; align-items: center; gap: 7px; padding: 8px 16px; border-radius: var(--radius);
|
||||
border: 1px dashed var(--border-strong); background: var(--surface); color: var(--text-muted);
|
||||
font-family: var(--mono); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
|
||||
cursor: pointer; transition: all .15s;
|
||||
}
|
||||
.add-btn:hover { border-color: var(--accent); color: var(--accent); border-style: solid; background: var(--accent-dim); }
|
||||
|
||||
/* ── WO TYPE NAME EDITING ── */
|
||||
td.wo-name-cell { padding: 4px; background: var(--surface2); }
|
||||
td.wo-name-cell input { font-family: var(--mono); font-size: 11px; color: var(--text); font-weight: 500; }
|
||||
.wo-custom-tag { font-family: var(--mono); font-size: 8px; color: var(--accent); letter-spacing: .1em; margin-left: 6px; vertical-align: middle; }
|
||||
|
||||
/* ── SEQUENCE BUILDER ── */
|
||||
.seq-builder { margin-bottom: 18px; }
|
||||
.seq-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
|
||||
.seq-step {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 14px;
|
||||
background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius);
|
||||
box-shadow: var(--shadow); transition: border-color .12s, box-shadow .12s, opacity .12s;
|
||||
}
|
||||
.seq-step:hover { border-color: var(--accent); }
|
||||
.seq-step.dragging { opacity: .35; border-style: dashed; }
|
||||
.seq-step.drag-over { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
|
||||
.seq-handle { cursor: grab; color: var(--text-dim); font-size: 16px; line-height: 1; user-select: none; flex-shrink: 0; }
|
||||
.seq-handle:active { cursor: grabbing; }
|
||||
.seq-num {
|
||||
width: 24px; height: 24px; flex-shrink: 0; border-radius: 50%; background: var(--accent-dim); color: var(--accent);
|
||||
font-family: var(--mono); font-size: 11px; font-weight: 600; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.seq-step input.seq-label {
|
||||
border: 1px solid transparent; background: transparent; font-size: 14px; padding: 5px 8px; color: var(--text); flex: 1;
|
||||
}
|
||||
.seq-step input.seq-label:focus { background: var(--surface2); border-color: var(--accent); box-shadow: none; }
|
||||
.seq-arrow { text-align: center; color: var(--text-dim); font-size: 13px; line-height: .4; margin: -2px 0; }
|
||||
.seq-add-row { display: flex; gap: 10px; align-items: center; }
|
||||
.seq-add-row input { flex: 1; }
|
||||
.seq-step.gate { border-color: var(--accent-amber); background: var(--accent-amber-dim); border-style: dashed; }
|
||||
.seq-step.gate .seq-label { color: var(--accent-amber); font-weight: 500; }
|
||||
.seq-gate-badge {
|
||||
flex-shrink: 0; padding: 3px 9px; border-radius: 20px; background: var(--accent-amber); color: #fff;
|
||||
font-family: var(--mono); font-size: 9px; font-weight: 600; letter-spacing: .08em; white-space: nowrap;
|
||||
}
|
||||
.add-btn-gate { border-color: var(--accent-amber); color: var(--accent-amber); }
|
||||
.add-btn-gate:hover { border-color: var(--accent-amber); color: var(--accent-amber); background: var(--accent-amber-dim); border-style: solid; }
|
||||
.field-hint { font-family: var(--mono); font-size: 10px; color: var(--text-dim); margin-top: 5px; line-height: 1.5; }
|
||||
.field-hint code { background: var(--surface2); padding: 1px 6px; border-radius: 3px; color: var(--accent); }
|
||||
.field-hint a { color: var(--accent); text-decoration: none; margin-left: 6px; font-weight: 600; }
|
||||
.field-hint a:hover { text-decoration: underline; }
|
||||
.other-field { margin-top: 6px; }
|
||||
|
||||
/* ── REVIEW COMMENTS ─────────────────────────────────────────────── */
|
||||
.cbadge-total { display:inline-block; min-width:16px; padding:0 5px; margin-left:4px; font-family:var(--mono);
|
||||
font-size:10px; font-weight:700; line-height:16px; text-align:center; color:#fff; background:var(--accent); border-radius:9px; }
|
||||
.step-tab { position:relative; }
|
||||
.step-tab .cbadge { position:absolute; top:4px; right:4px; min-width:15px; height:15px; padding:0 4px;
|
||||
font-family:var(--mono); font-size:9px; font-weight:700; line-height:15px; text-align:center;
|
||||
color:#fff; background:var(--accent-amber); border-radius:8px; box-shadow:0 0 0 2px var(--surface); }
|
||||
|
||||
.cmt-overlay { position:fixed; inset:0; background:rgba(20,30,50,.28); opacity:0; pointer-events:none;
|
||||
transition:opacity .2s ease; z-index:60; }
|
||||
.cmt-overlay.open { opacity:1; pointer-events:auto; }
|
||||
.cmt-drawer { position:fixed; top:0; right:0; height:100vh; width:380px; max-width:92vw; background:var(--surface);
|
||||
border-left:1px solid var(--border); box-shadow:var(--shadow-lg); transform:translateX(100%);
|
||||
transition:transform .24s ease; z-index:61; display:flex; flex-direction:column; }
|
||||
.cmt-drawer.open { transform:translateX(0); }
|
||||
.cmt-head { display:flex; align-items:center; justify-content:space-between; padding:16px 18px;
|
||||
border-bottom:1px solid var(--border); }
|
||||
.cmt-title { font-weight:700; font-size:14px; color:var(--text); }
|
||||
.cmt-x { background:none; border:none; color:var(--text-muted); font-size:15px; cursor:pointer; padding:4px 8px; border-radius:4px; }
|
||||
.cmt-x:hover { background:var(--surface2); color:var(--text); }
|
||||
.cmt-namebar { padding:12px 18px; border-bottom:1px solid var(--border); }
|
||||
.cmt-namebar label { display:block; font-size:10px; text-transform:uppercase; letter-spacing:.04em; color:var(--text-muted); margin-bottom:5px; }
|
||||
.cmt-namebar input { width:100%; padding:8px 10px; border:1px solid var(--border-strong); border-radius:var(--radius); font-family:var(--sans); font-size:13px; color:var(--text); background:var(--surface); }
|
||||
.cmt-compose { padding:14px 18px; border-bottom:1px solid var(--border); background:var(--surface2); }
|
||||
.cmt-compose-label { font-size:11px; color:var(--text-muted); margin-bottom:7px; }
|
||||
.cmt-compose-label strong { color:var(--accent); }
|
||||
.cmt-compose textarea { width:100%; padding:9px 11px; border:1px solid var(--border-strong); border-radius:var(--radius);
|
||||
font-family:var(--sans); font-size:13px; color:var(--text); resize:vertical; background:var(--surface); }
|
||||
.cmt-add { width:100%; margin-top:9px; }
|
||||
.cmt-list { flex:1; overflow-y:auto; padding:10px 14px; }
|
||||
.cmt-empty { text-align:center; color:var(--text-dim); font-size:12px; padding:36px 12px; }
|
||||
.cmt-item { border:1px solid var(--border); border-radius:var(--radius); padding:10px 12px; margin-bottom:9px; background:var(--surface); }
|
||||
.cmt-item.mine { border-color:var(--accent); background:var(--accent-dim); }
|
||||
.cmt-meta { display:flex; align-items:center; gap:7px; margin-bottom:5px; font-size:11px; }
|
||||
.cmt-author { font-weight:700; color:var(--text); }
|
||||
.cmt-step { font-family:var(--mono); font-size:9px; text-transform:uppercase; letter-spacing:.03em;
|
||||
color:var(--accent); background:var(--accent-dim); padding:1px 6px; border-radius:9px; }
|
||||
.cmt-time { color:var(--text-dim); margin-left:auto; font-size:10px; }
|
||||
.cmt-text { font-size:13px; color:var(--text); line-height:1.5; white-space:pre-wrap; word-break:break-word; }
|
||||
.cmt-del { background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:11px; padding:2px 5px; border-radius:4px; }
|
||||
.cmt-del:hover { background:var(--red-dim); color:var(--red); }
|
||||
.cmt-jump { background:none; border:none; color:var(--accent); cursor:pointer; font-size:10px; padding:0; margin-top:4px; }
|
||||
.cmt-jump:hover { text-decoration:underline; }
|
||||
.cmt-foot { border-top:1px solid var(--border); padding:12px 18px; }
|
||||
.cmt-note { font-size:10px; color:var(--text-muted); line-height:1.5; margin-bottom:10px; }
|
||||
.cmt-foot-btns { display:flex; gap:7px; flex-wrap:wrap; }
|
||||
.cmt-foot-btns .btn { padding:6px 11px; font-size:11px; }
|
||||
.cmt-clear { margin-left:auto; color:var(--text-muted); }
|
||||
|
||||
/* ── REV1: validation, governance, summary ───────────────────────── */
|
||||
.radio-group.group-invalid, .check-group.group-invalid { outline:2px solid var(--red); outline-offset:4px; border-radius:var(--radius); }
|
||||
.ov-select.ov-unset { color:var(--red) !important; border-color:var(--red); }
|
||||
.use-btn { display:inline-block; margin-left:8px; padding:4px 14px; font-family:var(--sans); font-size:11px; font-weight:700;
|
||||
color:#fff; background:var(--accent-green); border:none; border-radius:var(--radius); cursor:pointer; letter-spacing:.03em; }
|
||||
.use-btn:hover { background:#0f7a40; }
|
||||
.sum-chips { display:flex; flex-wrap:wrap; gap:7px; }
|
||||
.sum-chip { background:var(--accent-dim); color:var(--accent); border:1px solid #b9d2fb; border-radius:3px;
|
||||
padding:3px 10px; font-family:var(--mono); font-size:10px; }
|
||||
.sum-warn { margin-top:10px; color:var(--accent-amber); background:var(--accent-amber-dim); border:1px solid #f0d9ad;
|
||||
border-radius:var(--radius); padding:7px 10px; font-size:11px; }
|
||||
|
||||
/* ── CREATION TOOL ───────────────────────────────────────────────── */
|
||||
.ctx-bar { max-width:1080px; margin:0 auto; padding:12px 28px; display:flex; align-items:center; gap:20px;
|
||||
border-bottom:1px solid var(--border); background:var(--surface); flex-wrap:wrap; }
|
||||
.ctx-empty { color:var(--text-muted); font-size:13px; }
|
||||
.ctx-main .ctx-proj { font-weight:700; color:var(--text); font-size:14px; }
|
||||
.ctx-main .ctx-sub { font-size:11px; color:var(--text-muted); margin-top:2px; }
|
||||
.ctx-sample { font-family:var(--mono); font-size:9px; font-weight:700; color:var(--accent-amber);
|
||||
background:var(--accent-amber-dim); border:1px solid #f0d9ad; border-radius:9px; padding:1px 7px; margin-left:6px; vertical-align:middle; }
|
||||
.ctx-meta { margin-left:auto; display:flex; gap:16px; font-size:11px; color:var(--text-muted); flex-wrap:wrap; }
|
||||
.ctx-meta b { color:var(--accent); }
|
||||
.ctx-meta code { background:var(--surface2); padding:1px 6px; border-radius:3px; color:var(--accent); }
|
||||
.link-btn { background:none; border:none; color:var(--accent); cursor:pointer; font-size:inherit; padding:0; text-decoration:underline; }
|
||||
|
||||
.mode-wrap { max-width:1080px; margin:0 auto; padding:16px 28px 0; display:flex; align-items:center; gap:16px; }
|
||||
.mode-toggle { display:inline-flex; border:1px solid var(--border-strong); border-radius:6px; overflow:hidden; }
|
||||
.mode-btn { padding:8px 18px; font-family:var(--sans); font-size:13px; font-weight:600; border:none; background:var(--surface);
|
||||
color:var(--text-muted); cursor:pointer; }
|
||||
.mode-btn.active { background:var(--accent); color:#fff; }
|
||||
.created-count { font-size:11px; color:var(--text-muted); }
|
||||
|
||||
.wo-section { border:1px solid var(--border); border-radius:var(--radius); padding:11px 13px; margin-bottom:9px; background:var(--surface); }
|
||||
.wo-section-head { display:flex; align-items:center; gap:9px; margin-bottom:7px; flex-wrap:wrap; }
|
||||
.wo-section-name { font-weight:700; font-size:13px; color:var(--text); }
|
||||
.wo-section-src { font-family:var(--mono); font-size:10px; color:var(--text-muted); margin-left:auto; }
|
||||
.wo-section textarea { width:100%; padding:8px 10px; border:1px solid var(--border-strong); border-radius:var(--radius);
|
||||
font-family:var(--sans); font-size:13px; color:var(--text); resize:vertical; background:var(--surface); }
|
||||
.empty-hint { color:var(--text-dim); font-size:13px; padding:18px 6px; text-align:center; }
|
||||
|
||||
.crew-row { grid-template-columns:1fr 36px !important; }
|
||||
.crew-row.is-other { grid-template-columns:1fr 1fr 70px 36px !important; }
|
||||
.crew-row:not(.is-other) { grid-template-columns:1fr 70px 36px !important; }
|
||||
|
||||
.batch-head { display:flex; align-items:center; gap:16px; margin:14px 0 10px; font-size:13px; flex-wrap:wrap; }
|
||||
#batch-preview table input { width:100%; }
|
||||
|
||||
/* ── WORK PACKAGE FORM ───────────────────────────────────────────── */
|
||||
.sop-hint { color:var(--accent) !important; }
|
||||
.release-banner { max-width:1080px; margin:0 auto; padding:0 28px; }
|
||||
.release-banner .rb-inner { margin-top:14px; border-radius:var(--radius); padding:11px 16px; font-size:13px; font-weight:600;
|
||||
display:flex; align-items:center; gap:10px; }
|
||||
.rb-ready { background:var(--accent-green-dim); color:var(--accent-green); border:1px solid #b6e3c6; }
|
||||
.rb-notready { background:var(--accent-amber-dim); color:var(--accent-amber); border:1px solid #f0d9ad; }
|
||||
.rb-hold { background:var(--red-dim); color:var(--red); border:1px solid #f3c4c4; }
|
||||
.pill-hold.selected { background:var(--red) !important; border-color:var(--red) !important; }
|
||||
.pill-hold.selected .dot { background:#fff !important; }
|
||||
|
||||
.cstatus { display:inline-flex; border:1px solid var(--border-strong); border-radius:5px; overflow:hidden; }
|
||||
.cstatus button { border:none; background:var(--surface); color:var(--text-muted); font-family:var(--sans); font-size:11px;
|
||||
font-weight:600; padding:4px 10px; cursor:pointer; border-right:1px solid var(--border); }
|
||||
.cstatus button:last-child { border-right:none; }
|
||||
.cstatus button.on-open { background:var(--red); color:#fff; }
|
||||
.cstatus button.on-cleared { background:var(--accent-green); color:#fff; }
|
||||
.cstatus button.on-na { background:var(--text-muted); color:#fff; }
|
||||
#material-body input, #attach-body input, #constraint-body input, #signoff-body input { width:100%; }
|
||||
.signoff-check { width:18px; height:18px; cursor:pointer; }
|
||||
|
||||
/* ── COMMENT-DRIVEN ADDITIONS ────────────────────────────────────── */
|
||||
.locked-field { background:var(--surface2)!important; color:var(--text-muted); cursor:not-allowed; }
|
||||
.lock-row { display:flex; align-items:center; gap:10px; margin-top:5px; }
|
||||
.lock-edit { background:none; border:none; color:var(--accent); cursor:pointer; font-size:11px; padding:0; }
|
||||
.override-note { font-size:11px; color:var(--accent-amber); }
|
||||
.sop-tag { font-size:9px; font-weight:700; color:var(--accent); background:var(--accent-dim,#eaf0fd); border:1px solid #cdd9f2; border-radius:9px; padding:1px 6px; margin-left:6px; vertical-align:middle; }
|
||||
|
||||
#toast { position:fixed; bottom:26px; left:50%; transform:translateX(-50%) translateY(20px); background:var(--text); color:#fff;
|
||||
padding:10px 20px; border-radius:8px; font-size:13px; font-weight:600; opacity:0; pointer-events:none; transition:all .25s; z-index:9999; box-shadow:0 6px 24px rgba(0,0,0,.25); }
|
||||
#toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
|
||||
|
||||
.modal-overlay { position:fixed; inset:0; background:rgba(20,28,40,.55); display:none; align-items:center; justify-content:center; z-index:9000; padding:20px; }
|
||||
.modal-overlay.open { display:flex; }
|
||||
.modal { background:var(--surface); border-radius:12px; width:100%; max-width:520px; box-shadow:0 20px 60px rgba(0,0,0,.3); overflow:hidden; max-height:90vh; display:flex; flex-direction:column; }
|
||||
.modal-head { display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid var(--border); }
|
||||
.modal-title { font-weight:700; font-size:15px; color:var(--text); }
|
||||
.modal-body { padding:18px 20px; overflow-y:auto; }
|
||||
.modal-body .field { margin-bottom:14px; }
|
||||
.modal-foot { display:flex; justify-content:flex-end; gap:10px; padding:14px 20px; border-top:1px solid var(--border); }
|
||||
.hold-photo-preview img { max-width:160px; max-height:120px; border-radius:6px; border:1px solid var(--border); margin-top:8px; display:block; }
|
||||
|
||||
/* ── REV 2 ADDITIONS ─────────────────────────────────────────────── */
|
||||
.auto-tag { font-size:9px; font-weight:700; color:var(--accent-green); background:var(--accent-green-dim); border:1px solid #b6e3c6; border-radius:9px; padding:1px 6px; margin-left:6px; vertical-align:middle; }
|
||||
.derived-box { padding:9px 11px; border:1px dashed var(--border-strong); border-radius:var(--radius); background:var(--surface2); color:var(--text); font-size:13px; font-weight:600; min-height:38px; display:flex; align-items:center; }
|
||||
.material-actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:8px; }
|
||||
.workstep-row { display:flex; align-items:flex-start; gap:10px; margin-bottom:8px; }
|
||||
.workstep-row .ws-num { flex:0 0 26px; height:26px; border-radius:50%; background:var(--accent); color:#fff; font-size:12px; font-weight:700; display:flex; align-items:center; justify-content:center; margin-top:5px; }
|
||||
.workstep-row textarea { flex:1; padding:8px 10px; border:1px solid var(--border-strong); border-radius:var(--radius); font-family:var(--sans); font-size:13px; resize:vertical; min-height:38px; }
|
||||
.workstep-row .row-del { flex:0 0 auto; margin-top:6px; }
|
||||
|
||||
/* ── MOBILE (comment 5) ──────────────────────────────────────────── */
|
||||
@media (max-width: 720px) {
|
||||
.header { flex-wrap:wrap; gap:6px; padding:10px 14px; }
|
||||
.header-title { width:100%; order:5; }
|
||||
.header .btn { margin-left:0 !important; padding:6px 10px !important; font-size:12px; }
|
||||
.header .btn:first-of-type { margin-left:auto !important; }
|
||||
.main { padding:14px !important; }
|
||||
.ctx-bar, .release-banner { padding-left:14px; padding-right:14px; }
|
||||
.ctx-meta { margin-left:0; width:100%; }
|
||||
.field-grid { grid-template-columns:1fr !important; }
|
||||
.radio-group { flex-wrap:wrap; }
|
||||
.card { padding:16px 14px !important; }
|
||||
.modal { max-width:100% !important; }
|
||||
.output-toolbar { flex-wrap:wrap; }
|
||||
.material-actions .add-btn { flex:1 1 auto; }
|
||||
table { min-width:520px; } /* keep columns legible; .table-wrap scrolls */
|
||||
.cstatus button { padding:6px 8px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.header-logo { font-size:14px; }
|
||||
.workstep-row textarea { font-size:16px; } /* avoid iOS zoom */
|
||||
input, select, textarea { font-size:16px; } /* avoid iOS zoom on focus */
|
||||
}
|
||||
|
||||
/* ── REV 3 ADDITIONS ─────────────────────────────────────────────── */
|
||||
/* SOP-inherited field highlight (comment 8) */
|
||||
.sop-inherited { background:rgba(37,99,214,0.07) !important; border-color:var(--accent) !important; opacity:0.85; color:var(--text); }
|
||||
.sop-inherited:focus { opacity:1; }
|
||||
|
||||
/* Dev mode (comment 7) */
|
||||
.logo-wrap { position:relative; display:flex; align-items:center; }
|
||||
.dev-toggle { position:absolute; left:2px; bottom:-9px; width:18px; height:7px; padding:0; border:none;
|
||||
background:var(--text-dim); opacity:0.10; border-radius:3px; cursor:pointer; }
|
||||
.dev-toggle:hover { opacity:0.35; }
|
||||
.dev-banner { background:#3a2a00; color:#ffd479; font-size:12.5px; font-weight:700; text-align:center; padding:7px 14px; letter-spacing:.3px; }
|
||||
body.dev-mode .header { box-shadow: inset 0 -3px 0 #ffb000; }
|
||||
|
||||
/* SOP reference links (comments 1,3,4) */
|
||||
.sop-ref-links { margin-bottom:12px; }
|
||||
.ref-links-title { font-size:12px; color:var(--text-muted); margin-bottom:6px; }
|
||||
.ref-links { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.ref-link { display:inline-flex; align-items:center; gap:6px; font-size:12.5px; font-weight:600; color:var(--accent);
|
||||
background:var(--accent-dim,#eef3fd); border:1px solid #cdd9f2; border-radius:8px; padding:6px 11px; text-decoration:none; }
|
||||
.ref-link:hover { background:#e2ecfc; }
|
||||
.ref-link .ref-sys { font-weight:400; color:var(--text-muted); font-size:11px; }
|
||||
.ref-link-sm { font-size:12px; font-weight:600; color:var(--accent); text-decoration:none; }
|
||||
.ref-link-sm:hover { text-decoration:underline; }
|
||||
|
||||
/* Sign-off date override (comment 5) */
|
||||
.so-date { font-size:13px; font-variant-numeric:tabular-nums; }
|
||||
.so-ovr { margin-left:8px; font-size:11px; }
|
||||
|
||||
/* Disciplines + per-discipline scope */
|
||||
.disc-picker { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.disc-pill { display:flex; align-items:center; gap:7px; padding:7px 13px; border:1px solid var(--border-strong);
|
||||
border-radius:20px; font-size:13px; font-weight:600; cursor:pointer; user-select:none; transition:border-color .12s, background .12s, color .12s; }
|
||||
.disc-pill:hover { border-color:var(--accent); color:var(--text); }
|
||||
.disc-pill input { display:none; }
|
||||
.disc-pill .dot { width:7px; height:7px; border-radius:50%; background:var(--border-strong); transition:background .12s; flex-shrink:0; }
|
||||
.disc-pill.selected { border-color:var(--accent); background:var(--accent-dim,#eef3fd); color:var(--accent); }
|
||||
.disc-pill.selected .dot { background:var(--accent); }
|
||||
.disc-scope { border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:6px; padding:12px 14px; margin-bottom:12px; background:var(--bg); }
|
||||
.disc-scope-head { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; flex-wrap:wrap; }
|
||||
.disc-tag { font-weight:700; font-size:13px; color:var(--accent); text-transform:uppercase; letter-spacing:.03em; }
|
||||
.disc-status { font-size:12px; color:var(--text-muted); }
|
||||
.disc-status select { font-size:12px; padding:3px 6px; }
|
||||
|
||||
/* Dashboard */
|
||||
.dash-metrics { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:12px; margin-bottom:16px; }
|
||||
.dash-metric { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:14px 16px; text-align:center; }
|
||||
.dash-metric .dm-val { font-size:26px; font-weight:800; line-height:1; }
|
||||
.dash-metric .dm-label { font-size:11px; color:var(--text-muted); margin-top:6px; text-transform:uppercase; letter-spacing:.03em; }
|
||||
.dash-metric.dm-green .dm-val { color:var(--accent-green); }
|
||||
.dash-metric.dm-red .dm-val { color:var(--red); }
|
||||
.dash-breakdown { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px; }
|
||||
.dash-bd-title { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--text-muted); margin-bottom:6px; }
|
||||
.dash-chip { display:inline-block; font-size:12px; background:var(--surface2); border:1px solid var(--border); border-radius:14px; padding:3px 10px; margin:0 6px 6px 0; }
|
||||
.dash-chip.chip-red { background:var(--red-dim); color:var(--red); border-color:var(--red); }
|
||||
.dash-panel { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:14px 16px; margin-bottom:16px; }
|
||||
.dash-panel-title { font-weight:700; font-size:13px; margin-bottom:10px; }
|
||||
.dash-table { width:100%; border-collapse:collapse; font-size:12.5px; }
|
||||
.dash-table th { text-align:left; background:var(--surface2); border-bottom:1px solid var(--border); padding:6px 8px; font-size:11px; text-transform:uppercase; color:var(--text-muted); }
|
||||
.dash-table td { border-bottom:1px solid var(--border); padding:6px 8px; vertical-align:top; }
|
||||
.dash-filters { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:14px; }
|
||||
.dash-filters input, .dash-filters select { padding:7px 10px; border:1px solid var(--border-strong); border-radius:6px; font-size:13px; }
|
||||
.dash-filters input[type=search] { flex:1; min-width:200px; }
|
||||
@media (max-width:640px){ .dash-breakdown { grid-template-columns:1fr; } }
|
||||
Reference in New Issue
Block a user