/* Loadpath — deliverable documents rendered as "paper", plus the doc viewer. */ (function () { const { Icon, Status, engineerTypeLabel } = window; const h = React.createElement; function PaperHead({ eyebrow, title, subtitle, rows }) { return h('div', { className: 'paper-head' }, h('div', { className: 'paper-eyebrow' }, eyebrow), h('h1', null, title), subtitle && h('div', { style: { fontFamily: 'var(--font-mono)', fontSize: 12.5, color: '#6b6b6b', marginTop: 6 } }, subtitle), h('div', { className: 'paper-grid', style: { marginTop: 18 } }, rows.map((r, i) => h(React.Fragment, { key: i }, h('div', { className: 'k' }, r[0]), h('div', { className: 'v' }, r[1]))))); } function SealNote() { return h('div', { className: 'seal-note' }, 'Engineer of Record — to be sealed downstream by a Professional Engineer licensed in the project state. ', 'This document contains complete, seal-ready content and bears no draft or review mark.'); } // ---------- Calculation package ---------- function CalcDoc({ project, combo }) { if (!combo) { const ct = project.criteria || {}; return h('div', { className: 'paper' }, h(PaperHead, { eyebrow: 'PE Calculation Package · ASD · ASCE 7-22 / NDS 2018', title: project.name, rows: [ ['Project', project.name], ['Contractor', project.contractor || '—'], ['Address', project.address + ', ' + project.city], ['Outcome', project.outcome || '—'], ], }), ct.wind_speed_mph_ultimate && h('div', { className: 'formula', style: { marginBottom: 16 } }, 'Wind (ult.) = ' + ct.wind_speed_mph_ultimate + ' mph\n', 'Ground snow load = ' + ct.ground_snow_load_psf + ' psf\n', 'Seismic SDC = ' + (ct.seismic_design_category || '—') + '\n', 'Exposure cat. = ' + (ct.exposure_category || '—') + '\n', 'Applicable codes = ' + (ct.applicable_codes || '—')), h('div', { style: { background: 'var(--bg-sunken)', borderRadius: 'var(--radius-md)', padding: '14px 18px', color: 'var(--fg-muted)', fontSize: 13, fontFamily: 'var(--font-mono)', lineHeight: 1.6 } }, 'Framing combination detail not available for this run.\n', 'Download the structural calculation PDF for full DCR check results.'), h(SealNote)); } const c = combo; const failing = c.failing || (c.pass === false); const checks = c.checks || c.dcr_checks || []; const ld = c.loads || {}; const seis = c.seismic || {}; const dpv = Number(ld.dpv || ld.Dpv || 0); const droof = Number(ld.droof || ld.Droof || 0); const dtotal = Number(ld.dtotal || ld.Dtotal || 0); const snow = Number(ld.snow || 0); const windUp = Number(ld.wind_uplift || ld.windUplift || ld.WindUplift || 0); const windDown = Number(ld.wind_down || ld.windDown || ld.WindDown || 0); return h('div', { className: 'paper' }, h(PaperHead, { eyebrow: 'PE Calculation Package · ASD · ASCE 7-22 / NDS 2018', title: project.name, subtitle: (c.engineerType ? engineerTypeLabel(c.engineerType) + ' · ' : '') + c.label, rows: [ ['Project', project.name], ['Contractor', project.contractor], ['Address', project.address + ', ' + project.city], ['Engineer of Record', 'Engineer of Record'], ['Date of issue', project.date], ['Roof in scope', (c.roof || '') + ' · ' + (c.framing || '')], ['System (DC / AC)', (project.system.dc || '—') + ' / ' + (project.system.ac || '—')], ['Mean roof height', '24 ft'], ], }), failing && h('p', { style: { background: '#fbe7e5', border: '1px solid #e7bdb9', padding: '10px 14px', borderRadius: 4, color: '#9a342d', fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: 12, letterSpacing: '0.03em' } }, 'DOES NOT PASS AS EXISTING — REMEDIATION REQUIRED'), h('h2', null, h('span', { className: 'secnum' }, '1'), 'Purpose and Scope'), h('p', null, 'This package analyzes the ', (c.roof || 'roof').toLowerCase(), ' roof framed with ', (c.framing || '—'), ' supporting the added PV array. Allowable Stress Design (ASD) is used throughout. The Engineer of Record certifies the existing roof framing has capacity for the applied PV loads as described herein.'), h('h2', null, h('span', { className: 'secnum' }, '5'), 'Dead Load'), h('div', { className: 'formula' }, 'D_pv = W_pv_total / A_footprint = ', dpv.toFixed(2), ' psf\n', 'D_roof = Σ component weights = ', droof.toFixed(1), ' psf ', h('span', { className: 'cmt' }, '(assumed assembly, cited)'), '\n', 'D = D_roof + D_pv = ', dtotal.toFixed(1), ' psf\n', 'w_D = D × s = ', (dtotal * 16 / 12).toFixed(0), ' plf'), h('h2', null, h('span', { className: 'secnum' }, '6'), 'Snow Load'), h('div', { className: 'formula' }, 'p_f = 0.7 · C_e · C_t · I_s · pg = ', snow.toFixed(1), ' psf ', h('span', { className: 'cmt' }, '(pg from ASCE API)'), '\n', 'p_s = C_s · p_f governing roof snow\n', 'w_S = p_s · s = ', (snow * 16 / 12).toFixed(0), ' plf'), c.driftFlag && h('p', { style: { fontStyle: 'italic', color: '#8a5e0c' } }, '† Potential drift condition flagged (lower roof beneath upper roof). Not computed here — routed to the downstream PE.'), h('h2', null, h('span', { className: 'secnum' }, '7'), 'Wind Load'), h('div', { className: 'formula' }, 'q_h = 0.00256 · K_h · K_zt · K_d · K_e · V²\n', 'p = q_h · (GC_rn) ', h('span', { className: 'cmt' }, '(GC_rn per §29.4.3, matched to KB)'), '\n', 'Governing net uplift = ', windUp.toFixed(1), ' psf\n', 'Governing downward = ', windDown.toFixed(1), ' psf'), h('h2', null, h('span', { className: 'secnum' }, '9'), 'Roof Framing Check (ASD)'), checks.length > 0 ? h('table', { className: 'ptable' }, h('thead', null, h('tr', null, h('th', null, 'Check'), h('th', { className: 'r' }, 'Demand'), h('th', { className: 'r' }, 'Capacity'), h('th', { className: 'r' }, 'DCR'), h('th', { className: 'r' }, 'Status'))), h('tbody', null, checks.map((ck, i) => { const dcr = Number(ck.dcr || 0); const name = String(ck.name || ck.label || 'Check'); return h('tr', { key: i }, h('td', null, name.split(' — ')[0], ck.combo ? h('span', { style: { color: '#999', fontSize: 11 } }, ' · ' + ck.combo) : null), h('td', { className: 'n r' }, ck.demand != null ? ck.demand + ' ' + (ck.unit || '') : '—'), h('td', { className: 'n r' }, ck.capacity != null ? ck.capacity + ' ' + (ck.unit || '') : '—'), h('td', { className: 'n r', style: { fontWeight: 700, color: dcr > 1 ? '#9a342d' : '#222' } }, dcr.toFixed(3)), h('td', { className: 'r' }, h('span', { style: { fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 11, color: dcr > 1 ? '#9a342d' : '#2c6e4a' } }, ck.status || (ck.pass ? 'PASS' : 'FAIL')))); }))) : h('div', { style: { background: 'var(--bg-sunken)', borderRadius: 'var(--radius-md)', padding: '14px 18px', color: 'var(--fg-muted)', fontSize: 13, fontFamily: 'var(--font-mono)' } }, 'DCR check data not returned by the calculation engine for this run. Download the PDF package for full calculation details.'), h('h2', null, h('span', { className: 'secnum' }, '10'), 'Seismic — Less-Than-10% Rule (§13.1.4)'), h('div', { className: 'formula' }, seis.sds != null ? 'SDS = ' + seis.sds + ' SD1 = ' + (seis.sd1 || '—') + ' SDC ' + (seis.category || '—') + '\n' : '', seis.check || 'r < 0.10 → ', h('span', { className: 'cmt' }, seis.check ? '' : ' EXEMPT from §13.1.4 component seismic forces')), h('h2', null, h('span', { className: 'secnum' }, '11'), 'Summary of Results'), h('p', { style: { marginTop: 4 } }, failing ? h('span', { className: 'pass-stamp fail' }, h(Icon, { name: 'x-circle', size: 14 }), 'Does not clear-pass — bending DCR exceeds 1.00') : h('span', { className: 'pass-stamp pass' }, h(Icon, { name: 'check-circle', size: 14 }), 'Clear PASS — every check DCR ≤ 1.00 (unrounded)')), SealNote && h(SealNote)); } // ---------- PE letter ---------- function LetterDoc({ project, combo }) { if (!combo) return h('div', { className: 'paper' }, h(PaperHead, { eyebrow: 'Professional Engineer Letter', title: project.name, rows: [ ['Address', project.address + ', ' + project.city], ['Contractor', project.contractor || '—'], ['Outcome', project.outcome || '—'], ['State', project.state || '—'], ], }), h('div', { style: { background: 'var(--bg-sunken)', borderRadius: 'var(--radius-md)', padding: '14px 18px', color: 'var(--fg-muted)', fontSize: 13, fontFamily: 'var(--font-mono)' } }, 'Framing combination detail not available for this run. Download the PE letter PDF for the full engineering letter.'), h(SealNote)); const c = combo; const ct = project.criteria || {}; return h('div', { className: 'paper' }, h(PaperHead, { eyebrow: 'Professional Engineer Letter', title: project.name, rows: [ ['Date', project.date], ['Roof in scope', c.label], ['Engineering route', c.engineerType ? engineerTypeLabel(c.engineerType) : '—'], ['Address', project.address + ', ' + project.city], ['Contractor', project.contractor], ], }), h('p', { style: { marginTop: 8 } }, 'To Whom it May Concern,'), h('p', null, 'This letter certifies that the existing roof framing directly supporting the added photovoltaic gravity loads — ', h('b', null, c.framing || '—'), ' over the ', (c.roof || 'roof').toLowerCase(), ' roof — meets or exceeds the design criteria below and is adequate to carry the applied PV loads ', (project.outcome || '').indexOf('SISTERING') === 0 ? 'once the specified rafter sistering is performed' : 'without modification', '.'), h('table', { className: 'ptable', style: { margin: '18px 0' } }, h('tbody', null, [['Applicable codes', ct.applicable_codes], ['Ground snow load', ct.ground_snow_load_psf + ' psf'], ['Design wind speed', ct.wind_speed_mph_ultimate + ' mph (ult.)'], ['Risk category', ct.risk_category], ['Exposure category', ct.exposure_category], ['Roof framing', c.framing], ['Attachment system', project.system.attach], ['PV modules', project.system.modules + ' × ' + project.system.model], ['Racking system', project.system.racking]].map((r, i) => h('tr', { key: i }, h('td', { style: { width: '42%', color: '#777', fontFamily: 'var(--font-mono)', fontSize: 11 } }, r[0]), h('td', { className: 'n' }, r[1]))))), h('p', null, 'The framing was evaluated under combined dead, snow, wind, and seismic loads in accordance with ASCE 7-22 and the applicable state building code. Based on this analysis, the existing roof structure is adequate ', (project.outcome || '').indexOf('SISTERING') === 0 ? 'once the sistering detail is installed as specified.' : 'to support the proposed PV system as described.'), h('p', { style: { marginTop: 28 } }, 'Respectfully,'), h('p', { style: { margin: 0, fontFamily: 'var(--font-sans)', fontSize: 13, color: '#555' } }, 'Engineer of Record'), h('p', { style: { margin: 0, fontFamily: 'var(--font-sans)', fontSize: 12, color: '#999' } }, 'Licensed Professional Engineer · ' + project.state), h(SealNote)); } // ---------- Drawing set (stylized preview) ---------- function DrawingDoc({ project }) { return h('div', { className: 'paper', style: { padding: '40px 44px' } }, h('div', { style: { border: '2px solid #1a1a1a', minHeight: 760, display: 'grid', gridTemplateRows: '1fr 84px' } }, h('div', { style: { position: 'relative', background: 'repeating-linear-gradient(0deg,#fafafa,#fafafa 23px,#f0eee9 24px),repeating-linear-gradient(90deg,#fafafa,#fafafa 23px,#f0eeea 24px)' } }, h('div', { style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center', color: '#bbb' } }, h('div', null, h(Icon, { name: 'sun', size: 40, style: { color: '#d8c9a8' } }), h('div', { style: { fontFamily: 'var(--font-mono)', fontSize: 12, marginTop: 10, color: '#aaa' } }, 'PV ROOF PLAN — module layout · attachment grid · framing'), h('div', { style: { fontFamily: 'var(--font-mono)', fontSize: 11, marginTop: 4, color: '#c4c4c4' } }, project.system.modules + ' modules · ' + project.system.racking)))), h('div', { style: { borderTop: '2px solid #1a1a1a', display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', fontFamily: 'var(--font-mono)', fontSize: 10 } }, [['PROJECT', project.name], ['SHEET', 'PV5.0 — FRAMING'], ['SCALE', '1/4" = 1\'-0"']].map((b, i) => h('div', { key: i, style: { borderRight: i < 2 ? '1px solid #1a1a1a' : 'none', padding: '12px 14px' } }, h('div', { style: { color: '#999', fontSize: 9, letterSpacing: '0.06em' } }, b[0]), h('div', { style: { color: '#222', fontSize: 11, marginTop: 4, fontWeight: 600 } }, b[1])))))); } // ---------- Document viewer overlay ---------- function DocViewer({ project, files, dcrStyle, onClose }) { const docs = filesToDocs(project, files); const [active, setActive] = React.useState(0); const d = docs[active]; const downloadable = docs.filter(x => x.file && x.file.path); if (!d) { return h('div', { className: 'docview' }, h('div', { className: 'docview-side' }, h('h4', null, project.name), h('div', { className: 'ds-sub' }, '0 deliverables')), h('div', { className: 'docview-main' }, h('div', { className: 'docview-close' }, h('button', { className: 'btn btn-secondary btn-sm', onClick: onClose }, h(Icon, { name: 'x', size: 14 }), 'Close')), h('div', { className: 'paper', style: { display: 'grid', placeItems: 'center', minHeight: 400, textAlign: 'center', color: 'var(--fg-muted)' } }, 'No PDF deliverables are available.'))); } return h('div', { className: 'docview' }, h('div', { className: 'docview-side' }, h('h4', null, project.name), h('div', { className: 'ds-sub' }, files.length + ' deliverables · ' + project.outcome), downloadable.length > 0 && h('button', { className: 'btn btn-secondary btn-sm', style: { width: '100%', marginBottom: 14, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, onClick: () => downloadAllFiles(downloadable), }, h(Icon, { name: 'download', size: 14 }), 'Download all (' + downloadable.length + ')'), docs.map((doc, i) => h('div', { key: i, className: 'doc-item' + (i === active ? ' active' : ''), onClick: () => setActive(i) }, h('div', { className: 'di' }, doc.tag), h('div', { style: { flex: 1, minWidth: 0 } }, h('div', { className: 'dt' }, doc.title), h('div', { className: 'dm' }, doc.meta)), doc.file && doc.file.path && h('a', { href: doc.file.path, title: 'Download PDF', onClick: (e) => e.stopPropagation(), style: { flexShrink: 0, display: 'grid', placeItems: 'center', width: 26, height: 26, borderRadius: 6, color: 'var(--fg-subtle)' }, }, h(Icon, { name: 'download', size: 14 }))))), h('div', { className: 'docview-main' }, h('div', { className: 'docview-close', style: { display: 'flex', gap: 8 } }, d.file && d.file.path && h('a', { href: d.file.path, className: 'btn btn-secondary btn-sm', style: { display: 'flex', alignItems: 'center', gap: 6 }, }, h(Icon, { name: 'download', size: 14 }), 'Download PDF'), h('button', { className: 'btn btn-secondary btn-sm', onClick: onClose }, h(Icon, { name: 'x', size: 14 }), 'Close')), h('div', { className: 'fade-in', key: active }, h(RealPDFOrFallback, { doc: d })))); } // Shows the actual generated PDF (the file that was delivered) inline when // one exists, instead of the synthetic "paper" HTML reconstruction. function RealPDFOrFallback({ doc }) { const path = doc.file && doc.file.path; const [state, setState] = React.useState('loading'); // loading | ready | error const [url, setUrl] = React.useState(null); React.useEffect(() => { if (!path) return; let alive = true; let objectUrl = null; setState('loading'); setUrl(null); const token = window.API && window.API.auth ? window.API.auth.getToken() : ''; fetch(path, { headers: token ? { 'Authorization': 'Bearer ' + token } : {} }) .then(res => { if (!res.ok) throw new Error('failed to load PDF'); return res.blob(); }) .then(blob => { if (!alive) return; objectUrl = URL.createObjectURL(blob); setUrl(objectUrl); setState('ready'); }) .catch(() => { if (alive) setState('error'); }); return () => { alive = false; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [path]); if (!path) return doc.render(); if (state === 'error') return h('div', { className: 'paper', style: { display: 'grid', placeItems: 'center', minHeight: 400, textAlign: 'center' } }, h('div', null, h(Icon, { name: 'alert-triangle', size: 22, style: { color: 'var(--signal-danger)' } }), h('div', { style: { marginTop: 10, fontSize: 13.5, color: 'var(--fg-muted)' } }, 'Could not load the PDF for preview.'), h('a', { href: path, className: 'btn btn-secondary btn-sm', style: { marginTop: 14, display: 'inline-flex', alignItems: 'center', gap: 6 } }, h(Icon, { name: 'download', size: 14 }), 'Download instead'))); if (state === 'loading') return h('div', { style: { display: 'grid', placeItems: 'center', minHeight: 400 } }, h(Icon, { name: 'sun', size: 28, style: { color: 'var(--accent)', animation: 'spin 1.5s linear infinite' } })); return h('iframe', { src: url, title: doc.title, style: { width: '100%', height: '80vh', border: '1px solid var(--border)', borderRadius: 8, background: '#fff', display: 'block' }, }); } // Real (backend-stored) deliverable files carry a download path at // /api/v1/files/{id}/download (auth via the same-origin "token" cookie set // at login — a plain works without any fetch/blob plumbing). // Demo-mode / mock projects have no such path, so downloads are simply // omitted for them rather than pointing at a dead link. function fileMetaFor(f) { const id = f.id || f.fileID || f.file_id; const path = f.path || (id ? '/api/v1/files/' + id + '/download' : ''); if (!path) return null; return { id, path, name: f.name || 'document.pdf' }; } function downloadAllFiles(downloadable) { downloadable.forEach((doc, i) => { setTimeout(() => { const a = document.createElement('a'); a.href = doc.file.path; a.rel = 'noopener'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }, i * 300); }); } function filesToDocs(project, files) { const out = []; files.forEach((f) => { if (f.kind === 'manifest') return; const engineerType = f.engineerType || f.engineer_type || ''; let combo = comboForFile(project, engineerType); const file = fileMetaFor(f); if (f.kind === 'drawing' || f.kind === 'drawings' || f.kind === 'stamped_drawing' || f.kind === 'sistering_detail') { out.push({ tag: f.kind === 'sistering_detail' ? 'DET' : 'DWG', title: f.name, meta: f.meta, file, render: () => h(DrawingDoc, { project }) }); } else if (f.kind === 'calc') { out.push({ tag: 'CALC', title: f.name, meta: f.meta, file, render: () => h(CalcDoc, { project, combo }) }); } else if (f.kind === 'pe_letter') { out.push({ tag: 'PE', title: f.name, meta: f.meta, file, render: () => h(LetterDoc, { project, combo }) }); } else { out.push({ tag: 'DOC', title: f.name || 'Document', meta: f.meta, file, render: () => h(GenericDoc, { project, file: f }) }); } }); return out; } // ---------- Fallback preview for kinds with no dedicated "paper" renderer ---------- function GenericDoc({ project, file }) { return h('div', { className: 'paper' }, h(PaperHead, { eyebrow: 'Generated document', title: (file && file.name) || 'Document', rows: [['Project', project.name], ['Address', project.address + ', ' + project.city]], }), h('div', { style: { background: 'var(--bg-sunken)', borderRadius: 'var(--radius-md)', padding: '14px 18px', color: 'var(--fg-muted)', fontSize: 13, fontFamily: 'var(--font-mono)', lineHeight: 1.6 } }, 'Online preview is not available for this document type.\n', 'Use the Download button to get the generated PDF.')); } function comboForFile(project, engineerType) { const runs = project.engineeringResults || project.engineering_results || []; if (engineerType && runs.length) { const run = runs.find(r => (r.engineerType || r.engineer_type || '').toUpperCase() === engineerType.toUpperCase()); if (run && run.combinations && run.combinations.length) { const combo = Object.assign({}, run.combinations[0]); combo.engineerType = engineerType; return combo; } } const combos = project.combinations || []; if (engineerType) { const combo = combos.find(c => (c.engineerType || c.engineer_type || '').toUpperCase() === engineerType.toUpperCase()); if (combo) return Object.assign({}, combo, { engineerType }); } return combos[0]; } Object.assign(window, { DocViewer, CalcDoc, LetterDoc, DrawingDoc, GenericDoc }); })();