Initial commit: WP Suite project files
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
8
Project-SDE-WP-Suite.code-workspace
Normal file
8
Project-SDE-WP-Suite.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
501
index.html
Normal file
501
index.html
Normal file
@@ -0,0 +1,501 @@
|
||||
<!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="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);
|
||||
}
|
||||
|
||||
/* 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">
|
||||
|
||||
<!-- UNIFIED SUITE -->
|
||||
<a href="work-package-suite.html" class="card">
|
||||
<div class="card-badge">Recommended</div>
|
||||
<h3>Unified Suite</h3>
|
||||
<p>Complete end-to-end workflow. Configure project SOP in 10 steps, then immediately create Work Packages with all SOP defaults pre-populated.</p>
|
||||
<button class="card-button">Launch Suite</button>
|
||||
</a>
|
||||
|
||||
<!-- SOP CONFIG TOOL -->
|
||||
<a href="sop-config-tool.html" class="card">
|
||||
<h3>SOP Configuration</h3>
|
||||
<p>Standalone project baseline generator. Define roles, constraints, governance, and platforms. Generates exportable SOP JSON for sharing with team.</p>
|
||||
<button class="card-button">Open Tool</button>
|
||||
</a>
|
||||
|
||||
<!-- WP CREATION TOOL -->
|
||||
<a href="wp-creation-index.html" class="card">
|
||||
<h3>Work Package Creator</h3>
|
||||
<p>Individual Work Package authoring tool. Requires an SOP file. Create, review, and export individual IWPs with constraint checklists.</p>
|
||||
<button class="card-button">Open Tool</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- QUICK START -->
|
||||
<div class="section quick-start">
|
||||
<h2>Getting Started</h2>
|
||||
<ol>
|
||||
<li><strong>Click "Unified Suite"</strong> to begin</li>
|
||||
<li><strong>Complete the 10-step SOP Configuration</strong> for your project (~15 minutes)</li>
|
||||
<li><strong>Switch to "Work Package Creation" tab</strong> to start authoring Work Packages</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="toggleComments()">Close</button>
|
||||
</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>Three Complementary Tools</h3>
|
||||
<p>Unified workflow for end-to-end SOP+WP creation, standalone SOP generator, and individual WP authoring. Choose the approach that fits your workflow.</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>
|
||||
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));
|
||||
|
||||
document.getElementById('comment-text').value = '';
|
||||
document.getElementById('commenter-name').value = '';
|
||||
loadComments();
|
||||
alert('Thank you! Feedback submitted.');
|
||||
}
|
||||
|
||||
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
prime-controls-logo.jpg
Normal file
BIN
prime-controls-logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
587
sop-config-app.js
Normal file
587
sop-config-app.js
Normal file
@@ -0,0 +1,587 @@
|
||||
// ── STATE ────────────────────────────────────────────────────────────────────
|
||||
let currentStep = 1;
|
||||
let allComments = []; // Track all comments for export
|
||||
let state = {
|
||||
project: {name:'', number:'', client:'', division:'', site:''},
|
||||
team: {pm:'', cm:'', qm:''},
|
||||
signoffRoles: [{role:'Superintendent',name:''},{role:'Foreman',name:''}],
|
||||
wpTypes: [],
|
||||
governance: {woformat:'', wosize:'', issuance:[]},
|
||||
quality: {qcreq:'', photo:'', hold:''},
|
||||
platforms: {tracking:'CxAlloy', commissioning:'CxAlloy'},
|
||||
sequence: [],
|
||||
constraints: [],
|
||||
sources: []
|
||||
};
|
||||
|
||||
// ── LIBRARIES ────────────────────────────────────────────────────────────────
|
||||
|
||||
const DISCIPLINES = [
|
||||
'Conduit Installation',
|
||||
'Cable Tray Installation',
|
||||
'Instrument Raceway Installation',
|
||||
'Wire / Cable Pull',
|
||||
'Electrical/Instrument Cable Installation',
|
||||
'Cable Terminations',
|
||||
'Electrical Junction Box Installation',
|
||||
'Instrument Installation',
|
||||
'Control Panel Installation',
|
||||
'Heat Tracing Installation'
|
||||
];
|
||||
|
||||
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 TYPE_DISCIPLINE_MAP = {
|
||||
'Rough-In':'Conduit Installation',
|
||||
'Mechanical Install':'Heat Tracing Installation',
|
||||
'Panel Install':'Control Panel Installation',
|
||||
'Conduit Install':'Conduit Installation',
|
||||
'Tray Install':'Cable Tray Installation',
|
||||
'Mechanical Tubing':'Heat Tracing Installation',
|
||||
'Instrument Install':'Instrument Installation',
|
||||
'Wire Pull':'Wire / Cable Pull',
|
||||
'Terminations':'Cable Terminations',
|
||||
'Prefab/Kitting':'Electrical Junction Box Installation',
|
||||
'Network Cabling':'Cable Tray Installation',
|
||||
'Fiber':'Wire / Cable Pull',
|
||||
'Calibrations':'Instrument Installation'
|
||||
};
|
||||
|
||||
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 = [
|
||||
'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();
|
||||
renderOptionalRoles();
|
||||
renderStandardConstraints();
|
||||
renderSequenceSteps();
|
||||
renderSources();
|
||||
updateStepUI();
|
||||
});
|
||||
|
||||
function initializeWPTypes(){
|
||||
state.wpTypes = JSON.parse(JSON.stringify(DEFAULT_WP_TYPES)).map(t=>({...t,discipline:TYPE_DISCIPLINE_MAP[t.name]||''}));
|
||||
renderWPTypes();
|
||||
}
|
||||
|
||||
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
|
||||
function loadSampleData(){
|
||||
// Populate Step 1
|
||||
document.getElementById('proj_name').value = 'Micron — INC Construction Work Packages';
|
||||
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 = 'Nick Siegfried';
|
||||
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]-[Discipline]-[TYPE]';
|
||||
document.getElementById('gov_wosize').value = '3–5 days / 40–80 hours';
|
||||
|
||||
// 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();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// ── RENDERING ─────────────────────────────────────────────────────────────────
|
||||
function renderWPTypes(){
|
||||
const container = document.getElementById('wp-types-table');
|
||||
container.innerHTML = `<div style="padding:0.5rem; font-size:12px; font-weight:600; display:grid; grid-template-columns:30px 200px 200px; gap:1rem; margin-bottom:1rem; border-bottom:1px solid var(--border);">
|
||||
<div></div><div>Type</div><div>Discipline</div>
|
||||
</div>`;
|
||||
state.wpTypes.forEach((t,i)=>{
|
||||
const row = document.createElement('div');
|
||||
row.className = 'wp-type-row';
|
||||
row.innerHTML = `
|
||||
<input type="checkbox" ${t.enabled?'checked':''} onchange="toggleWPType(${i})">
|
||||
<div>${t.name}</div>
|
||||
<select onchange="state.wpTypes[${i}].discipline=this.value">
|
||||
${DISCIPLINES.map(d=>`<option ${t.discipline===d?'selected':''}>${d}</option>`).join('')}
|
||||
</select>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWPType(i){
|
||||
state.wpTypes[i].enabled = !state.wpTypes[i].enabled;
|
||||
renderWPTypes();
|
||||
}
|
||||
|
||||
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" 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 class="constraint-check">
|
||||
<input type="checkbox" id="const_${c.name}" checked onchange="toggleConstraint('${c.name}')">
|
||||
<div style="flex:1;">
|
||||
<label for="const_${c.name}" style="margin:0; font-weight:600;">${c.name}</label>
|
||||
<div style="font-size:12px; color:var(--text-dim); margin-top:0.25rem;">${c.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
// Initialize constraints with all standard 10
|
||||
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" 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();
|
||||
}
|
||||
|
||||
function renderSequenceSteps(){
|
||||
const container = document.getElementById('sequence-list');
|
||||
if(!state.sequence.length) state.sequence = [{label:'Layout',kind:'step'},{label:'Installation',kind:'step'},{label:'Testing',kind:'step'},{label:'Commissioning',kind:'step'}];
|
||||
container.innerHTML = state.sequence.map((s,i)=>`
|
||||
<div class="sequence-row">
|
||||
<input type="text" value="${s.label}" placeholder="Step name" onchange="state.sequence[${i}].label=this.value">
|
||||
<select onchange="state.sequence[${i}].kind=this.value">
|
||||
<option ${s.kind==='step'?'selected':''}>step</option>
|
||||
<option ${s.kind==='gate'?'selected':''}>gate</option>
|
||||
</select>
|
||||
<button class="row-del" onclick="state.sequence.splice(${i},1); renderSequenceSteps()">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function addSequenceStep(){
|
||||
state.sequence.push({label:'',kind:'step'});
|
||||
renderSequenceSteps();
|
||||
}
|
||||
|
||||
function renderSources(){
|
||||
const container = document.getElementById('sources-list');
|
||||
if(!state.sources.length) state.sources = [
|
||||
{label:'Design Drawings',system:'Procore',notes:'',link:''},
|
||||
{label:'Specifications',system:'Procore',notes:'',link:''},
|
||||
{label:'IO List',system:'controls.dev',notes:'',link:''},
|
||||
{label:'Cable Schedule',system:'SharePoint',notes:'',link:''}
|
||||
];
|
||||
container.innerHTML = state.sources.map((s,i)=>`
|
||||
<div class="source-row">
|
||||
<input type="text" value="${s.label}" placeholder="Label" onchange="state.sources[${i}].label=this.value">
|
||||
<input type="text" value="${s.system}" placeholder="System" onchange="state.sources[${i}].system=this.value">
|
||||
<input type="text" value="${s.link}" placeholder="URL" onchange="state.sources[${i}].link=this.value">
|
||||
<input type="text" value="${s.notes}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value">
|
||||
<button class="row-del" 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();
|
||||
}else{
|
||||
exportSOP();
|
||||
}
|
||||
}
|
||||
|
||||
function previousStep(){
|
||||
if(currentStep > 1){
|
||||
currentStep--;
|
||||
updateStepUI();
|
||||
}
|
||||
}
|
||||
|
||||
function updateStepUI(){
|
||||
// Hide all steps
|
||||
document.querySelectorAll('.step').forEach(s=>s.style.display='none');
|
||||
document.getElementById(`step-${currentStep}`).style.display='block';
|
||||
|
||||
// Update 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('prev-btn').disabled = currentStep === 1;
|
||||
document.getElementById('next-btn').style.display = currentStep < 10 ? 'block' : 'none';
|
||||
document.getElementById('export-btn').style.display = currentStep === 10 ? 'block' : 'none';
|
||||
|
||||
// Collect form data on display
|
||||
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.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);
|
||||
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;
|
||||
const team = state.team;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── EXPORT ────────────────────────────────────────────────────────────────────
|
||||
function exportSOP(){
|
||||
collectStepData();
|
||||
|
||||
const 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,
|
||||
cm: state.team.cm,
|
||||
qm: state.team.qm,
|
||||
site: state.project.site
|
||||
},
|
||||
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
|
||||
},
|
||||
woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({
|
||||
name: t.name,
|
||||
enabled: true,
|
||||
discipline: t.discipline || ''
|
||||
})),
|
||||
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 || ''}))
|
||||
};
|
||||
|
||||
// Validate SOP
|
||||
if(!sop.project.name || !sop.project.number || !sop.governance.woFormat){
|
||||
alert('Cannot export: Missing critical fields (Project Name, Number, or WP Format).');
|
||||
return;
|
||||
}
|
||||
|
||||
// Download as JSON
|
||||
const json = JSON.stringify(sop, null, 2);
|
||||
const blob = new Blob([json], {type: 'application/json'});
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `wp-sop-${sop.project.name.replace(/\s+/g,'-').toLowerCase()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
alert(`✓ SOP exported: ${a.download}`);
|
||||
|
||||
// Also offer to export comments
|
||||
if(allComments.length > 0){
|
||||
setTimeout(()=>{
|
||||
if(confirm('Export feedback comments as well? (CSV file)')){
|
||||
exportComments();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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('sop_config_comments', JSON.stringify(allComments));
|
||||
|
||||
document.getElementById('comment-text').value = '';
|
||||
document.getElementById('commenter-name').value = '';
|
||||
loadStepComments();
|
||||
alert('✓ Comment submitted! Thank you for your feedback.');
|
||||
}
|
||||
|
||||
function loadStepComments(){
|
||||
const saved = localStorage.getItem('sop_config_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('');
|
||||
}
|
||||
}
|
||||
|
||||
function exportComments(){
|
||||
const saved = localStorage.getItem('sop_config_comments');
|
||||
if(!saved || JSON.parse(saved).length === 0){
|
||||
alert('No comments to export.');
|
||||
return;
|
||||
}
|
||||
|
||||
allComments = JSON.parse(saved);
|
||||
|
||||
// Build CSV
|
||||
let csv = 'Step,Name,Timestamp,Feedback\n';
|
||||
allComments.forEach(c=>{
|
||||
const text = `"${c.text.replace(/"/g,'""')}"`;
|
||||
csv += `${c.step},"${c.name}","${c.timestamp}",${text}\n`;
|
||||
});
|
||||
|
||||
const blob = new Blob([csv], {type: 'text/csv'});
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `sop-config-feedback-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
alert(`✓ ${allComments.length} comments exported to CSV.`);
|
||||
}
|
||||
507
sop-config-styles.css
Normal file
507
sop-config-styles.css
Normal file
@@ -0,0 +1,507 @@
|
||||
: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;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, #1e40af 100%);
|
||||
color: white;
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.logo:hover { opacity: 0.9; }
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
margin-bottom: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255,255,255,0.4);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-button:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.header-meta { font-size: 12px; color: var(--text-dim); }
|
||||
.step-counter {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* STEP NAVIGATION */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* SUB-HEADING */
|
||||
.sub-heading {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* REQUIRED 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 TABLE */
|
||||
#wp-types-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.wp-type-row {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 200px 200px 100px;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.wp-type-row input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* SEQUENCE */
|
||||
.sequence-row {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 100px 30px;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.sequence-row .row-del {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.sequence-row .row-del:hover { background: #b91c1c; }
|
||||
|
||||
/* CONSTRAINTS */
|
||||
.constraint-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.constraint-check input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.constraint-check label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* SOURCES */
|
||||
.source-row {
|
||||
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);
|
||||
}
|
||||
.source-row input,
|
||||
.source-row select {
|
||||
padding: 0.5rem;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.source-row .row-del {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.source-row .row-del:hover { background: #b91c1c; }
|
||||
|
||||
/* 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(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.nav-btn.primary:hover {
|
||||
background: #1d4ed8;
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
position: relative;
|
||||
}
|
||||
.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); }
|
||||
|
||||
.constraint-option {
|
||||
padding: 0.75rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.constraint-option:hover {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 768px) {
|
||||
.wp-type-row,
|
||||
.source-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.step-navigation {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* COMMENTS SECTION */
|
||||
.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;
|
||||
}
|
||||
309
sop-config-tool.html
Normal file
309
sop-config-tool.html
Normal file
@@ -0,0 +1,309 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Work Package SOP Configuration Tool</title>
|
||||
<link rel="stylesheet" href="theme-light.css">
|
||||
<link rel="stylesheet" href="sop-config-styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="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;">
|
||||
<div class="logo-text">SOP Configuration</div>
|
||||
</a>
|
||||
<div>
|
||||
<div class="header-title">SOP Configuration Tool</div>
|
||||
<div class="header-subtitle">Project-specific baseline parameters</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="header-button" onclick="loadSampleData()" title="Load example data">⭐ Load Sample</button>
|
||||
<span class="step-counter"><span id="current-step">1</span> / 10</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- STEP CONTENT -->
|
||||
<div class="step-content">
|
||||
|
||||
<!-- STEP 1: PROJECT BASICS -->
|
||||
<div class="step" id="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">
|
||||
</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="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., Nick Siegfried">
|
||||
</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>
|
||||
|
||||
<!-- STEP 3: SIGN-OFF ROLES -->
|
||||
<div class="step" id="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;">
|
||||
<!-- Added dynamically -->
|
||||
</div>
|
||||
<button class="add-btn" onclick="addOptionalRole()">+ Add Role</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 4: WORK PACKAGE TYPES -->
|
||||
<div class="step" id="step-4" style="display: none;">
|
||||
<h2>4. Work Package Types & Disciplines</h2>
|
||||
<div class="notice">Enable the WP types your project will use. Assign a discipline to each for IWP constraint checklists.</div>
|
||||
<div id="wp-types-table" style="margin-top: 1.5rem;">
|
||||
<!-- Built dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 5: GOVERNANCE -->
|
||||
<div class="step" id="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.</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]-[Discipline]-[TYPE]">
|
||||
<small>Use ## for counter, [Sector] [Discipline] [TYPE] as variables</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Typical WP Size</label>
|
||||
<input type="text" id="gov_wosize" placeholder="e.g., 3–5 days or 40–80 hours">
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- STEP 6: QUALITY -->
|
||||
<div class="step" id="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="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>
|
||||
<small>Primary tool for WP status, progress, and reporting</small>
|
||||
</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>
|
||||
<small>Tool for system turnover, testing, and sign-offs</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 8: SEQUENCE -->
|
||||
<div class="step" id="step-8" style="display: none;">
|
||||
<h2>8. Construction Sequence</h2>
|
||||
<div class="notice">Define the major phases and logical sequence of construction work. These become predecessors in WP planning.</div>
|
||||
<div id="sequence-list" style="margin-top: 1rem;">
|
||||
<!-- Built dynamically -->
|
||||
</div>
|
||||
<button class="add-btn" onclick="addSequenceStep()">+ Add Sequence Step</button>
|
||||
</div>
|
||||
|
||||
<!-- STEP 9: CONSTRAINTS -->
|
||||
<div class="step" id="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. Start with the standard 10 (AWP Vol II), or add custom constraints per your project needs.</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;">
|
||||
<!-- Built dynamically -->
|
||||
</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;">
|
||||
<!-- Built dynamically -->
|
||||
</div>
|
||||
<button class="add-btn" onclick="showConstraintLibrary()">+ Add Custom Constraint</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 10: SOURCES & REFERENCES -->
|
||||
<div class="step" id="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 (Procore drawings, IO lists, cable schedules, specifications, etc.).</div>
|
||||
<div id="sources-list" style="margin-top: 1rem;">
|
||||
<!-- Built dynamically -->
|
||||
</div>
|
||||
<button class="add-btn" onclick="addSource()">+ Add Source</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- COMMENTS SECTION -->
|
||||
<div class="comments-section">
|
||||
<button class="comments-toggle" onclick="toggleComments()">💬 Step Comments</button>
|
||||
<div id="comments-panel" style="display: none; margin-top: 1rem; padding: 1rem; background: var(--bg); border-radius: 6px; border: 1px solid var(--border);">
|
||||
<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 on Step {{STEP}}</label>
|
||||
<textarea id="comment-text" rows="3" placeholder="e.g., 'This field should be higher in the form' or 'Good, but missing [X] option'" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem; font-family: inherit;"></textarea>
|
||||
</div>
|
||||
<button onclick="submitComment()" style="background: var(--primary); color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">Submit Comment</button>
|
||||
<button onclick="toggleComments()" style="background: var(--border); color: var(--text); padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; margin-left: 0.5rem;">Close</button>
|
||||
<div id="comments-list" style="margin-top: 1rem; max-height: 200px; overflow-y: auto;">
|
||||
<!-- Comments loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NAVIGATION -->
|
||||
<div class="step-navigation">
|
||||
<button class="nav-btn" id="prev-btn" onclick="previousStep()">← Back</button>
|
||||
<button class="nav-btn" id="next-btn" onclick="nextStep()">Next →</button>
|
||||
<button class="nav-btn primary" id="export-btn" onclick="exportSOP()" style="display: none;">⤓ Export SOP JSON</button>
|
||||
</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;">
|
||||
<!-- Built dynamically -->
|
||||
</div>
|
||||
<button class="nav-btn" onclick="closeConstraintModal()">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="sop-config-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
159
theme-light.css
Normal file
159
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);
|
||||
}
|
||||
569
work-package-suite-app.js
Normal file
569
work-package-suite-app.js
Normal file
@@ -0,0 +1,569 @@
|
||||
// ── GLOBAL STATE ──────────────────────────────────────────────────────────────
|
||||
let currentTool = 'sop';
|
||||
let currentStep = 1;
|
||||
let sopComplete = false;
|
||||
let allComments = [];
|
||||
|
||||
let state = {
|
||||
project: {name:'', number:'', client:'', division:'', site:''},
|
||||
team: {pm:'', cm:'', qm:''},
|
||||
signoffRoles: [{role:'Superintendent',name:''},{role:'Foreman',name:''}],
|
||||
wpTypes: [],
|
||||
governance: {woformat:'', wosize:'', issuance:[]},
|
||||
quality: {qcreq:'', photo:'', hold:''},
|
||||
platforms: {tracking:'CxAlloy', commissioning:'CxAlloy'},
|
||||
sequence: [],
|
||||
constraints: [],
|
||||
sources: []
|
||||
};
|
||||
|
||||
let sop = null; // Generated SOP for WP tool
|
||||
|
||||
// ── LIBRARIES ─────────────────────────────────────────────────────────────────
|
||||
const DISCIPLINES = [
|
||||
'Conduit Installation',
|
||||
'Cable Tray Installation',
|
||||
'Instrument Raceway Installation',
|
||||
'Wire / Cable Pull',
|
||||
'Electrical/Instrument Cable Installation',
|
||||
'Cable Terminations',
|
||||
'Electrical Junction Box Installation',
|
||||
'Instrument Installation',
|
||||
'Control Panel Installation',
|
||||
'Heat Tracing Installation'
|
||||
];
|
||||
|
||||
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 TYPE_DISCIPLINE_MAP = {
|
||||
'Rough-In':'Conduit Installation',
|
||||
'Mechanical Install':'Heat Tracing Installation',
|
||||
'Panel Install':'Control Panel Installation',
|
||||
'Conduit Install':'Conduit Installation',
|
||||
'Tray Install':'Cable Tray Installation',
|
||||
'Mechanical Tubing':'Heat Tracing Installation',
|
||||
'Instrument Install':'Instrument Installation',
|
||||
'Wire Pull':'Wire / Cable Pull',
|
||||
'Terminations':'Cable Terminations',
|
||||
'Prefab/Kitting':'Electrical Junction Box Installation',
|
||||
'Network Cabling':'Cable Tray Installation',
|
||||
'Fiber':'Wire / Cable Pull',
|
||||
'Calibrations':'Instrument Installation'
|
||||
};
|
||||
|
||||
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 = [
|
||||
'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();
|
||||
renderOptionalRoles();
|
||||
renderStandardConstraints();
|
||||
renderSequenceSteps();
|
||||
renderSources();
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
});
|
||||
|
||||
function initializeWPTypes(){
|
||||
state.wpTypes = JSON.parse(JSON.stringify(DEFAULT_WP_TYPES)).map(t=>({...t,discipline:TYPE_DISCIPLINE_MAP[t.name]||''}));
|
||||
renderWPTypes();
|
||||
}
|
||||
|
||||
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
|
||||
function loadSampleData(){
|
||||
// Populate Step 1
|
||||
document.getElementById('proj_name').value = 'Micron — INC Construction Work Packages';
|
||||
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 = 'Nick Siegfried';
|
||||
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]-[Discipline]-[TYPE]';
|
||||
document.getElementById('gov_wosize').value = '3–5 days / 40–80 hours';
|
||||
|
||||
// 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();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// ── 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 = '—';
|
||||
}
|
||||
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
}
|
||||
|
||||
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 style="padding:0.5rem; font-size:12px; font-weight:600; display:grid; grid-template-columns:30px 200px 200px; gap:1rem; margin-bottom:1rem; border-bottom:1px solid var(--border);">
|
||||
<div></div><div>Type</div><div>Discipline</div>
|
||||
</div>`;
|
||||
state.wpTypes.forEach((t,i)=>{
|
||||
const row = document.createElement('div');
|
||||
row.className = 'wp-type-row';
|
||||
row.innerHTML = `
|
||||
<input type="checkbox" ${t.enabled?'checked':''} onchange="toggleWPType(${i})">
|
||||
<div>${t.name}</div>
|
||||
<select onchange="state.wpTypes[${i}].discipline=this.value">
|
||||
${DISCIPLINES.map(d=>`<option ${t.discipline===d?'selected':''}>${d}</option>`).join('')}
|
||||
</select>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWPType(i){
|
||||
state.wpTypes[i].enabled = !state.wpTypes[i].enabled;
|
||||
renderWPTypes();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function renderSequenceSteps(){
|
||||
const container = document.getElementById('sequence-list');
|
||||
if(!state.sequence.length) state.sequence = [{label:'Layout',kind:'step'},{label:'Installation',kind:'step'},{label:'Testing',kind:'step'},{label:'Commissioning',kind:'step'}];
|
||||
container.innerHTML = state.sequence.map((s,i)=>`
|
||||
<div style="display:grid; grid-template-columns:200px 100px 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="${s.label}" placeholder="Step name" onchange="state.sequence[${i}].label=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
<select onchange="state.sequence[${i}].kind=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
<option ${s.kind==='step'?'selected':''}>step</option>
|
||||
<option ${s.kind==='gate'?'selected':''}>gate</option>
|
||||
</select>
|
||||
<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.sequence.splice(${i},1); renderSequenceSteps()">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function addSequenceStep(){
|
||||
state.sequence.push({label:'',kind:'step'});
|
||||
renderSequenceSteps();
|
||||
}
|
||||
|
||||
function renderSources(){
|
||||
const container = document.getElementById('sources-list');
|
||||
if(!state.sources.length) state.sources = [
|
||||
{label:'Design Drawings',system:'Procore',notes:'',link:''},
|
||||
{label:'Specifications',system:'Procore',notes:'',link:''},
|
||||
{label:'IO List',system:'controls.dev',notes:'',link:''},
|
||||
{label:'Cable Schedule',system:'SharePoint',notes:'',link:''}
|
||||
];
|
||||
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="${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="${s.system}" placeholder="System" 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="${s.link}" placeholder="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="${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(){
|
||||
// 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.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);
|
||||
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,
|
||||
cm: state.team.cm,
|
||||
qm: state.team.qm,
|
||||
site: state.project.site
|
||||
},
|
||||
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
|
||||
},
|
||||
woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({
|
||||
name: t.name,
|
||||
enabled: true,
|
||||
discipline: t.discipline || ''
|
||||
})),
|
||||
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();
|
||||
|
||||
alert('✓ SOP Configuration Complete!\n\nSwitch to "Work Package Creation" to start creating Work Packages.');
|
||||
|
||||
// Update WP tool status
|
||||
document.getElementById('sop-status').textContent = '✓ Ready';
|
||||
document.getElementById('sop-progress-text').textContent = `Project: ${sop.project.name} | ${sop.woTypes.length} WP Types | ${sop.constraints.length} Constraints`;
|
||||
}
|
||||
|
||||
// ── 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));
|
||||
|
||||
document.getElementById('comment-text').value = '';
|
||||
document.getElementById('commenter-name').value = '';
|
||||
loadStepComments();
|
||||
alert('✓ Comment submitted!');
|
||||
}
|
||||
|
||||
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('');
|
||||
}
|
||||
}
|
||||
462
work-package-suite-styles.css
Normal file
462
work-package-suite-styles.css
Normal file
@@ -0,0 +1,462 @@
|
||||
: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: linear-gradient(135deg, var(--primary) 0%, #1e40af 100%);
|
||||
color: white;
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.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: white;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.logo:hover { opacity: 0.9; }
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255,255,255,0.4);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-button:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.step-counter {
|
||||
background: rgba(255,255,255,0.2);
|
||||
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: 30px 200px 200px;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* 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; }
|
||||
|
||||
/* 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; }
|
||||
.step-navigation { flex-direction: column; }
|
||||
}
|
||||
322
work-package-suite.html
Normal file
322
work-package-suite.html
Normal file
@@ -0,0 +1,322 @@
|
||||
<!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="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;">
|
||||
<div class="logo-text">Work Package Suite</div>
|
||||
</a>
|
||||
<div class="header-title">Work Package Suite</div>
|
||||
<div class="header-subtitle" id="project-display"></div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="load-sample-btn" class="header-button" onclick="loadSampleData()" title="Load example SOP data">⭐ Load Sample</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., Nick Siegfried">
|
||||
</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>
|
||||
|
||||
<!-- 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 & Disciplines</h2>
|
||||
<div class="notice">Enable the WP types your project will use. Assign a discipline to each for IWP constraint checklists.</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.</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]-[Discipline]-[TYPE]">
|
||||
<small>Use ## for counter, [Sector] [Discipline] [TYPE] as variables</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Typical WP Size</label>
|
||||
<input type="text" id="gov_wosize" placeholder="e.g., 3–5 days or 40–80 hours">
|
||||
</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>
|
||||
</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">Define the major phases and logical sequence of construction work.</div>
|
||||
<div id="sequence-list" style="margin-top: 1rem;"></div>
|
||||
<button class="add-btn" onclick="addSequenceStep()">+ Add Sequence Step</button>
|
||||
</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 COMMENTS -->
|
||||
<div class="comments-section">
|
||||
<button class="comments-toggle" onclick="toggleComments()">💬 Step Comments</button>
|
||||
<div id="comments-panel" style="display: none; margin-top: 1rem; padding: 1rem; background: var(--bg); border-radius: 6px; border: 1px solid var(--border);">
|
||||
<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>
|
||||
<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="toggleComments()" style="background: var(--border); color: var(--text); padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; margin-left: 0.5rem;">Close</button>
|
||||
<div id="comments-list" style="margin-top: 1rem; max-height: 200px; overflow-y: auto;"></div>
|
||||
</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">
|
||||
<div style="padding: 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.</p>
|
||||
<div style="margin-top: 2rem; padding: 2rem; background: var(--bg); border-radius: 8px; border: 2px dashed var(--border);">
|
||||
<h3 style="color: var(--text-dim);">SOP Status: <span id="sop-status">Not started</span></h3>
|
||||
<p id="sop-progress-text" style="color: var(--text-light); margin-top: 0.5rem;"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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="work-package-suite-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
676
wp-creation-app.js
Normal file
676
wp-creation-app.js
Normal file
@@ -0,0 +1,676 @@
|
||||
/* Work Package (IWP) form — the downstream installation work package record.
|
||||
Parameters (numbering, types, QC, sources) are inherited from the project SOP
|
||||
exported by the Configuration tool. Implements the AWP constraint checklist +
|
||||
release gate from IR 272-2 Vol II. */
|
||||
|
||||
// ── SAMPLE SOP (same shape the Configuration tool exports) ───────────────────
|
||||
const SAMPLE_SOP = {
|
||||
meta:{tool:'Work Package Configuration', sample:true},
|
||||
project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'},
|
||||
roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}],
|
||||
governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'3–5 days', woFormat:'WP##-[Sector]-[Discipline]-[TYPE]' },
|
||||
woTypes:[
|
||||
{name:'Conduit Install', enabled:true}, {name:'Tray Install', enabled:true},
|
||||
{name:'Wire Pull', enabled:true}, {name:'Terminations', enabled:true},
|
||||
{name:'Instrument Install', enabled:true}, {name:'Panel Install', enabled:true},
|
||||
],
|
||||
sources:[
|
||||
{label:'Design Drawings',system:'Procore',notes:'',link:'https://us02.procore.com/562949954073428/project/documents/folders/drawings'},
|
||||
{label:'Specifications',system:'Procore',notes:'',link:'https://us02.procore.com/562949954073428/project/documents/folders/specs'},
|
||||
{label:'IO List',system:'controls.dev',notes:'',link:'https://controls.dev/io'},
|
||||
{label:'Cable Schedule',system:'SharePoint',notes:'',link:'https://primecontrols.sharepoint.com/cable-schedule'},
|
||||
],
|
||||
field:{ trackPlatform:'CxAlloy' },
|
||||
quality:{ qcReq:'Yes', qcScope:'Detailed inspection items', photo:'Key checkpoints only', cxTool:'CxAlloy', holdPoints:'HOLD: Prime QAQC to inspect rough-in before cover/cover-up. WITNESS: client QC to observe megger / insulation-resistance test before energization.' },
|
||||
sequence:[{label:'Layout',kind:'step'},{label:'Conduit Install',kind:'step'},{label:'Tray Install',kind:'step'},{label:'Wire Pull',kind:'step'},{label:'Terminations',kind:'step'},{label:'Commissioning',kind:'step'}],
|
||||
costCodes:['5100 - Rough In','5200 - Wire Pull','5300 - Terminations','5400 - Instrumentation'],
|
||||
};
|
||||
|
||||
// AWP / COAA discipline checklists (Vol II, Appendix I)
|
||||
// Limited to Prime's I&C scope of work (subset of the AWP Appendix I discipline checklists)
|
||||
const DISCIPLINES = ['Conduit Installation','Cable Tray Installation','Instrument Raceway Installation','Wire / Cable Pull','Electrical/Instrument Cable Installation','Cable Terminations','Electrical Junction Box Installation','Instrument Installation','Control Panel Installation','Heat Tracing Installation'];
|
||||
// Standard AWP constraint set (Vol I §2.3.2; Vol II IWP checklists)
|
||||
const DEFAULT_CONSTRAINTS = ['Safety & Permitting','Quality Control / Inspection','IFC Drawings & Specs','Schedule','Materials (on site, bagged & tagged)','Prefabrication','Work Access & Laydown','Craft Availability','Construction Equipment & Tools','Scaffolding / Access Equipment'];
|
||||
const SIGNOFF_ROLES = ['Planner','Superintendent','HSE Professional','Quality Representative','Work Foreman'];
|
||||
const STATUS_ORDER = ['Draft','Scheduled','Issued','In Progress','QC','Closed'];
|
||||
const ISSUED_IDX = STATUS_ORDER.indexOf('Issued');
|
||||
|
||||
// Acumatica cost codes (comment 10) — code|description
|
||||
const 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','2290|Programming Subcontract','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','4940|Contract Labor','4960|Electrical Subcontract','4970|Mechanical Subcontract','4980|Security Subcontract','4990|Other/Radio Subcontract','5010|Instrument Material','5020|Network & Computers Material','5030|Software Material','5040|PLC Material','5050|Panel Material','5060|Electrical Material','5070|Mechanical Material','5080|Security Material','5090|Radio Material','6000|Production','7000|Quality','7100|Panel Quality Control','7200|Factory Acceptance Testing','7300|Site Acceptance Testing','8000|Safety','9000|Administration','9100|Warranty','9200|Freight','9300|Travel','9350|Jobsite Costs/Consumable/Other Direct Costs','9400|Contingency','9450|Other','9500|Accrued Incentive Compensation','9600|Sales Tax','9650|Job Cost Labor Burden','9700|Bonding','9800|Non-Billable Compensation'];
|
||||
// Acumatica allowed units of measure (comment 15) — common first
|
||||
const ACU_UNITS = ['EA','EACH','FT','M','METER','HR','DAYS','MINUTE','KG','LITER','CASE','LOT','LS','PK','PACK','PALLET','PIECE','BOTTLE','CAN'];
|
||||
// WP type → AWP discipline (comment 6: discipline derived from type)
|
||||
const TYPE_DISCIPLINE = {'Conduit Install':'Conduit Installation','Tray Install':'Cable Tray Installation','Wire Pull':'Wire / Cable Pull','Terminations':'Cable Terminations','Instrument Install':'Instrument Installation','Panel Install':'Control Panel Installation'};
|
||||
// WP type → short discipline/trade code used in the WP number (comment 2: single source = type)
|
||||
const TYPE_TRADE = {'Conduit Install':'ELEC','Tray Install':'ELEC','Wire Pull':'ELEC','Terminations':'ELEC','Instrument Install':'INST','Panel Install':'ELEC'};
|
||||
|
||||
// Example built work package (comment 4 / "Load Example") — WP02 export
|
||||
const EXAMPLE_PKG = {"number":"WP02-1P-ELEC-Conduit","status":"Issue","subject":"FAB 1P Horn Strobe Conduit","type":"Conduit Install","system":"Chilled Water","location":"FAB / LVL 1 / Sect P","cost":"4060","wbs":"2001","assignees":"David Velazquez (Catapult Solutions Group), Jesus Casiano-Figueroa (Prime Controls)","distribution":"Bill Clarida (Prime Controls), Sean Tolley (Prime Controls), Jefferson Dufriend (Prime Controls)","due":"2026-06-18","spec":"26_05_33_31 - Conduit","desc":"1P Conduit run for horns and strobes","work":"Layout conduit route\nUsing ladders and MEWP install conduit and boxes with proper strapping, supports, and pull points according to 2D drawings and 3D model. Reference package drawings included in attachments.\nWhen complete initiate inspection with Prime QAQC","workSteps":["Layout conduit route","Using ladders and MEWP install conduit and boxes with proper strapping, supports, and pull points according to 2D drawings and 3D model. Reference package drawings included in attachments.","When complete initiate inspection with Prime QAQC"],"numberDims":{"Sector":"1P","Discipline":"ELEC"},"hours":"60","seq":"Layout","materials":[{"qty":"400","unit":"FT","desc":"3/4\" EMT"},{"qty":"300","unit":"FT","desc":"1\" EMT"},{"qty":"50","unit":"FT","desc":"7/8\" strut"},{"qty":"50","unit":"FT","desc":"1-5/8\" strut"},{"qty":"8","unit":"EA","desc":"4x4x4 NEMA 3 Box"},{"qty":"2","unit":"EA","desc":"1\" C-Type Conduit Body"},{"qty":"1","unit":"EA","desc":"1\" T-Type Conduit Body"},{"qty":"2","unit":"EA","desc":"3/4\" C-Type Conduit Body"},{"qty":"2","unit":"EA","desc":"3/4\" T-Type Conduit Body"},{"qty":"3","unit":"EA","desc":"3/4\" EMT LB & Cover"},{"qty":"6","unit":"EA","desc":"Hoffman F44GCPNK Horn Strobe Box"},{"qty":"2","unit":"EA","desc":"EMO 2x4 Box"},{"qty":"4","unit":"EA","desc":"1\" Bond Bushing w/ Lug"},{"qty":"4","unit":"EA","desc":"3/4\" Bond Bushing w/ Lug"},{"qty":"50","unit":"EA","desc":"1/4\"x3\" Toggle Bolt"},{"qty":"100","unit":"EA","desc":"Drywall Anchor"},{"qty":"5","unit":"EA","desc":"1\" to 3/4\" threaded reducer"}],"attachments":[{"doc":"EE-1YA-2P-5_ HPM PANEL CALLOUT 05","rev":"0","link":"https://us02.procore.com/webclients/host/companies/562949953431440/projects/562949954073428/tools/document-viewer/prostore/562950644886790"}],"kitStatus":"Open","kitOwner":"Ian Spielburg","kitDate":"2026-06-15","mimoTime":"2026-06-11T10:30","mimoLoc":"04","constraints":[{"name":"Safety & Permitting","status":"cleared","comment":""},{"name":"Quality Control / Inspection","status":"cleared","comment":""},{"name":"IFC Drawings & Specs","status":"cleared","comment":""},{"name":"Schedule","status":"cleared","comment":""},{"name":"Materials (on site, bagged & tagged)","status":"cleared","comment":""},{"name":"Prefabrication","status":"cleared","comment":""},{"name":"Work Access & Laydown","status":"cleared","comment":""},{"name":"Craft Availability","status":"cleared","comment":""},{"name":"Construction Equipment & Tools","status":"open","comment":"Boom lift not yet on site"},{"name":"Scaffolding / Access Equipment","status":"cleared","comment":""}],"qc":"Yes — Detailed inspection items","photo":"Key checkpoints only","hold":"HOLD: Prime QAQC to inspect rough-in before cover/cover-up. WITNESS: client QC to observe megger / insulation-resistance test before energization.","overrides":{"wp_photo":"Change Order"},"signoffs":[{"role":"Planner","name":"L. Graver","date":"2026-06-10","signed":true,"dateReason":""},{"role":"Superintendent","name":"K. Boyd","date":"2026-06-10","signed":true,"dateReason":""},{"role":"HSE Professional","name":"A. Reyes","date":"","signed":false,"dateReason":""},{"role":"Quality Representative","name":"D. Nguyen","date":"","signed":false,"dateReason":""},{"role":"Work Foreman","name":"M. Torres","date":"","signed":false,"dateReason":""}],"holds":[],"actualHrs":"54","installedQty":"200 ft","redlines":"Conduit size changed. need to update model","lessons":"Prepping trapeze hangers with conduit straps saved time","project":"Micron — INC Construction Work Packages","track":"CxAlloy"};
|
||||
|
||||
// ── STATE ────────────────────────────────────────────────────────────────────
|
||||
let SOP=null, editingId=null, numberDirty=false;
|
||||
let pkgMaterials=[], pkgAttach=[], pkgConstraints=[], pkgSignoffs=[], pkgHolds=[], pkgWorkSteps=[];
|
||||
let pkgOverrides={}; // {fieldId: reason} for SOP-locked fields that were edited
|
||||
let numberDims={}; // {Sector:'', Discipline:''} dimensions that build the WP number (comment 3)
|
||||
let prevStatus='Draft';
|
||||
let devMode=false; // comment 7: pauses usage tracking during review
|
||||
let savedPackages=[];
|
||||
let currentView='Work Package Form';
|
||||
|
||||
// ── HELPERS ──────────────────────────────────────────────────────────────────
|
||||
function gv(id){ return document.getElementById(id)?.value?.trim() || ''; }
|
||||
function esc(v){ if(v==null) return ''; return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function ns(){ return '<span style="color:var(--text-dim)">—</span>'; }
|
||||
function cell(v){ return v ? esc(v) : ns(); }
|
||||
function pad2(n){ return n<10?'0'+n:''+n; }
|
||||
function typeCode(name){ return (name||'').replace(/[^a-z0-9]+/gi,''); }
|
||||
function linkify(v){ if(!v) return ''; return esc(v).replace(/(https?:\/\/[^\s]+)/g,u=>`<a href="${u}" target="_blank" rel="noopener">${u}</a>`); }
|
||||
function toast(msg){ let t=document.getElementById('toast'); if(!t){ t=document.createElement('div'); t.id='toast'; document.body.appendChild(t); } t.textContent=msg; t.classList.add('show'); clearTimeout(toast._t); toast._t=setTimeout(()=>t.classList.remove('show'),2200); }
|
||||
function getRadio(name){ const s=document.querySelector(`.radio-pill.selected input[name="${name}"]`); return s?.closest('.radio-pill')?.dataset?.val||''; }
|
||||
function setRadio(name,val){ document.querySelectorAll(`.radio-pill input[name="${name}"]`).forEach(i=>{const p=i.closest('.radio-pill'); const on=p.dataset.val===val; p.classList.toggle('selected',on); i.checked=on;}); }
|
||||
function enabledTypes(){ return ((SOP&&SOP.woTypes)||[]).filter(t=>t.enabled!==false); }
|
||||
function constraintNames(){ return (SOP&&Array.isArray(SOP.constraints)&&SOP.constraints.length)?SOP.constraints:DEFAULT_CONSTRAINTS; }
|
||||
function nextSeq(){ return savedPackages.length+1; }
|
||||
|
||||
// ── SOP LOADING ──────────────────────────────────────────────────────────────
|
||||
function loadSampleSOP(){ SOP=JSON.parse(JSON.stringify(SAMPLE_SOP)); applySOP(); newPackage(); toast('Sample SOP loaded — ' + (SOP.project&&SOP.project.name||'project')); track('sample_loaded'); }
|
||||
function importSOP(ev){
|
||||
const f=ev.target.files&&ev.target.files[0]; if(!f) return; const r=new FileReader();
|
||||
r.onload=()=>{ try{ const d=JSON.parse(r.result); if(!d.woTypes){ alert('That file does not look like an SOP export from the Configuration tool.'); return; }
|
||||
SOP=d; applySOP(); newPackage(); toast('Loaded SOP — '+((d.project&&d.project.name)||'project')); track('sop_imported');
|
||||
}catch(e){ alert('Could not read that file.'); } ev.target.value=''; };
|
||||
r.readAsText(f);
|
||||
}
|
||||
function applySOP(){
|
||||
renderCtxBar(); buildTypePicker(); buildCostCodes(); buildSequencePicker(); buildNumberDims();
|
||||
renderSopRefLinks(); renderSpecFolderLink();
|
||||
// SOP-inherited Quality fields — populated then locked (editable only with a logged reason)
|
||||
if(SOP.quality){
|
||||
document.getElementById('wp_qc').value=[SOP.quality.qcReq,SOP.quality.qcScope].filter(Boolean).join(' — ');
|
||||
document.getElementById('wp_photo').value=SOP.quality.photo||'';
|
||||
document.getElementById('wp_hold').value=SOP.quality.holdPoints||'';
|
||||
}
|
||||
lockQuality('wp_qc'); lockQuality('wp_photo'); lockQuality('wp_hold');
|
||||
const hint=document.getElementById('wp_number_hint'); hint.textContent = SOP.governance&&SOP.governance.woFormat ? 'auto-built · format: '+SOP.governance.woFormat : '';
|
||||
buildConstraints(); buildSignoffs();
|
||||
if(!pkgMaterials.length){ pkgMaterials=[{qty:'',unit:'',desc:''}]; buildMaterials(); }
|
||||
if(!pkgAttach.length){ pkgAttach=[{doc:'',rev:'',link:''}]; buildAttach(); }
|
||||
if(!pkgWorkSteps.length){ pkgWorkSteps=['']; buildWorkSteps(); }
|
||||
updateNumber(); updateReleaseBanner();
|
||||
}
|
||||
function buildCostCodes(){
|
||||
const sel=document.getElementById('wp_cost'); const cur=sel.value;
|
||||
sel.innerHTML=`<option value="">Select cost code…</option>`+COST_CODES.map(c=>{const [code,desc]=c.split('|'); return `<option value="${code}">${code} — ${esc(desc)}</option>`;}).join('');
|
||||
if(cur) sel.value=cur;
|
||||
}
|
||||
// ── SOP-LOCKED QUALITY FIELDS (comment 3) ────────────────────────────────────
|
||||
function sopValueFor(id){
|
||||
if(!SOP||!SOP.quality) return '';
|
||||
if(id==='wp_qc') return [SOP.quality.qcReq,SOP.quality.qcScope].filter(Boolean).join(' — ');
|
||||
if(id==='wp_photo') return SOP.quality.photo||'';
|
||||
if(id==='wp_hold') return SOP.quality.holdPoints||'';
|
||||
return '';
|
||||
}
|
||||
function lockQuality(id){
|
||||
const el=document.getElementById(id); if(!el) return;
|
||||
const fromSOP = !!(SOP&&SOP.quality) && !pkgOverrides[id];
|
||||
el.readOnly = fromSOP;
|
||||
el.classList.toggle('sop-inherited', fromSOP); // comment 8: blue-tinted SOP field
|
||||
el.classList.toggle('locked-field', false);
|
||||
const wrap=el.closest('.field'); const btn=wrap&&wrap.querySelector('.lock-edit'); const note=wrap&&wrap.querySelector('.override-note');
|
||||
if(btn) btn.textContent = fromSOP ? '🔒 Edit (reason required)' : '↺ Revert to SOP';
|
||||
if(note) note.innerHTML = pkgOverrides[id] ? `Overridden: ${esc(pkgOverrides[id])}` : '';
|
||||
}
|
||||
function editQuality(id){
|
||||
if(pkgOverrides[id]){ // currently overridden -> revert to SOP value
|
||||
delete pkgOverrides[id];
|
||||
document.getElementById(id).value=sopValueFor(id);
|
||||
lockQuality(id); track('quality_reverted',{field:id}); return;
|
||||
}
|
||||
const reason=prompt('This field is set by the project SOP. Enter the reason for overriding it on this package:');
|
||||
if(reason===null) return;
|
||||
if(!reason.trim()){ alert('A reason is required to override an SOP field.'); return; }
|
||||
pkgOverrides[id]=reason.trim();
|
||||
const el=document.getElementById(id); el.readOnly=false; el.classList.remove('locked-field'); el.focus();
|
||||
lockQuality(id); track('quality_overridden',{field:id});
|
||||
}
|
||||
function renderCtxBar(){
|
||||
const bar=document.getElementById('ctx-bar');
|
||||
if(!SOP){ bar.innerHTML=`<div class="ctx-empty">No SOP loaded — <button class="link-btn" onclick="loadSampleSOP()">load the sample</button> or import one from the Configuration tool.</div>`; return; }
|
||||
const p=SOP.project||{}, g=SOP.governance||{};
|
||||
const sample=SOP.meta&&SOP.meta.sample?`<span class="ctx-sample">SAMPLE</span>`:'';
|
||||
bar.innerHTML=`<div class="ctx-main"><div class="ctx-proj">${esc(p.name||'Untitled')} ${sample}</div>
|
||||
<div class="ctx-sub">${esc(p.number||'')}${p.division?' · '+esc(p.division):''}</div></div>
|
||||
<div class="ctx-meta"><span><b>${enabledTypes().length}</b> types</span><span>format <code>${esc(g.woFormat||'—')}</code></span><span>track: <b>${esc((SOP.field&&SOP.field.trackPlatform)||'—')}</b></span></div>`;
|
||||
}
|
||||
|
||||
// ── DEV MODE (comment 7) ─────────────────────────────────────────────────────
|
||||
function toggleDevMode(){
|
||||
devMode=!devMode;
|
||||
document.body.classList.toggle('dev-mode', devMode);
|
||||
document.getElementById('dev-banner').style.display = devMode ? '' : 'none';
|
||||
toast(devMode ? 'DEV MODE on — usage tracking paused' : 'DEV MODE off — tracking resumed');
|
||||
}
|
||||
|
||||
// ── SOP REFERENCE LINKS (comments 1, 3, 4) ───────────────────────────────────
|
||||
function sopLinkedSources(){ return ((SOP&&SOP.sources)||[]).filter(s=>s.link); }
|
||||
function renderSopRefLinks(){
|
||||
const box=document.getElementById('sop-ref-links'); if(!box) return;
|
||||
const srcs=sopLinkedSources();
|
||||
if(!srcs.length){ box.innerHTML=''; return; }
|
||||
box.innerHTML=`<div class="ref-links-title">Reference folders (from SOP) — navigate to find & copy the specific file link:</div>`+
|
||||
`<div class="ref-links">`+srcs.map(s=>`<a href="${esc(s.link)}" target="_blank" rel="noopener" class="ref-link">📁 ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`;
|
||||
}
|
||||
function renderSpecFolderLink(){
|
||||
const el=document.getElementById('spec-folder-link'); if(!el) return;
|
||||
const spec=sopLinkedSources().find(s=>/spec/i.test(s.label));
|
||||
el.innerHTML = spec ? `<a href="${esc(spec.link)}" target="_blank" rel="noopener" class="ref-link-sm">↗ Open spec folder (${esc(spec.system||'SOP')})</a>` : '';
|
||||
}
|
||||
function buildTypePicker(){ document.getElementById('wp_type').innerHTML=`<option value="">Select…</option>`+enabledTypes().map(t=>`<option>${esc(t.name)}</option>`).join(''); }
|
||||
function buildSequencePicker(){ const steps=((SOP&&SOP.sequence)||[]).filter(s=>s.kind!=='gate'&&(s.label||'').trim()); document.getElementById('wp_seq').innerHTML=`<option value="">None (no predecessor)</option>`+steps.map(s=>`<option>${esc(s.label)}</option>`).join(''); }
|
||||
function derivedDiscipline(){ const t=gv('wp_type'); return TYPE_DISCIPLINE[t]||''; }
|
||||
function onTypeChange(){
|
||||
const d=derivedDiscipline(); const el=document.getElementById('wp_discipline_derived');
|
||||
if(el) el.textContent = d ? d : '—';
|
||||
updateNumber(); track('type_selected');
|
||||
}
|
||||
|
||||
// ── WP NUMBER (auto-built from per-WP dimensions + type + sequence) ───────────
|
||||
function numberTokens(){
|
||||
const fmt=(SOP&&SOP.governance&&SOP.governance.woFormat)||'WP##-[TYPE]';
|
||||
return (fmt.match(/\[[^\]]+\]/g)||[]).map(t=>t.replace(/[\[\]]/g,'')).filter(t=>t.toUpperCase()!=='TYPE');
|
||||
}
|
||||
function buildNumberDims(){
|
||||
const wrap=document.getElementById('number-dims'); if(!wrap) return;
|
||||
const toks=numberTokens();
|
||||
wrap.innerHTML = toks.length ? toks.map(t=>{
|
||||
if(/^discipline$/i.test(t)){
|
||||
return `<div class="field"><label>${esc(t)} code <span class="auto-tag">auto</span></label>
|
||||
<div class="derived-box" id="ndim_disc_box">${esc(numberDims[t]||'—')}</div>
|
||||
<div class="field-hint">trade code from WP type — same discipline used for tracking</div></div>`;
|
||||
}
|
||||
return `<div class="field"><label>${esc(t)}</label>
|
||||
<input type="text" id="ndim_${esc(t)}" value="${(numberDims[t]||'').replace(/"/g,'"')}" placeholder="${esc(t)} code" oninput="numberDims['${esc(t)}']=this.value;updateNumber()"></div>`;
|
||||
}).join('') : '<div class="field-hint">SOP number format has no scope tokens.</div>';
|
||||
}
|
||||
function typeNumberCode(name){ return (name||'').replace(/\b(install|installation)\b/ig,'').replace(/[^a-z0-9]+/gi,'').trim(); }
|
||||
function updateNumber(){
|
||||
const fmt=(SOP&&SOP.governance&&SOP.governance.woFormat)||'WP##-[TYPE]';
|
||||
const t=gv('wp_type');
|
||||
let out=fmt.replace(/WO##|WP##/i,'WP'+pad2(editingSeq()));
|
||||
numberTokens().forEach(tok=>{
|
||||
let v;
|
||||
if(/^discipline$/i.test(tok)){ v=TYPE_TRADE[t]||''; numberDims[tok]=v; const b=document.getElementById('ndim_disc_box'); if(b) b.textContent=v||'—'; }
|
||||
else v=(numberDims[tok]||'').trim();
|
||||
out=out.split('['+tok+']').join(v||('['+tok+']'));
|
||||
});
|
||||
out=out.split('[TYPE]').join(t?typeNumberCode(t):'[TYPE]');
|
||||
document.getElementById('wp_number').value=out;
|
||||
}
|
||||
function editingSeq(){ // keep an existing package's sequence number if editing, else next
|
||||
if(editingId){ const ix=savedPackages.findIndex(p=>p.id===editingId); if(ix>=0) return ix+1; }
|
||||
return nextSeq();
|
||||
}
|
||||
|
||||
// ── MATERIAL LIST ────────────────────────────────────────────────────────────
|
||||
function unitOptions(cur){
|
||||
const list=ACU_UNITS.slice(); const c=(cur||'').toUpperCase();
|
||||
if(c && !list.includes(c)) list.unshift(c);
|
||||
return `<option value="">Unit…</option>`+list.map(u=>`<option ${c===u?'selected':''}>${esc(u)}</option>`).join('');
|
||||
}
|
||||
function buildMaterials(){
|
||||
const tb=document.getElementById('material-body'); tb.innerHTML='';
|
||||
pkgMaterials.forEach((m,i)=>{ const tr=document.createElement('tr');
|
||||
tr.innerHTML=`<td><input type="text" value="${(m.qty||'').replace(/"/g,'"')}" placeholder="20" oninput="pkgMaterials[${i}].qty=this.value"></td>
|
||||
<td><select onchange="pkgMaterials[${i}].unit=this.value">${unitOptions(m.unit)}</select></td>
|
||||
<td><input type="text" value="${(m.desc||'').replace(/"/g,'"')}" placeholder="3/8 lock washers gold" oninput="pkgMaterials[${i}].desc=this.value"></td>
|
||||
<td class="center"><button class="row-del" onclick="removeMaterial(${i})">✕</button></td>`;
|
||||
tb.appendChild(tr); });
|
||||
}
|
||||
function addMaterial(){ pkgMaterials.push({qty:'',unit:'',desc:''}); buildMaterials(); }
|
||||
function removeMaterial(i){ pkgMaterials.splice(i,1); if(!pkgMaterials.length)pkgMaterials=[{qty:'',unit:'',desc:''}]; buildMaterials(); }
|
||||
function downloadMaterialTemplate(){
|
||||
const csv='Qty,Unit,Description\n10,FT,Single deep strut gold\n4,EA,2-hole strut base gold\n200,FT,3/4 EMT conduit\n';
|
||||
const blob=new Blob([csv],{type:'text/csv'}); const a=document.createElement('a');
|
||||
a.href=URL.createObjectURL(blob); a.download='material-list-template.csv'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); track('material_template');
|
||||
}
|
||||
function parseCSV(text){
|
||||
const rows=[]; text.replace(/\r\n/g,'\n').split('\n').forEach(line=>{
|
||||
if(!line.trim()) return;
|
||||
const cells=[]; let cur='', q=false;
|
||||
for(let i=0;i<line.length;i++){ const ch=line[i];
|
||||
if(q){ if(ch==='"'){ if(line[i+1]==='"'){cur+='"';i++;} else q=false; } else cur+=ch; }
|
||||
else { if(ch===','){cells.push(cur);cur='';} else if(ch==='"'){q=true;} else cur+=ch; } }
|
||||
cells.push(cur); rows.push(cells);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
function importMaterials(ev){
|
||||
const f=ev.target.files&&ev.target.files[0]; if(!f) return;
|
||||
if(/\.xlsx?$/i.test(f.name)){ alert('Please save the Excel file as CSV first (File → Save As → CSV), then import. The template download is already CSV.'); ev.target.value=''; return; }
|
||||
const r=new FileReader(); r.onload=()=>{
|
||||
try{
|
||||
const rows=parseCSV(r.result); if(!rows.length){ alert('No rows found.'); return; }
|
||||
let start=0; const h=rows[0].map(c=>c.trim().toLowerCase());
|
||||
const qi=h.indexOf('qty'), ui=h.indexOf('unit'), di=h.indexOf('description');
|
||||
let cq=0,cu=1,cd=2;
|
||||
if(qi>-1||ui>-1||di>-1){ start=1; if(qi>-1)cq=qi; if(ui>-1)cu=ui; if(di>-1)cd=di; }
|
||||
const imported=[];
|
||||
for(let i=start;i<rows.length;i++){ const c=rows[i]; const desc=(c[cd]||'').trim();
|
||||
if(!desc && !(c[cq]||'').trim()) continue;
|
||||
imported.push({qty:(c[cq]||'').trim(), unit:(c[cu]||'').trim().toUpperCase(), desc}); }
|
||||
if(!imported.length){ alert('No material rows found.'); return; }
|
||||
pkgMaterials=imported; buildMaterials(); toast('Imported '+imported.length+' material rows'); track('material_imported',{count:imported.length});
|
||||
}catch(e){ alert('Could not parse that CSV.'); }
|
||||
ev.target.value='';
|
||||
};
|
||||
r.readAsText(f);
|
||||
}
|
||||
|
||||
// ── DESCRIPTION OF WORK — STEP LIST (comment 16) ─────────────────────────────
|
||||
function buildWorkSteps(){
|
||||
const tb=document.getElementById('worksteps-body'); if(!tb) return; tb.innerHTML='';
|
||||
pkgWorkSteps.forEach((s,i)=>{ const row=document.createElement('div'); row.className='workstep-row';
|
||||
row.innerHTML=`<span class="ws-num">${i+1}</span>
|
||||
<textarea rows="1" placeholder="Describe step ${i+1}…" oninput="pkgWorkSteps[${i}]=this.value">${esc(s)}</textarea>
|
||||
<button class="row-del" onclick="removeWorkStep(${i})" title="Remove step">✕</button>`;
|
||||
tb.appendChild(row); });
|
||||
}
|
||||
function addWorkStep(){ pkgWorkSteps.push(''); buildWorkSteps(); }
|
||||
function removeWorkStep(i){ pkgWorkSteps.splice(i,1); if(!pkgWorkSteps.length)pkgWorkSteps=['']; buildWorkSteps(); }
|
||||
|
||||
// ── ATTACHMENTS ──────────────────────────────────────────────────────────────
|
||||
function buildAttach(){
|
||||
const tb=document.getElementById('attach-body'); tb.innerHTML='';
|
||||
pkgAttach.forEach((a,i)=>{ const tr=document.createElement('tr');
|
||||
tr.innerHTML=`<td><input type="text" value="${(a.doc||'').replace(/"/g,'"')}" placeholder="drawing / spec name" oninput="pkgAttach[${i}].doc=this.value"></td>
|
||||
<td><input type="text" value="${(a.rev||'').replace(/"/g,'"')}" placeholder="0" oninput="pkgAttach[${i}].rev=this.value"></td>
|
||||
<td><input type="text" value="${(a.link||'').replace(/"/g,'"')}" placeholder="link or note" oninput="pkgAttach[${i}].link=this.value"></td>
|
||||
<td class="center"><button class="row-del" onclick="removeAttach(${i})">✕</button></td>`;
|
||||
tb.appendChild(tr); });
|
||||
}
|
||||
function addAttach(){ pkgAttach.push({doc:'',rev:'',link:''}); buildAttach(); }
|
||||
function removeAttach(i){ pkgAttach.splice(i,1); if(!pkgAttach.length)pkgAttach=[{doc:'',rev:'',link:''}]; buildAttach(); }
|
||||
|
||||
// ── CONSTRAINTS + RELEASE GATE ───────────────────────────────────────────────
|
||||
function buildConstraints(){
|
||||
const names=constraintNames();
|
||||
// preserve existing statuses if rebuilding
|
||||
const prev={}; pkgConstraints.forEach(c=>prev[c.name]=c);
|
||||
pkgConstraints=names.map(n=> prev[n] || {name:n, status:'open', comment:''});
|
||||
const tb=document.getElementById('constraint-body'); tb.innerHTML='';
|
||||
pkgConstraints.forEach((c,i)=>{ const tr=document.createElement('tr');
|
||||
tr.innerHTML=`<td class="row-label">${esc(c.name)}</td>
|
||||
<td><span class="cstatus">
|
||||
<button class="${c.status==='open'?'on-open':''}" onclick="setConstraint(${i},'open')">Open</button>
|
||||
<button class="${c.status==='cleared'?'on-cleared':''}" onclick="setConstraint(${i},'cleared')">Cleared</button>
|
||||
<button class="${c.status==='na'?'on-na':''}" onclick="setConstraint(${i},'na')">N/A</button>
|
||||
</span></td>
|
||||
<td><input type="text" value="${(c.comment||'').replace(/"/g,'"')}" placeholder="note" oninput="pkgConstraints[${i}].comment=this.value"></td>`;
|
||||
tb.appendChild(tr); });
|
||||
}
|
||||
function setConstraint(i,val){
|
||||
const before=pkgConstraints[i].status;
|
||||
pkgConstraints[i].status=val; buildConstraints(); updateReleaseBanner();
|
||||
// If a constraint is flagged Open after the package was released, require logging the issue
|
||||
if(val==='open' && STATUS_ORDER.indexOf(getRadio('status'))>=ISSUED_IDX){
|
||||
prevStatus=getRadio('status'); holdContext={index:i, before};
|
||||
openHoldModal(pkgConstraints[i].name, true);
|
||||
}
|
||||
}
|
||||
function readiness(){ const open=pkgConstraints.filter(c=>c.status==='open').length; return {open, total:pkgConstraints.length, cleared:pkgConstraints.filter(c=>c.status==='cleared').length, ready:open===0}; }
|
||||
function updateReleaseBanner(){
|
||||
const b=document.getElementById('release-banner'); const r=readiness(); const st=getRadio('status');
|
||||
let cls, txt;
|
||||
if(st==='Issue'){ cls='rb-hold'; txt=`⛔ On hold — ${r.open} constraint${r.open===1?'':'s'} reopened. Resolve to resume.`; }
|
||||
else if(r.ready){ cls='rb-ready'; txt=`✓ Release-ready — all ${r.total} constraints cleared or N/A.`; }
|
||||
else { cls='rb-notready'; txt=`⚠ Not release-ready — ${r.open} of ${r.total} constraint${r.open===1?'':'s'} still open.`; }
|
||||
b.innerHTML=`<div class="rb-inner ${cls}">${txt}</div>`;
|
||||
}
|
||||
function onStatusChange(target){
|
||||
const idx=STATUS_ORDER.indexOf(target);
|
||||
if(idx>=ISSUED_IDX && readiness().open>0){
|
||||
const open=pkgConstraints.filter(c=>c.status==='open').map(c=>c.name);
|
||||
alert('Cannot move to "'+target+'" — these constraints are still open:\n\n• '+open.join('\n• ')+'\n\nClear or mark N/A first.');
|
||||
setRadio('status', prevStatus||'Scheduled'); updateReleaseBanner(); return;
|
||||
}
|
||||
if(target==='Issue'){ openHoldModal(); return; } // modal commits or reverts
|
||||
prevStatus=target; updateReleaseBanner(); track('status_change',{status:target});
|
||||
}
|
||||
|
||||
// ── HOLD LOG MODAL (comments 7) ──────────────────────────────────────────────
|
||||
let holdPhotoData='', holdContext=null;
|
||||
function openHoldModal(preselect, fromConstraint){
|
||||
holdPhotoData=''; if(!fromConstraint) holdContext=null;
|
||||
const sel=document.getElementById('hold-constraint');
|
||||
sel.innerHTML = pkgConstraints.map(c=>`<option ${c.name===preselect?'selected':''}>${esc(c.name)}</option>`).join('') + '<option>Other</option>';
|
||||
const existing = fromConstraint ? (pkgConstraints.find(c=>c.name===preselect)||{}).comment||'' : '';
|
||||
document.getElementById('hold-details').value=existing;
|
||||
document.getElementById('hold-doclink').value='';
|
||||
document.getElementById('hold-photo-preview').innerHTML='';
|
||||
document.getElementById('hold-modal').classList.add('open');
|
||||
}
|
||||
function holdPhotoChange(ev){
|
||||
const f=ev.target.files&&ev.target.files[0]; if(!f) return;
|
||||
const r=new FileReader(); r.onload=()=>{ holdPhotoData=r.result; document.getElementById('hold-photo-preview').innerHTML=`<img src="${holdPhotoData}" alt="supporting photo">`; }; r.readAsDataURL(f);
|
||||
}
|
||||
function submitHold(){
|
||||
const constraint=document.getElementById('hold-constraint').value;
|
||||
const details=document.getElementById('hold-details').value.trim();
|
||||
const doclink=document.getElementById('hold-doclink').value.trim();
|
||||
if(!details){ alert('A comment defining the issue is required.'); return; }
|
||||
pkgHolds.push({ ts:new Date().toISOString(), constraint, details, doc:doclink, photo:holdPhotoData||'' });
|
||||
const c=pkgConstraints.find(x=>x.name===constraint); if(c){ c.status='open'; c.comment=details; }
|
||||
buildConstraints(); setRadio('status','Issue'); prevStatus='Issue'; holdContext=null;
|
||||
document.getElementById('hold-modal').classList.remove('open');
|
||||
updateReleaseBanner(); toast('Hold logged'); track('hold_logged',{constraint});
|
||||
}
|
||||
function cancelHold(){
|
||||
if(holdContext){ // came from flagging a constraint on a released package -> revert that constraint
|
||||
pkgConstraints[holdContext.index].status=holdContext.before; holdContext=null; buildConstraints(); updateReleaseBanner();
|
||||
document.getElementById('hold-modal').classList.remove('open'); return;
|
||||
}
|
||||
closeHoldModal(false);
|
||||
}
|
||||
function closeHoldModal(committed){
|
||||
document.getElementById('hold-modal').classList.remove('open');
|
||||
if(!committed){ setRadio('status', prevStatus||'Scheduled'); updateReleaseBanner(); }
|
||||
}
|
||||
|
||||
// ── SIGN-OFFS ────────────────────────────────────────────────────────────────
|
||||
function sopNameForSignoff(role){
|
||||
if(!SOP) return '';
|
||||
const roles=SOP.roles||[]; const proj=SOP.project||{};
|
||||
const find=(re)=>{ const m=roles.find(r=>re.test(r.role||'')); return m&&m.name||''; };
|
||||
switch(role){
|
||||
case 'Planner': return find(/planner/i);
|
||||
case 'Superintendent': return find(/superintendent/i) || proj.cm || '';
|
||||
case 'HSE Professional': return find(/safety|hse|ehs/i);
|
||||
case 'Quality Representative': return find(/quality/i) || proj.qm || '';
|
||||
case 'Work Foreman': return find(/general foreman|foreman/i);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function buildSignoffs(){
|
||||
const prev={}; pkgSignoffs.forEach(s=>prev[s.role]=s);
|
||||
pkgSignoffs=SIGNOFF_ROLES.map(r=>{
|
||||
if(prev[r]) return prev[r];
|
||||
return {role:r, name:sopNameForSignoff(r), date:'', signed:false, fromSOP:!!sopNameForSignoff(r)};
|
||||
});
|
||||
const tb=document.getElementById('signoff-body'); tb.innerHTML='';
|
||||
pkgSignoffs.forEach((s,i)=>{ const tr=document.createElement('tr');
|
||||
tr.innerHTML=`<td class="row-label">${esc(s.role)}</td>
|
||||
<td><input type="text" value="${(s.name||'').replace(/"/g,'"')}" placeholder="name" oninput="pkgSignoffs[${i}].name=this.value">${s.fromSOP?'<span class="sop-tag">from SOP</span>':''}</td>
|
||||
<td><span class="so-date">${s.date?esc(s.date):'—'}</span> <button class="lock-edit so-ovr" onclick="onSignoffDateOverride(${i})" title="Manual date override (reason required)">✎ override</button>${s.dateReason?`<span class="override-note" title="${esc(s.dateReason)}">overridden</span>`:''}</td>
|
||||
<td class="center"><input type="checkbox" class="signoff-check" ${s.signed?'checked':''} onchange="onSignoffSigned(${i},this.checked)"></td>`;
|
||||
tb.appendChild(tr); });
|
||||
}
|
||||
function todayStr(){ const d=new Date(); return d.getFullYear()+'-'+pad2(d.getMonth()+1)+'-'+pad2(d.getDate()); }
|
||||
function onSignoffSigned(i,checked){
|
||||
pkgSignoffs[i].signed=checked;
|
||||
if(checked && !pkgSignoffs[i].date){ pkgSignoffs[i].date=todayStr(); pkgSignoffs[i].dateReason=''; }
|
||||
buildSignoffs(); track('signoff',{role:pkgSignoffs[i].role, signed:checked});
|
||||
}
|
||||
function onSignoffDateOverride(i){
|
||||
const s=pkgSignoffs[i];
|
||||
const nd=prompt('Manual sign-off date for '+s.role+' (YYYY-MM-DD). The date is normally set automatically when Signed is checked.', s.date||todayStr());
|
||||
if(nd===null) return;
|
||||
if(!/^\d{4}-\d{2}-\d{2}$/.test(nd.trim())){ alert('Enter the date as YYYY-MM-DD.'); return; }
|
||||
const reason=prompt('Reason for manually overriding the '+s.role+' sign-off date:');
|
||||
if(reason===null || !reason.trim()){ alert('A reason is required for a manual date override.'); return; }
|
||||
s.date=nd.trim(); s.dateReason=reason.trim(); buildSignoffs(); track('signoff_date_override',{role:s.role});
|
||||
}
|
||||
|
||||
// ── SAVE / OUTPUT ────────────────────────────────────────────────────────────
|
||||
function collectPackage(){
|
||||
const steps=pkgWorkSteps.map(s=>s.trim()).filter(Boolean);
|
||||
return {
|
||||
id: editingId || ('wp_'+Date.now().toString(36)+Math.random().toString(36).slice(2,5)),
|
||||
number:gv('wp_number'), status:getRadio('status')||'Draft', subject:gv('wp_subject'),
|
||||
type:gv('wp_type'), discipline:derivedDiscipline(), system:gv('wp_system'), location:gv('wp_location'),
|
||||
cost:gv('wp_cost'), wbs:gv('wp_wbs'), assignees:gv('wp_assignees'), distribution:gv('wp_distribution'),
|
||||
due:gv('wp_due'), spec:gv('wp_spec'), desc:gv('wp_desc'),
|
||||
work:steps.join('\n'), workSteps:steps, numberDims:{...numberDims},
|
||||
hours:gv('wp_hours'), seq:gv('wp_seq'),
|
||||
materials:pkgMaterials.filter(m=>m.qty||m.desc), attachments:pkgAttach.filter(a=>a.doc),
|
||||
kitStatus:gv('wp_kit_status'), kitOwner:gv('wp_kit_owner'), kitDate:gv('wp_kit_date'),
|
||||
mimoTime:gv('wp_mimo_time'), mimoLoc:gv('wp_mimo_loc'),
|
||||
constraints:pkgConstraints.map(c=>({name:c.name,status:c.status,comment:c.comment})),
|
||||
qc:gv('wp_qc'), photo:gv('wp_photo'), hold:gv('wp_hold'),
|
||||
qcFromSOP: !pkgOverrides['wp_qc'], photoFromSOP: !pkgOverrides['wp_photo'], holdFromSOP: !pkgOverrides['wp_hold'], overrides:{...pkgOverrides},
|
||||
signoffs:pkgSignoffs.map(s=>({role:s.role,name:s.name,date:s.date,signed:s.signed,dateReason:s.dateReason||''})),
|
||||
holds:pkgHolds.map(h=>({...h})),
|
||||
actualHrs:gv('wp_actual_hrs'), installedQty:gv('wp_installed_qty'), redlines:gv('wp_redlines'), lessons:gv('wp_lessons'),
|
||||
project:(SOP&&SOP.project&&SOP.project.name)||'', track:(SOP&&SOP.field&&SOP.field.trackPlatform)||'',
|
||||
updatedAt:new Date().toISOString()
|
||||
};
|
||||
}
|
||||
function savePackage(view){
|
||||
if(!gv('wp_subject') || !gv('wp_type')){ alert('Subject and WP Type are required.'); return; }
|
||||
const pkg=collectPackage();
|
||||
const ix=savedPackages.findIndex(p=>p.id===pkg.id);
|
||||
if(ix>=0) savedPackages[ix]=pkg; else savedPackages.push(pkg);
|
||||
editingId=pkg.id; saveStore(); renderSavedList(); track('package_saved',{status:pkg.status});
|
||||
document.getElementById('loadingOverlay').classList.add('active');
|
||||
setTimeout(()=>{ document.getElementById('loadingOverlay').classList.remove('active'); if(view) renderPackage(pkg); }, 400);
|
||||
}
|
||||
function renderPackage(pkg){
|
||||
const r = pkg.constraints ? {open:pkg.constraints.filter(c=>c.status==='open').length,total:pkg.constraints.length} : {open:0,total:0};
|
||||
const readyTxt = pkg.status==='Issue' ? 'ON HOLD' : (r.open===0?'RELEASE-READY':(r.open+' OPEN CONSTRAINTS'));
|
||||
let h=`<h1>${esc(pkg.number||'(no number)')} — Work Package</h1>
|
||||
<div class="doc-subtitle">${esc(pkg.project)} · TYPE: ${esc(pkg.type).toUpperCase()} · STATUS: ${esc(pkg.status).toUpperCase()} · ${readyTxt}</div>`;
|
||||
const costDesc = (COST_CODES.find(c=>c.split('|')[0]===pkg.cost)||'').split('|')[1];
|
||||
h+=`<h2>1.0 General Information</h2><table><tbody>
|
||||
<tr><th style="width:200px">WP Number</th><td>${cell(pkg.number)}</td></tr>
|
||||
<tr><th>Subject</th><td>${cell(pkg.subject)}</td></tr>
|
||||
<tr><th>Type / Discipline</th><td>${cell(pkg.type)}${pkg.discipline?' · '+esc(pkg.discipline):''}</td></tr>
|
||||
<tr><th>System / Facility Code / UPN</th><td>${cell(pkg.system)}</td></tr>
|
||||
<tr><th>Location</th><td>${cell(pkg.location)}</td></tr>
|
||||
<tr><th>Cost Code</th><td>${pkg.cost?esc(pkg.cost)+(costDesc?' — '+esc(costDesc):''):ns()}</td></tr>
|
||||
<tr><th>Acumatica Task</th><td>${cell(pkg.wbs)}</td></tr>
|
||||
<tr><th>Assignees</th><td>${cell(pkg.assignees)}</td></tr>
|
||||
<tr><th>Distribution</th><td>${cell(pkg.distribution)}</td></tr>
|
||||
<tr><th>Due Date</th><td>${cell(pkg.due)}</td></tr>
|
||||
<tr><th>Specification Section</th><td>${cell(pkg.spec)}</td></tr>
|
||||
<tr><th>Description</th><td>${cell(pkg.desc)}</td></tr>
|
||||
</tbody></table>`;
|
||||
const stepsArr = pkg.workSteps && pkg.workSteps.length ? pkg.workSteps : (pkg.work?String(pkg.work).split('\n').filter(Boolean):[]);
|
||||
const stepsHtml = stepsArr.length ? '<ol style="margin:0;padding-left:18px">'+stepsArr.map(s=>`<li>${esc(s)}</li>`).join('')+'</ol>' : ns();
|
||||
h+=`<h2>2.0 Scope & Work</h2><table><tbody>
|
||||
<tr><th style="width:200px">Description of Work</th><td>${stepsHtml}</td></tr>
|
||||
<tr><th>Labor Est. Hrs.</th><td>${cell(pkg.hours)}</td></tr>
|
||||
<tr><th>Package Predecessor</th><td>${pkg.seq?('After: '+esc(pkg.seq)):'None (no predecessor)'}</td></tr>
|
||||
</tbody></table>`;
|
||||
if(pkg.materials&&pkg.materials.length){ h+=`<h2>3.0 Material List</h2><table><thead><tr><th style="width:80px">Qty</th><th style="width:80px">Unit</th><th>Description</th></tr></thead><tbody>`;
|
||||
pkg.materials.forEach(m=>h+=`<tr><td>${cell(m.qty)}</td><td>${cell(m.unit)}</td><td>${cell(m.desc)}</td></tr>`); h+=`</tbody></table>`; }
|
||||
if(pkg.attachments&&pkg.attachments.length){ h+=`<h2>4.0 Drawings & Attachments</h2><table><thead><tr><th>Document</th><th style="width:60px">Rev</th><th>Link / Note</th></tr></thead><tbody>`;
|
||||
pkg.attachments.forEach(a=>h+=`<tr><td>${cell(a.doc)}</td><td>${cell(a.rev)}</td><td>${a.link?linkify(a.link):ns()}</td></tr>`); h+=`</tbody></table>`; }
|
||||
h+=`<h2>5.0 Kitting & MIMO</h2><table><tbody>
|
||||
<tr><th style="width:200px">Kitting Status</th><td>${cell(pkg.kitStatus)}</td></tr>
|
||||
<tr><th>Warehouse Owner</th><td>${cell(pkg.kitOwner)}</td></tr>
|
||||
<tr><th>Kitting Need Date</th><td>${cell(pkg.kitDate)}</td></tr>
|
||||
<tr><th>MIMO Sch. Time / Location</th><td>${cell(pkg.mimoTime)} ${pkg.mimoLoc?'· '+esc(pkg.mimoLoc):''}</td></tr>
|
||||
</tbody></table>`;
|
||||
h+=`<h2>6.0 Constraints — Release Readiness</h2><table><thead><tr><th>Constraint</th><th style="width:90px">Status</th><th>Comment</th></tr></thead><tbody>`;
|
||||
(pkg.constraints||[]).forEach(c=>{ const lbl=c.status==='cleared'?'Cleared':c.status==='na'?'N/A':'Open'; const col=c.status==='cleared'?'var(--accent-green)':c.status==='na'?'var(--text-dim)':'var(--red)';
|
||||
h+=`<tr><td>${esc(c.name)}</td><td style="color:${col};font-weight:700">${lbl}</td><td>${cell(c.comment)}</td></tr>`; });
|
||||
h+=`</tbody></table>`;
|
||||
h+=`<h2>7.0 Quality & Hold Points</h2><table><tbody>
|
||||
<tr><th style="width:200px">QC</th><td>${cell(pkg.qc)}${pkg.overrides&&pkg.overrides.wp_qc?` <span style="color:var(--accent-amber)">(overridden: ${esc(pkg.overrides.wp_qc)})</span>`:(pkg.qcFromSOP?' <span style="color:var(--accent);font-size:10px">[from SOP]</span>':'')}</td></tr>
|
||||
<tr><th>Photo Documentation</th><td>${cell(pkg.photo)}${pkg.overrides&&pkg.overrides.wp_photo?` <span style="color:var(--accent-amber)">(overridden: ${esc(pkg.overrides.wp_photo)})</span>`:(pkg.photoFromSOP?' <span style="color:var(--accent);font-size:10px">[from SOP]</span>':'')}</td></tr>
|
||||
<tr><th>Witness / Hold Points</th><td>${cell(pkg.hold)}</td></tr>
|
||||
</tbody></table>`;
|
||||
if(pkg.holds&&pkg.holds.length){
|
||||
h+=`<h2>7.5 Hold Log</h2><table><thead><tr><th style="width:150px">Logged</th><th style="width:200px">Constraint</th><th>Details</th><th style="width:120px">Support</th></tr></thead><tbody>`;
|
||||
pkg.holds.forEach(hd=>{ const when=hd.ts?new Date(hd.ts).toLocaleString():''; const sup=[hd.doc?linkify(hd.doc):'', hd.photo?'<span style="color:var(--accent-green)">photo attached</span>':''].filter(Boolean).join('<br>')||ns();
|
||||
h+=`<tr><td>${esc(when)}</td><td>${cell(hd.constraint)}</td><td>${cell(hd.details)}</td><td>${sup}</td></tr>`; });
|
||||
h+=`</tbody></table>`;
|
||||
}
|
||||
h+=`<h2>8.0 Approvals & Sign-offs</h2><table><thead><tr><th>Role</th><th>Name</th><th style="width:120px">Date</th><th style="width:80px">Signed</th></tr></thead><tbody>`;
|
||||
(pkg.signoffs||[]).forEach(s=>h+=`<tr><td>${esc(s.role)}</td><td>${cell(s.name)}</td><td>${cell(s.date)}</td><td>${s.signed?'✓':'—'}</td></tr>`);
|
||||
h+=`</tbody></table>`;
|
||||
if(pkg.actualHrs||pkg.installedQty||pkg.redlines||pkg.lessons){ h+=`<h2>9.0 Closeout</h2><table><tbody>
|
||||
<tr><th style="width:200px">Actual Hrs.</th><td>${cell(pkg.actualHrs)}</td></tr>
|
||||
<tr><th>Installed Quantity</th><td>${cell(pkg.installedQty)}</td></tr>
|
||||
<tr><th>Redlines / As-Built</th><td>${cell(pkg.redlines)}</td></tr>
|
||||
<tr><th>Lessons Learned</th><td>${cell(pkg.lessons)}</td></tr>
|
||||
</tbody></table>`; }
|
||||
h+=`<p style="margin-top:18px;color:var(--text-dim);font-size:11px">Pushed to: ${cell(pkg.track)}</p>`;
|
||||
document.getElementById('pkg-doc').innerHTML=h; showOutput();
|
||||
}
|
||||
function printPackage(){
|
||||
const c=document.getElementById('pkg-doc').innerHTML; const w=window.open('','_blank');
|
||||
w.document.write(`<!DOCTYPE html><html><head><title>Work Package</title><style>body{font-family:Arial,sans-serif;font-size:12px;line-height:1.6;color:#1a2230;padding:40px;max-width:820px;margin:0 auto}h1{font-size:20px;margin-bottom:4px}h2{font-size:13px;font-weight:700;text-transform:uppercase;border-bottom:2px solid #cdd9f2;padding-bottom:4px;margin-top:22px;color:#2563d6}table{width:100%;border-collapse:collapse;margin:10px 0;font-size:11px}th{background:#f0f2f5;border:1px solid #ccc;padding:5px 8px;text-align:left}td{border:1px solid #ccc;padding:5px 8px}@page{margin:18mm}</style></head><body>${c}</body></html>`);
|
||||
w.document.close(); w.print();
|
||||
}
|
||||
|
||||
// ── VIEWS ────────────────────────────────────────────────────────────────────
|
||||
function showOutput(){ document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='none'); document.getElementById('pkg-output').style.display=''; renderSavedList(); currentView='Package View'; window.scrollTo({top:0,behavior:'smooth'}); }
|
||||
function showForm(){ document.querySelectorAll('.main > .card').forEach(e=>e.style.display=''); document.querySelector('.main > .nav-row').style.display='flex'; document.getElementById('pkg-output').style.display='none'; document.getElementById('saved-card').style.display = savedPackages.length?'':'none'; currentView='Work Package Form'; window.scrollTo({top:0,behavior:'smooth'}); }
|
||||
|
||||
// ── SAVED PACKAGES ───────────────────────────────────────────────────────────
|
||||
const STORE_KEY='wp_iwp_v1';
|
||||
function saveStore(){ try{ localStorage.setItem(STORE_KEY, JSON.stringify(savedPackages)); }catch(e){} }
|
||||
function loadStore(){ try{ const d=JSON.parse(localStorage.getItem(STORE_KEY)); if(Array.isArray(d)) savedPackages=d; }catch(e){} }
|
||||
function renderSavedList(){
|
||||
const card=document.getElementById('saved-card'), body=document.getElementById('saved-body');
|
||||
document.getElementById('saved-count').textContent=savedPackages.length?`(${savedPackages.length})`:'';
|
||||
if(!savedPackages.length){ card.style.display='none'; return; }
|
||||
if(document.getElementById('pkg-output').style.display==='none' || document.getElementById('pkg-output').style.display==='') card.style.display='';
|
||||
body.innerHTML=savedPackages.map((p,i)=>{ const open=(p.constraints||[]).filter(c=>c.status==='open').length;
|
||||
const ready = p.status==='Issue' ? '<span class="badge badge-N">On Hold</span>' : (open===0?'<span class="badge badge-Y">Ready</span>':`<span class="badge badge-O">${open} open</span>`);
|
||||
return `<tr><td class="row-label">${esc(p.number||'—')}</td><td>${esc(p.type||'')}</td><td>${esc(p.subject||'')}</td>
|
||||
<td>${esc(p.status||'')}</td><td>${ready}</td>
|
||||
<td class="center"><button class="link-btn" onclick="editPackage(${i})">edit</button> <button class="link-btn" onclick="viewPackage(${i})">view</button> <button class="row-del" onclick="deletePackage(${i})">✕</button></td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
function viewPackage(i){ if(savedPackages[i]) renderPackage(savedPackages[i]); }
|
||||
function deletePackage(i){ const p=savedPackages[i]; if(!p) return; if(!confirm('Delete work package "'+(p.number||p.subject||'untitled')+'"? This cannot be undone.')) return; savedPackages.splice(i,1); saveStore(); renderSavedList(); track('package_deleted'); }
|
||||
function clearSaved(){ if(!savedPackages.length) return; if(!confirm('Delete all '+savedPackages.length+' saved packages on this device?')) return; savedPackages=[]; saveStore(); renderSavedList(); }
|
||||
function editPackage(i){ const p=savedPackages[i]; if(!p) return; editingId=p.id; loadPackageIntoForm(p); }
|
||||
function loadExample(){ loadPackageIntoForm(JSON.parse(JSON.stringify(EXAMPLE_PKG))); editingId=null; toast('Example work package loaded'); track('example_loaded'); }
|
||||
function loadPackageIntoForm(p){
|
||||
const set=(id,v)=>{const el=document.getElementById(id); if(el) el.value=v||'';};
|
||||
set('wp_subject',p.subject); set('wp_system',p.system); set('wp_location',p.location);
|
||||
set('wp_assignees',p.assignees); set('wp_distribution',p.distribution);
|
||||
set('wp_due',p.due); set('wp_spec',p.spec); set('wp_desc',p.desc); set('wp_hours',p.hours);
|
||||
set('wp_kit_owner',p.kitOwner); set('wp_kit_date',p.kitDate); set('wp_mimo_time',p.mimoTime); set('wp_mimo_loc',p.mimoLoc);
|
||||
set('wp_actual_hrs',p.actualHrs); set('wp_installed_qty',p.installedQty); set('wp_redlines',p.redlines); set('wp_lessons',p.lessons);
|
||||
buildTypePicker(); document.getElementById('wp_type').value=p.type||'';
|
||||
buildCostCodes(); document.getElementById('wp_cost').value=p.cost||'';
|
||||
set('wp_wbs',p.wbs);
|
||||
document.getElementById('wp_kit_status').value=p.kitStatus||'';
|
||||
buildSequencePicker(); document.getElementById('wp_seq').value=p.seq||'';
|
||||
setRadio('status',p.status||'Draft');
|
||||
// number dimensions
|
||||
numberDims = p.numberDims ? {...p.numberDims} : {}; buildNumberDims();
|
||||
// derived discipline
|
||||
const dd=document.getElementById('wp_discipline_derived'); if(dd) dd.textContent=derivedDiscipline()||'—';
|
||||
// overrides + locked quality/hold
|
||||
pkgOverrides=p.overrides?{...p.overrides}:{};
|
||||
set('wp_qc', p.qc!=null?p.qc:sopValueFor('wp_qc'));
|
||||
set('wp_photo', p.photo!=null?p.photo:sopValueFor('wp_photo'));
|
||||
set('wp_hold', (p.hold&&p.hold.trim())?p.hold:sopValueFor('wp_hold'));
|
||||
lockQuality('wp_qc'); lockQuality('wp_photo'); lockQuality('wp_hold');
|
||||
// collections
|
||||
pkgMaterials=(p.materials&&p.materials.length)?p.materials.map(m=>({...m,unit:(m.unit||'').toUpperCase()})):[{qty:'',unit:'',desc:''}]; buildMaterials();
|
||||
pkgAttach=(p.attachments&&p.attachments.length)?p.attachments.map(a=>({...a})):[{doc:'',rev:'',link:''}]; buildAttach();
|
||||
pkgWorkSteps=(p.workSteps&&p.workSteps.length)?p.workSteps.slice():(p.work?String(p.work).split('\n').filter(Boolean):['']); if(!pkgWorkSteps.length)pkgWorkSteps=['']; buildWorkSteps();
|
||||
pkgConstraints=(p.constraints||[]).map(c=>({...c})); if(!pkgConstraints.length) buildConstraints(); else renderConstraintRows();
|
||||
pkgSignoffs=(p.signoffs||[]).map(s=>({...s, fromSOP:!!s.name})); if(!pkgSignoffs.length) buildSignoffs(); else renderSignoffRows();
|
||||
pkgHolds=(p.holds||[]).map(h=>({...h}));
|
||||
updateNumber(); updateReleaseBanner(); prevStatus=p.status||'Draft'; showForm();
|
||||
}
|
||||
function renderConstraintRows(){ const tmp=pkgConstraints; pkgConstraints=[]; buildConstraints();
|
||||
tmp.forEach(s=>{ const c=pkgConstraints.find(x=>x.name===s.name); if(c){ c.status=s.status; c.comment=s.comment; }});
|
||||
buildConstraints(); }
|
||||
function renderSignoffRows(){ const tmp=pkgSignoffs; pkgSignoffs=[]; buildSignoffs(); tmp.forEach(s=>{ const c=pkgSignoffs.find(x=>x.role===s.role); if(c){ c.name=s.name; c.date=s.date; c.signed=s.signed; c.dateReason=s.dateReason||''; }}); buildSignoffs(); }
|
||||
|
||||
function newPackage(){
|
||||
editingId=null;
|
||||
['wp_subject','wp_system','wp_location','wp_wbs','wp_assignees','wp_distribution','wp_due','wp_spec','wp_desc','wp_hours','wp_kit_owner','wp_kit_date','wp_mimo_time','wp_mimo_loc','wp_actual_hrs','wp_installed_qty','wp_redlines','wp_lessons'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';});
|
||||
document.getElementById('wp_type').value=''; document.getElementById('wp_kit_status').value=''; document.getElementById('wp_cost').value='';
|
||||
const dd=document.getElementById('wp_discipline_derived'); if(dd) dd.textContent='—';
|
||||
setRadio('status','Draft');
|
||||
numberDims={}; buildNumberDims();
|
||||
pkgMaterials=[{qty:'',unit:'',desc:''}]; buildMaterials();
|
||||
pkgAttach=[{doc:'',rev:'',link:''}]; buildAttach();
|
||||
pkgWorkSteps=['']; buildWorkSteps();
|
||||
pkgConstraints=[]; buildConstraints(); pkgSignoffs=[]; buildSignoffs();
|
||||
pkgHolds=[]; pkgOverrides={};
|
||||
set_quality_from_sop(); lockQuality('wp_qc'); lockQuality('wp_photo'); lockQuality('wp_hold');
|
||||
prevStatus='Draft';
|
||||
updateNumber(); updateReleaseBanner(); showForm(); track('new_package');
|
||||
}
|
||||
function set_quality_from_sop(){ const set=(id,v)=>{const el=document.getElementById(id); if(el) el.value=v||'';}; set('wp_qc',sopValueFor('wp_qc')); set('wp_photo',sopValueFor('wp_photo')); set('wp_hold',sopValueFor('wp_hold')); }
|
||||
function exportPackages(){
|
||||
if(!savedPackages.length){ alert('No packages saved yet.'); return; }
|
||||
const payload={tool:'Work Package (IWP)', project:(SOP&&SOP.project&&SOP.project.name)||'', exportedAt:new Date().toISOString(), packages:savedPackages};
|
||||
const blob=new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}); const a=document.createElement('a');
|
||||
a.href=URL.createObjectURL(blob); a.download='work-packages-'+new Date().toISOString().slice(0,10)+'.json';
|
||||
document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); track('packages_exported',{count:savedPackages.length});
|
||||
}
|
||||
|
||||
// ── VIEW SOP REFERENCE (comment 2) ───────────────────────────────────────────
|
||||
function openSopModal(){
|
||||
if(!SOP){ alert('No SOP loaded.'); return; }
|
||||
const p=SOP.project||{}, g=SOP.governance||{}, q=SOP.quality||{};
|
||||
const row=(k,v)=>`<tr><th style="width:170px;text-align:left;padding:4px 8px;background:var(--surface2)">${esc(k)}</th><td style="padding:4px 8px">${v||'—'}</td></tr>`;
|
||||
let h=`<table style="width:100%;border-collapse:collapse;font-size:13px">`;
|
||||
h+=row('Project', esc(p.name)); h+=row('Number', esc(p.number)); h+=row('Client', esc(p.client));
|
||||
h+=row('Division', esc(p.division)); h+=row('PM / CM / QM', [p.pm,p.cm,p.qm].filter(Boolean).map(esc).join(' / '));
|
||||
h+=row('WP number format', `<code>${esc(g.woFormat||'—')}</code>`); h+=row('Issuance', esc((g.issuance||[]).join(', ')));
|
||||
h+=row('Target WP size', esc(g.woSize));
|
||||
h+=row('WP types', enabledTypes().map(t=>esc(t.name)).join(', '));
|
||||
h+=row('QC', esc([q.qcReq,q.qcScope].filter(Boolean).join(' — '))); h+=row('Photo', esc(q.photo));
|
||||
h+=row('Hold points', esc(q.holdPoints)); h+=row('Tracking tool', esc((SOP.field&&SOP.field.trackPlatform)));
|
||||
h+=row('Sequence', ((SOP.sequence||[]).map(s=>esc(s.label)+(s.kind==='gate'?' (gate)':'')).join(' → ')));
|
||||
h+=row('Roles', (SOP.roles||[]).map(r=>esc(r.role)+': '+esc(r.name)).join('; '));
|
||||
h+=row('Sources', (SOP.sources||[]).map(s=>esc(s.label)+' ('+esc(s.system)+')').join('; '));
|
||||
h+=`</table>`;
|
||||
document.getElementById('sop-modal-body').innerHTML=h;
|
||||
document.getElementById('sop-modal').classList.add('open'); track('view_sop');
|
||||
}
|
||||
function closeSopModal(){ document.getElementById('sop-modal').classList.remove('open'); }
|
||||
const ANALYTICS_KEY='wp_iwp_analytics_v1'; 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(d){ try{ localStorage.setItem(ANALYTICS_KEY, JSON.stringify(d)); }catch(e){} }
|
||||
function track(event,detail){ if(devMode) return; try{ const d=analyticsLoad(); d.events.push({ts:new Date().toISOString(),session:_session,event,detail:detail||null}); if(d.events.length>5000)d.events=d.events.slice(-5000); analyticsSave(d); }catch(e){} }
|
||||
function showAnalytics(){ const d=analyticsLoad(); const by={}; const ses=new Set(); d.events.forEach(e=>{by[e.event]=(by[e.event]||0)+1;ses.add(e.session);});
|
||||
let t=`USAGE ANALYTICS\n\nSessions: ${ses.size} Events: ${d.events.length}\n\nActions:\n`; Object.keys(by).forEach(k=>t+=` ${k}: ${by[k]}\n`); t+=`\nSaved packages (this device): ${savedPackages.length}\n\nDownload full log as JSON?`;
|
||||
if(confirm(t)) downloadAnalytics(); }
|
||||
function downloadAnalytics(){ const d=analyticsLoad(); const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='wp-iwp-usage-'+new Date().toISOString().slice(0,10)+'.json'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); track('analytics_exported'); }
|
||||
|
||||
// ── COMMENTS ─────────────────────────────────────────────────────────────────
|
||||
const COMMENTS_KEY='wp_iwp_comments_v1';
|
||||
function cmtLoad(){ let d; try{ d=JSON.parse(localStorage.getItem(COMMENTS_KEY)); }catch(e){} if(!d||typeof d!=='object')d={}; if(!d.clientId)d.clientId='c_'+Date.now().toString(36)+Math.random().toString(36).slice(2,6); if(typeof d.author!=='string')d.author=''; if(!Array.isArray(d.comments))d.comments=[]; return d; }
|
||||
function cmtSave(d){ try{ localStorage.setItem(COMMENTS_KEY, JSON.stringify(d)); }catch(e){} }
|
||||
function cmtSaveAuthor(v){ const d=cmtLoad(); d.author=v; cmtSave(d); }
|
||||
function toggleComments(){ const dr=document.getElementById('cmt-drawer'),ov=document.getElementById('cmt-overlay'); const open=!dr.classList.contains('open'); dr.classList.toggle('open',open); ov.classList.toggle('open',open); dr.setAttribute('aria-hidden',open?'false':'true'); if(open){ cmtUpdateCurStep(); renderComments(); const a=document.getElementById('cmt-author'); if(a&&!a.value)a.focus(); else document.getElementById('cmt-input')?.focus(); } }
|
||||
function cmtUpdateCurStep(){ const el=document.getElementById('cmt-cur-step'); if(el) el.textContent=currentView; }
|
||||
function addComment(){ const d=cmtLoad(); const author=(document.getElementById('cmt-author').value||'').trim(); const text=(document.getElementById('cmt-input').value||'').trim(); if(!author){ alert('Please add your name first.'); document.getElementById('cmt-author').focus(); return; } if(!text){ document.getElementById('cmt-input').focus(); return; } d.author=author; d.comments.push({id:'m_'+Date.now().toString(36)+Math.random().toString(36).slice(2,5), view:currentView, author, clientId:d.clientId, text, ts:new Date().toISOString()}); cmtSave(d); document.getElementById('cmt-input').value=''; renderComments(); refreshCommentBadges(); track('comment_added'); }
|
||||
function deleteComment(id){ const d=cmtLoad(); d.comments=d.comments.filter(c=>c.id!==id); cmtSave(d); renderComments(); refreshCommentBadges(); }
|
||||
function renderComments(){ const d=cmtLoad(); const list=document.getElementById('cmt-list'); if(!list) return; if(!d.comments.length){ list.innerHTML='<div class="cmt-empty">No comments yet.</div>'; return; } const sorted=[...d.comments].sort((a,b)=>(b.ts||'').localeCompare(a.ts||'')); list.innerHTML=sorted.map(c=>{ const mine=c.clientId===d.clientId; const when=c.ts?new Date(c.ts).toLocaleString():''; return `<div class="cmt-item${mine?' mine':''}"><div class="cmt-meta"><span class="cmt-author">${esc(c.author||'Anonymous')}</span><span class="cmt-step">${esc(c.view||'')}</span><span class="cmt-time">${esc(when)}</span></div><div class="cmt-text">${esc(c.text)}</div>${mine?`<div><button class="cmt-del" style="float:right" onclick="deleteComment('${c.id}')">Delete</button></div>`:''}</div>`; }).join(''); }
|
||||
function refreshCommentBadges(){ const d=cmtLoad(); const t=document.getElementById('cbadge-total'); if(t){ if(d.comments.length){ t.style.display=''; t.textContent=d.comments.length; } else t.style.display='none'; } }
|
||||
function exportComments(){ const d=cmtLoad(); const payload={tool:'Work Package (IWP)', exportedBy:d.author||'', exportedAt:new Date().toISOString(), clientId:d.clientId, comments:d.comments}; const blob=new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}); const safe=(d.author||'review').replace(/[^a-z0-9]+/gi,'-').toLowerCase(); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`wp-iwp-comments-${safe}-${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); track('comments_exported'); }
|
||||
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.'); return; } const d=cmtLoad(); const seen=new Set(d.comments.map(c=>c.id)); let added=0; incoming.forEach(c=>{ if(c&&c.id&&!seen.has(c.id)){ d.comments.push(c); seen.add(c.id); added++; }}); cmtSave(d); renderComments(); refreshCommentBadges(); alert(`Imported ${added} comment${added===1?'':'s'}.`); }catch(e){ alert('Could not read that file.'); } ev.target.value=''; }; r.readAsText(f); }
|
||||
function clearMyComments(){ const d=cmtLoad(); const mine=d.comments.filter(c=>c.clientId===d.clientId).length; if(!mine){ alert('No comments to clear.'); return; } if(!confirm(`Delete your ${mine} comment(s)?`)) return; d.comments=d.comments.filter(c=>c.clientId!==d.clientId); cmtSave(d); renderComments(); refreshCommentBadges(); }
|
||||
function cmtInit(){ const d=cmtLoad(); cmtSave(d); const a=document.getElementById('cmt-author'); if(a)a.value=d.author||''; cmtUpdateCurStep(); renderComments(); refreshCommentBadges(); }
|
||||
|
||||
// ── STATUS PILLS ─────────────────────────────────────────────────────────────
|
||||
document.querySelectorAll('#status-group .radio-pill').forEach(p=>p.addEventListener('click',e=>{
|
||||
if(e.target.tagName==='INPUT') return;
|
||||
const v=p.dataset.val;
|
||||
prevStatus = getRadio('status') || 'Draft'; // capture before switching (for revert)
|
||||
document.querySelectorAll('#status-group .radio-pill').forEach(x=>x.classList.remove('selected'));
|
||||
p.classList.add('selected'); p.querySelector('input').checked=true;
|
||||
onStatusChange(v);
|
||||
}));
|
||||
|
||||
// ── BOOT ─────────────────────────────────────────────────────────────────────
|
||||
loadStore();
|
||||
loadSampleSOP();
|
||||
setRadio('status','Draft');
|
||||
renderSavedList();
|
||||
cmtInit();
|
||||
track('app_open');
|
||||
235
wp-creation-index.html
Normal file
235
wp-creation-index.html
Normal file
@@ -0,0 +1,235 @@
|
||||
<!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="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" 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" style="padding:7px 16px" onclick="loadSampleSOP()">⤓ Sample SOP</button>
|
||||
<button class="btn btn-ghost" 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="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>Discipline</label><div class="derived-box" id="wp_discipline_derived">—</div><div class="field-hint">derived from WP type · drives the readiness checklist</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>
|
||||
|
||||
<!-- SCOPE & WORK -->
|
||||
<div class="card">
|
||||
<div class="sub-heading">Scope & Work</div>
|
||||
<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 class="field-grid">
|
||||
<div class="field"><label>Labor – Est. Hrs.</label><input type="number" id="wp_hours" min="0" step="1" placeholder="e.g. 20"><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 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>
|
||||
</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>
|
||||
|
||||
<!-- 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="wp-creation-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
559
wp-creation-styles.css
Normal file
559
wp-creation-styles.css
Normal file
@@ -0,0 +1,559 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300&display=swap');
|
||||
|
||||
: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', monospace;
|
||||
--sans: 'IBM Plex Sans', 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: 10px; font-weight: 600; letter-spacing: .15em;
|
||||
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; }
|
||||
Reference in New Issue
Block a user