fix: crash bugs in TeamInvoices + faster load

Bug fixes:
- TeamInvoices: add useAuth import/currentUser (invoice-create crash)
- TeamInvoices: setChartYear -> setExportYear (year-dropdown crash)
- TeamDashboard: drop redundant setState-in-effect (cascading renders)
- Companies: delete dead _UnusedClientCompanies (illegal hook calls)
- annotate intentional empty PDF-fallback catches

Load speed:
- preconnect/dns-prefetch to Supabase origin
- lazy-load heic-to in Converters: page chunk 2737KB -> 9KB
- split recharts into its own 'charts' vendor chunk

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-08 12:59:11 -04:00
parent 85625f4d95
commit 04e0911e9f
101 changed files with 11786 additions and 7445 deletions
@@ -0,0 +1,686 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checked Sites Revision Audit</title>
<style>
:root {
--bg: #f5f1e8;
--panel: rgba(255,255,255,0.78);
--panel-strong: rgba(255,255,255,0.92);
--border: rgba(43,37,28,0.12);
--text: #231d14;
--muted: #6b6255;
--accent: #b8831f;
--accent-soft: rgba(184,131,31,0.18);
--ok: #1f8a4c;
--ok-soft: rgba(31,138,76,0.12);
--warn: #b45309;
--warn-soft: rgba(180,83,9,0.14);
--review: #2563eb;
--review-soft: rgba(37,99,235,0.13);
--todo: #7c3aed;
--todo-soft: rgba(124,58,237,0.12);
--shadow: 0 18px 40px rgba(35,29,20,0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(184,131,31,0.14), transparent 28%),
radial-gradient(circle at top right, rgba(37,99,235,0.09), transparent 24%),
linear-gradient(180deg, #fbf8f2 0%, var(--bg) 100%);
}
.wrap {
width: min(1400px, calc(100vw - 40px));
margin: 32px auto 48px;
}
.hero, .panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 18px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: var(--shadow);
}
.hero {
padding: 28px 30px;
margin-bottom: 24px;
}
.eyebrow {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
margin-bottom: 10px;
}
h1 {
margin: 0 0 10px;
font-size: 34px;
line-height: 1.05;
letter-spacing: -0.04em;
}
.subtitle {
margin: 0;
color: var(--muted);
font-size: 15px;
max-width: 900px;
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.card {
padding: 20px 22px;
}
.label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
margin-bottom: 8px;
}
.value {
font-size: 34px;
line-height: 1;
letter-spacing: -0.05em;
}
.sub {
margin-top: 8px;
font-size: 13px;
color: var(--muted);
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.panel {
padding: 20px 22px;
}
h2 {
margin: 0 0 14px;
font-size: 18px;
letter-spacing: -0.03em;
}
.bar-row {
display: grid;
grid-template-columns: 150px 1fr 36px;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.bar-label, .bar-value {
font-size: 13px;
}
.bar-track {
height: 10px;
border-radius: 999px;
background: rgba(35,29,20,0.08);
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent), #d7a84b);
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th, td {
padding: 10px 12px;
border-bottom: 1px solid rgba(35,29,20,0.08);
vertical-align: top;
text-align: left;
font-size: 13px;
}
th {
position: sticky;
top: 0;
background: var(--panel-strong);
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 11px;
z-index: 1;
}
.mono {
font-variant-numeric: tabular-nums;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 72px;
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
border: 1px solid transparent;
white-space: nowrap;
}
.ok { color: var(--ok); background: var(--ok-soft); border-color: rgba(31,138,76,0.2); }
.warn { color: var(--warn); background: var(--warn-soft); border-color: rgba(180,83,9,0.2); }
.review { color: var(--review); background: var(--review-soft); border-color: rgba(37,99,235,0.2); }
.todo { color: var(--todo); background: var(--todo-soft); border-color: rgba(124,58,237,0.18); }
.neutral { color: var(--muted); background: rgba(35,29,20,0.06); border-color: rgba(35,29,20,0.08); }
.section {
margin-bottom: 24px;
}
.small {
color: var(--muted);
font-size: 13px;
}
@media (max-width: 1100px) {
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.two-col { grid-template-columns: 1fr; }
}
@media (max-width: 700px) {
.wrap { width: min(100vw - 20px, 1400px); margin: 20px auto 32px; }
.hero, .panel { padding: 18px; border-radius: 14px; }
.grid { grid-template-columns: 1fr; }
h1 { font-size: 28px; }
}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<div class="eyebrow">Fourge Portal Audit</div>
<h1>Checked Sites Revision Audit</h1>
<p class="subtitle">This report treats your checkmark as “R00 was completed,” then separates that from billing and from any newer active revisions. It is designed to make client-billing comparison easier at a glance.</p>
</section>
<section class="grid">
<div class="panel card">
<div class="label">Checked Sites</div>
<div class="value">26</div>
<div class="sub">Sites found in the database from your checked list.</div>
</div>
<div class="panel card">
<div class="label">Active Revisions</div>
<div class="value">16</div>
<div class="sub">Checked sites that now have an active R01+.</div>
</div>
<div class="panel card">
<div class="label">R00 Billed</div>
<div class="value">23</div>
<div class="sub">Checked sites where the new-book unit is already on an invoice.</div>
</div>
<div class="panel card">
<div class="label">R00 Unbilled</div>
<div class="value">3</div>
<div class="sub">Checked sites completed at R00 but not yet billed.</div>
</div>
</section>
<section class="two-col">
<div class="panel">
<h2>Active Revision Status</h2>
<div class="bar-row">
<div class="bar-label">not_started</div>
<div class="bar-track"><div class="bar-fill" style="width:100%"></div></div>
<div class="bar-value">12</div>
</div>
<div class="bar-row">
<div class="bar-label">client_review</div>
<div class="bar-track"><div class="bar-fill" style="width:25%"></div></div>
<div class="bar-value">3</div>
</div>
<div class="bar-row">
<div class="bar-label">client_approved</div>
<div class="bar-track"><div class="bar-fill" style="width:8%"></div></div>
<div class="bar-value">1</div>
</div>
</div>
<div class="panel">
<h2>Active Revision Depth</h2>
<div class="bar-row">
<div class="bar-label">R01</div>
<div class="bar-track"><div class="bar-fill" style="width:100%"></div></div>
<div class="bar-value">10</div>
</div>
<div class="bar-row">
<div class="bar-label">R02</div>
<div class="bar-track"><div class="bar-fill" style="width:60%"></div></div>
<div class="bar-value">6</div>
</div>
</div>
</section>
<section class="panel section">
<h2>Sites With Active Revisions</h2>
<p class="small">These are the ones where R00 may be done, but a newer version is still active or awaiting review/approval.</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px;">Site #</th>
<th style="width:240px;">DB Title</th>
<th style="width:80px;">Internal #</th>
<th style="width:96px;">R00 Done</th>
<th style="width:180px;">R00 Billed</th>
<th style="width:90px;">Active R#</th>
<th style="width:120px;">Active Status</th>
<th style="width:170px;">Completed Revisions</th>
<th style="width:220px;">Billed Versions</th>
<th>Version Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono">00286</td>
<td>00286 Riverside, CA</td>
<td class="mono">31</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill todo">not_started</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=not_started</td>
</tr>
<tr>
<td class="mono">30055</td>
<td>30055 Moreno Valley, CA</td>
<td class="mono">33</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill review">client_review</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=client_review</td>
</tr>
<tr>
<td class="mono">30096</td>
<td>30096 Eerie, CO</td>
<td class="mono">36</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R02</td>
<td><span class="pill todo">not_started</span></td>
<td>R01 (unbilled)</td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=completed; R02=not_started</td>
</tr>
<tr>
<td class="mono">68639</td>
<td>68639 NSA Los Lunas NM</td>
<td class="mono">40</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill review">client_review</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=client_review</td>
</tr>
<tr>
<td class="mono">68663</td>
<td>68663 NSA Oklahoma City OK</td>
<td class="mono">41</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill todo">not_started</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=not_started</td>
</tr>
<tr>
<td class="mono">68664</td>
<td>68664 NSA Oklahoma City OK</td>
<td class="mono">42</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill todo">not_started</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=not_started</td>
</tr>
<tr>
<td class="mono">68666</td>
<td>68666 NSA Oklahoma City, OK</td>
<td class="mono">43</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill todo">not_started</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=not_started</td>
</tr>
<tr>
<td class="mono">68669</td>
<td>68669 NSA Oklahoma City OK</td>
<td class="mono">44</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill todo">not_started</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=not_started</td>
</tr>
<tr>
<td class="mono">68670</td>
<td>68670 NSA Oklahoma City OK</td>
<td class="mono">45</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R02</td>
<td><span class="pill todo">not_started</span></td>
<td>R01 (billed)</td>
<td>R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=completed/billed; R02=not_started</td>
</tr>
<tr>
<td class="mono">68671</td>
<td>68671 NSA Oklahoma City OK</td>
<td class="mono">46</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill todo">not_started</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=not_started</td>
</tr>
<tr>
<td class="mono">68673</td>
<td>68673 NSA Oklahoma City OK</td>
<td class="mono">47</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill review">client_review</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=client_review</td>
</tr>
<tr>
<td class="mono">68721</td>
<td>68721 NSA Mechanicsburg PA</td>
<td class="mono">48</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R02</td>
<td><span class="pill approved">client_approved</span></td>
<td>R01 (billed), R02 (unbilled)</td>
<td>R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=completed/billed; R02=completed</td>
</tr>
<tr>
<td class="mono">68808</td>
<td>68808 Camas, WA</td>
<td class="mono">49</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R01</td>
<td><span class="pill todo">not_started</span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=not_started</td>
</tr>
<tr>
<td class="mono">68809</td>
<td>68809 Centralia, WA</td>
<td class="mono">50</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R02</td>
<td><span class="pill todo">not_started</span></td>
<td>R01 (billed)</td>
<td>R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=completed/billed; R02=not_started</td>
</tr>
<tr>
<td class="mono">68810</td>
<td>68810 Chehalis, WA</td>
<td class="mono">51</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R02</td>
<td><span class="pill todo">not_started</span></td>
<td>R01 (billed)</td>
<td>R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=completed/billed; R02=not_started</td>
</tr>
<tr>
<td class="mono">68811</td>
<td>68811 Kelso, WA</td>
<td class="mono">52</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill review">Yes</span></td>
<td class="mono">R02</td>
<td><span class="pill todo">not_started</span></td>
<td>R01 (unbilled)</td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed; R01=completed; R02=not_started</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="panel section">
<h2>Checked Sites With No Active Revision</h2>
<p class="small">These are simpler to read: R00 completed, and no current revision chain is active right now.</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px;">Site #</th>
<th style="width:240px;">DB Title</th>
<th style="width:80px;">Internal #</th>
<th style="width:96px;">R00 Done</th>
<th style="width:180px;">R00 Billed</th>
<th style="width:90px;">Active R#</th>
<th style="width:120px;">Active Status</th>
<th style="width:170px;">Completed Revisions</th>
<th style="width:220px;">Billed Versions</th>
<th>Version Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono">30062</td>
<td>30062 Riverside, CA</td>
<td class="mono">34</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed</td>
</tr>
<tr>
<td class="mono">30094</td>
<td>30094 Colorado Springs, CO</td>
<td class="mono">35</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill warn">No</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td></td>
<td>R00=completed</td>
</tr>
<tr>
<td class="mono">30258</td>
<td>30258 Lebanon, NH</td>
<td class="mono">38</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed</td>
</tr>
<tr>
<td class="mono">30259</td>
<td>30259 Hamburg, NJ</td>
<td class="mono">39</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed</td>
</tr>
<tr>
<td class="mono">30261</td>
<td>30261 NSA Clovis NM</td>
<td class="mono">70</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill warn">No</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td></td>
<td>R00=completed</td>
</tr>
<tr>
<td class="mono">68720</td>
<td>68720 NSA Lancaster PA</td>
<td class="mono">74</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill warn">No</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td></td>
<td>R00=completed</td>
</tr>
<tr>
<td class="mono">68724</td>
<td>68724 NSA York PA</td>
<td class="mono">75</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed</td>
</tr>
<tr>
<td class="mono">68752</td>
<td>68752 NSA Brownsville TX</td>
<td class="mono">76</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed</td>
</tr>
<tr>
<td class="mono">68753</td>
<td>68753 NSA Brownsville TX</td>
<td class="mono">77</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed</td>
</tr>
<tr>
<td class="mono">68758</td>
<td>68758 NSA Brownsville TX</td>
<td class="mono">81</td>
<td><span class="pill ok">Yes</span></td>
<td><span class="pill ok">INV-2026-006 (sent)</span></td>
<td><span class="pill neutral">No</span></td>
<td class="mono"></td>
<td><span class="pill neutral"></span></td>
<td></td>
<td>R00: INV-2026-006 (sent)</td>
<td>R00=completed/billed</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</body>
</html>
@@ -0,0 +1,340 @@
[
{
"site": "00286",
"dbTitle": "00286 Riverside, CA",
"internalTaskNumber": 31,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "not_started",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=not_started"
},
{
"site": "30055",
"dbTitle": "30055 Moreno Valley, CA",
"internalTaskNumber": 33,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "client_review",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=client_review"
},
{
"site": "30062",
"dbTitle": "30062 Riverside, CA",
"internalTaskNumber": 34,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed"
},
{
"site": "30094",
"dbTitle": "30094 Colorado Springs, CO",
"internalTaskNumber": 35,
"r00Completed": "Yes",
"r00Billed": "No",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "—",
"versionSummary": "R00=completed"
},
{
"site": "30096",
"dbTitle": "30096 Eerie, CO",
"internalTaskNumber": 36,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R02",
"activeStatus": "not_started",
"completedRevisions": "R01 (unbilled)",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=completed; R02=not_started"
},
{
"site": "30258",
"dbTitle": "30258 Lebanon, NH",
"internalTaskNumber": 38,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed"
},
{
"site": "30259",
"dbTitle": "30259 Hamburg, NJ",
"internalTaskNumber": 39,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed"
},
{
"site": "30261",
"dbTitle": "30261 NSA Clovis NM",
"internalTaskNumber": 70,
"r00Completed": "Yes",
"r00Billed": "No",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "—",
"versionSummary": "R00=completed"
},
{
"site": "68639",
"dbTitle": "68639 NSA Los Lunas NM",
"internalTaskNumber": 40,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "client_review",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=client_review"
},
{
"site": "68663",
"dbTitle": "68663 NSA Oklahoma City OK",
"internalTaskNumber": 41,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "not_started",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=not_started"
},
{
"site": "68664",
"dbTitle": "68664 NSA Oklahoma City OK",
"internalTaskNumber": 42,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "not_started",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=not_started"
},
{
"site": "68666",
"dbTitle": "68666 NSA Oklahoma City, OK",
"internalTaskNumber": 43,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "not_started",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=not_started"
},
{
"site": "68669",
"dbTitle": "68669 NSA Oklahoma City OK",
"internalTaskNumber": 44,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "not_started",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=not_started"
},
{
"site": "68670",
"dbTitle": "68670 NSA Oklahoma City OK",
"internalTaskNumber": 45,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R02",
"activeStatus": "not_started",
"completedRevisions": "R01 (billed)",
"billedVersions": "R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=completed/billed; R02=not_started"
},
{
"site": "68671",
"dbTitle": "68671 NSA Oklahoma City OK",
"internalTaskNumber": 46,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "not_started",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=not_started"
},
{
"site": "68673",
"dbTitle": "68673 NSA Oklahoma City OK",
"internalTaskNumber": 47,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "client_review",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=client_review"
},
{
"site": "68720",
"dbTitle": "68720 NSA Lancaster PA",
"internalTaskNumber": 74,
"r00Completed": "Yes",
"r00Billed": "No",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "—",
"versionSummary": "R00=completed"
},
{
"site": "68721",
"dbTitle": "68721 NSA Mechanicsburg PA",
"internalTaskNumber": 48,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R02",
"activeStatus": "client_approved",
"completedRevisions": "R01 (billed), R02 (unbilled)",
"billedVersions": "R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=completed/billed; R02=completed"
},
{
"site": "68724",
"dbTitle": "68724 NSA York PA",
"internalTaskNumber": 75,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed"
},
{
"site": "68752",
"dbTitle": "68752 NSA Brownsville TX",
"internalTaskNumber": 76,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed"
},
{
"site": "68753",
"dbTitle": "68753 NSA Brownsville TX",
"internalTaskNumber": 77,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed"
},
{
"site": "68758",
"dbTitle": "68758 NSA Brownsville TX",
"internalTaskNumber": 81,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "No",
"activeRevision": "—",
"activeStatus": "—",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed"
},
{
"site": "68808",
"dbTitle": "68808 Camas, WA",
"internalTaskNumber": 49,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R01",
"activeStatus": "not_started",
"completedRevisions": "—",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=not_started"
},
{
"site": "68809",
"dbTitle": "68809 Centralia, WA",
"internalTaskNumber": 50,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R02",
"activeStatus": "not_started",
"completedRevisions": "R01 (billed)",
"billedVersions": "R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=completed/billed; R02=not_started"
},
{
"site": "68810",
"dbTitle": "68810 Chehalis, WA",
"internalTaskNumber": 51,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R02",
"activeStatus": "not_started",
"completedRevisions": "R01 (billed)",
"billedVersions": "R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=completed/billed; R02=not_started"
},
{
"site": "68811",
"dbTitle": "68811 Kelso, WA",
"internalTaskNumber": 52,
"r00Completed": "Yes",
"r00Billed": "INV-2026-006 (sent)",
"hasActiveRevision": "Yes",
"activeRevision": "R02",
"activeStatus": "not_started",
"completedRevisions": "R01 (unbilled)",
"billedVersions": "R00: INV-2026-006 (sent)",
"versionSummary": "R00=completed/billed; R01=completed; R02=not_started"
}
]
@@ -0,0 +1,32 @@
# Checked Sites Revision Audit
Definition used: your checkbox means `R00` was completed at some point, not that the task has no active revisions now.
| Site # | DB Title | Internal # | R00 Completed | R00 Billed | Active Revision? | Active R# | Active Status | Completed Revisions | Billed Versions | Version Summary |
|---|---|---:|---|---|---|---|---|---|---|---|
| 00286 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 30055 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 30062 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 30094 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 30096 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 30258 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 30259 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 30261 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68639 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68663 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68664 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68666 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68669 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68670 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68671 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68673 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68720 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68721 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68724 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68752 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68753 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68758 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68808 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68809 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68810 | — | — | — | — | — | — | — | — | — | Missing from DB |
| 68811 | — | — | — | — | — | — | — | — | — | Missing from DB |
@@ -0,0 +1,118 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 1008 612 ] /Parent 10 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 1008 612 ] /Parent 10 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 1008 612 ] /Parent 10 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/PageMode /UseNone /Pages 10 0 R /Type /Catalog
>>
endobj
9 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260604232324-04'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260604232324-04'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
10 0 obj
<<
/Count 3 /Kids [ 5 0 R 6 0 R 7 0 R ] /Type /Pages
>>
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3045
>>
stream
Gb"/+D/\/u')q<+0u0K5Ru<Y^j)TE65PQfn?X::/'c.)l-to$RfLSt:U20P]hs;:rfqKnF)^CH.(Mg@hS=(DC4O<@l$jD6tWVJQE!:16Sh0TKn_;HP7&IB;E^sQQQIlgLW8YQ>UWJF-Gn.U9a!OJ8ddL%-cJ]''O3IH4\'+'Oti&n.FVg'4@^[uoO,W.VRUmqq]VTt]2n44fR0EQk5m/EI0j?UJ8_4NE+USn*G#5Pf]5qE\V?5pFQ.BOE0[+KTkRBqHZ$[1F"IH^]Q"M^Q$#C@i&pYkUdYb:QD;Qc9KmcC]0L%9o90gsMJ+O]-,oSG$6S&0ieKc+gq*fT[KVu.IYM^k>d5Q@dO(Xj#jDQet5V,8dL@['F4c29q-CDA_@5nCrSWkkX$j6ce-SQ/M2nuS:trUb7;6Z)39Rsfm)%[W.E4s=m#mGW0XH(7t3oipbt:lY#Yk5"1VQK_s\UTB'.ZfqUqMXe2)D]gR6D+@X`6^cin1WY;NnQ1`SXUF8'**7Z&>j(_<VF25#;j_*fEYsgp]W?EcPZF@"ME,u7cfTqI<'fYp[sKpVb$c?nntf0)M(cF6,r'gB3)a9K`TiEP(J3QYMXR_=S"bQc],Ku=%4c;eB1sDI-VcAL-%:l*;&[Cj#T0I=![_?m9FIa-9`r:JC`8J(M>dPo)]AE*e<?`#AhV1On["(dIV:JPa,-9oIhHP?&4Z0d)^opEbOLTT2Ak$Qe0hu]M3df'djG%uIYlb89Nu)%_ktSkNt-V&3e[+b_9mPorKJ3n2G-[:-o%RG`oIIAI)G&6Ytm0PRl(i3XT(8n7H`ue)<Gh.iEaOe\P8,3lS&@Z`Oo@iaT')kJ*o_Y#+e)U-r'1X77f$F6<F17*.kd3B:!:RaqBdEh8V0]7?hbM3$'Nf+GgpoiFdd]kbA`BQ)H*n_F=omQu:Vn)!+5OP;q>XYEdb4?3eFgVOWN)9^@p_njtFi:A<2=W+Ej54KF&r'L/s]U,+&iN,UinZUi8Hn:MCeTFCE0#uiS*$J/Ei`@oPKVo2s'eNA!#@am,u7dM)2HqB"E3uArE*'sVgiqWHHm.AJR%"/GDnKl2fW4BF.hSCU-[`qFj]7!$Gq$gS8/%CD&i>*Om\4Gkc9'?>d,gscL%g+G@XMH2;mdjna$jSDAoiM_h2[1)/Bdt_;(L8NZ+.o&C*NmP1)PhQ8iEug6B.[B!)s-9b?,9o/hn,=P5J_2>QjQ"ritgYN^+RNMjdoho1tX($!GiD=!,N;4ic+)`ZN1.A3#2tY2*RM"`30Pb9[;UPH2oi0I/l0`<rX2EWRKQMnJ?I.:YkUj#*V%C[TE="Q&Gp:&#Yt;';gWnLc0#a0c"Tt,4tuQ[*7Lo<`_-n=DWQcr7b!2eJ0mT4[%NXbE$g]6=2P:\W;)P@rg8Uh=!ciJj!1/qa7Gba%flUGZ`"7C8;;`ljcM\*:%ji]e/L=f&T>4]!6$W@?s1@%c7,?S)4NGB9:q\7AUOHg`bFpj"nuQ*(<c":O@fencCYR<.Z;,X3D^[8'$T%n.tm-gn-=!c8-p+-$3@UMNk@A$G9l0Y?2?-;mi;iMHRJn,T=Fca>@0S;9B=Oij?`BCd9dMOT7M=6baZ?=[YV+3o#KV;2s=q;'hqrPd]VXb%G9DL4?Sq6e"HSLJas*oaoTZ?!0dO.bcm5frC(Q'FQa`CoE@H5?'A9;A]B3G*hb6EB_`!*>"iE=`q=XKr#3Tn\@?j=TeV4>q-*-C[WQSjKmAt-*h6`r,Bg:r,sj;B(r>U9S"1t@?DEfSVV1rFjCQ.d7XaD.uqh)hOqjt+3h'H2cV5H)WO:3oJ)YN`RdYV#CE!Q'QhHeG!Sr17SlQaM(B5H?)^0/;FQ\:FeP73Tc,BWmjPRI/='gY^7ga^d!oL=RqOG-Iq(=CM0tK^^22C)Y:5T3D*6BdS>enpZNf$cqqo&HcJIPW)rQR5@.d0>5br9@M9[dME%1+)(hm,G`+$jd;N=O+HAjQ2,G^>i/5%F1VHiQ7o<Pt*IUU>ICKn']gn:q$-geb1^p^4.Q"5,$,/t6-;]QjW5X;N*-Zp$Z^(%=)D5eS19oH55_^:@P7;G-IY3q?rQ-H=`Y^-[5g/'p\%UA!k]&9)sGipL(?\qmf%pAa9YUmqg`)Rc'#I`d8q@W`h]>soETl`VN>]Q0sYlWuP5\MDgNk<pD227"jUChsCcLPu_(E'gCIP""p]Bo7_(<6]jmVQM<dd[L6m:1gBZ*OWZ1r84ds2C744Ka=3@0D2]e)5-"[[Ie3KWQ+M(tGANU\t8@+8Xb:^T3p90W0mDJtg(BS:FT7'lcQJCH=F`l$ZaP$<5EWXH7Vm&[]QNXH6%6b%HFb+.RJY6/^k5YLToZ<IA$(J0s9@YsI$pknDXa##4T'h>SNJ"sDd=jmHu"EGu'o>@*uEL(F-q-ng7^VMc8gkRW1U\E;@n$5G8c28H%o,U$.&!&5DNoR%0EVK4`-/[II-rmJn'6!#X/(K\OcCUldYf>\OkA#F\:n/utaDs%ZI%+a&k:p.MZL'VTiW1=$M_ifUVnQco'$=q%&=QKg`cu?kTVf=V`:,4dWWof4[B0UV26Vn#DY%J1Lgfqr47(kL-#T>d?n3\3YgUEM7=2$4IKS6&^ZU8,9JimgF/2/r;%VeO:=Cr,j@YUQ!([^AgQ5Gk=<aJ>V=%3Q`"/hV'lH%X>;Gc^k3?][I4IZrb)\[l:6lMF)\fNZs1b9lmVi:+g"%ij1IaRg&3i:Y($)6GhNNJt/TRkQd$$G<F*@7@'?^)0i!mn8&+g<[e."TOVPgK)"6&&s(oQd?)+]g>NpIG3KoX5B!a*TpQ`s4ojArVEf[B4h,NdO\f#`([jVLu+]4Gs["IF6NX]f'l6?0V&"cJAMX2K>M#a3Gf'k$mqb^;*P86M-aco`a!Nqp?F,5IQ!TI]^lhlZV*F`n11O?.8EAS[H$NgV,mHlY^P8?J2DZ2@JWZm_u'jNMU5F]cu3GgcuOLGt&=TAi%FdEA"Q!#9uk9&^tJa\Bo/SlOhlt0VVOBm"f@5,=3#%@U%)J/E;OH]61.lOTst?Li)r"qh?Qdr#aH1lVR~>endstream
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2064
>>
stream
Gb!l"D/\/e&H;*)EPT;u7AKEZ</5p9dWOd-od)=R$j]\>WS4kgS^)CV%+pWunO97D=Jh'sl"8O\6LcHFn*]^/i[LZgn3cThZO+G+!$4>!5T'to!>U<=_+XQ;d/1.N</QoD5Z%hZ.ZM45-P8'0gmblH#go=7c<1UKme7&SnI?PZ[GVbfj7])_n!-jQ#:[%3J_eg/V\6WRAHe%.?ip9c_p:3EHSG3rKDT9E>?ju\(A6Z$9GdkJ\,#f!ENVS@Kk;=oAmrjbLh[-e9%g;ZnnYC#-:MK^XpAh2@>a$bAm2o`>/mQW6e5%kmY^bG(:,hPQ$h>#d>*QMbLML%cc^5ZK\2+!!)m>ObQ<NfO=B+H\T"[+#n?B7"pba31!gCfB$UCtBMgFVU5]P>'$[9:kogI)MfH3]W^S(PBmW$Le?eCY$pSk!A)+RXO1Jgfi9A4fgGi5rXND#Cj)P%&;]V435ZcAce^9ZYSt%B%=2D9CEOSSd,qtD@Z-m??PJqK\3UbHGPfflnk[O+*>VGB'E*Qb0k+diK[pE4cJn<Br@L;Mgfm92]Q:7tFLK=[-Q]oO1p/e^u@(Ig[YE<j!@hK,`)c.A0j7K4g_X#/<<_uRhh!.mCPpDK1(p_^nqC"-&33`cg(X+Co;'ES5Glk2L(_=qG-:9a2/V:h;f.U#..B`i,9I(lad7,"K;B][D6#!<k@/-tA.ldO=L[4ce/$uo@.Z$W8r9_L.E]Gg9";7Ua%C7&2\!jn*]\u]I49q>'1d@B5NL/d9L,3+-*&73orSQpkeUqF18,?^V!Stc&(:AHU9(XN1D,W>eBr&Y4Jk<p";kCaMGJFJ_#DB%q-H\'uEQDjD?;DB8pQeLU=I6uZ"6[YK0k#pg)o3c+@Mk,WYu4,-^ekD3mf>P97t?'l?1PGPf3m&^fnQ`Ik;Z1g+/]p5p[c@C[YXQuYV`(!`uA+&D-b8qd_[`4E8(>"1N/4d8"+'_DloJ3dnLn".`;LL_;BbNHkcG3KAej&4*VtLU*$!b"SpD)m`5)r5j2B#Hb:QhX2mc/Jn65IRN`\'[rkW,DWpKO-E@N-9$rVHU]!8E3';Wpp4dUniWm6N.EZ?*B_P%fW,.]>MM@p_->tY3'2=?OZEGXl+WI]iIS]O?"Ac9*@C]Nmhi9"g\j1?>e$B0hT&!ra,57-EZJoL!Q=pBA=22;sQs]t\,"iL>TLnq(EU-c%A$QQ;K@gseP@rnfHLh$9^)GPZJif==FR]LqGKLSEe'[Q.,5Z[aH];$f>/:nPPH7GTZIF>TWi`b0l^7fS>,M)&5!'i]7q_E(Qa>#S)/l+BQm/^AKMF3)eT?#'JsL6AQm+24dNu!-!4p8g[f]IC"MN(s_lmE[(K[`0`R:[p`lOYtQKqZaT]KepqrMq;8f*asQJTB+SL_;s\#sciB?/kOCkNZ]kGo:k]!JYS,H`d;9s$lE)GZ%Y#OkrdH*WkhWYG&D]sGAhWdSBJXOp,5:Ek%,Y(i3##AgV+TkqoHftY*Wah=,o'eDt%AgZ`eNh-O_cccM,*23CF_sqEM&K>7<=a'fWJS,(jZb);km2HpRL!Bm,&@/Vq2"IMpXkof<X_U>^Irga#c$/Xe?0*Y`Bq"mF/Oa^N$70Y5?CrIECp_=*C9?iO9Q[A1G.>%.pTM#j'g-2:Bq]2&La?ODfSW](s/oapH,Y!C-o?"uMPaaTjP?k5$8:]7r*lFL8krl-G=lOTQg?2mls7R[Do!q%RUfKf,jUfSEQQ5h5Q4<0X#hn;"fZj<Z(;b\_>djr-/gV($&(8;_hF@.2&YIh6saqZE8hD=RJ%Im_O85sQT'?2oq97rpq,n6lSH%d&gcKhdcr-e%iaD09!@+pPs+/>A\K27cr/UCBi`kk"=`@V/C:l$9T(<?m3(FU(<rYh*G&EDo1VpAg-I&"#r:BJ19%'p)"L_[G*".s(sL-M]B+3['XR=f]*^<?"uF?e<?lQt/K:+i$8T_u"P]WB`Sd,,dpKP].bcb!hMV;1`(5hgLntUoBgjPn>_GY,<Ku/2O;Or>ER>pa:/PJ16Xl:3`'oF!(q#H;i^.+-K#C*3`;"T!>Pj!.5En[d"o~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2151
>>
stream
Gb!lc>BA7Q'Z],&.F-/O'S[N3XP;%e""h[t"'oRrlnZIO851$o'@q0XiS3g`I#15Mf^9`H-FBV7-!C7hUnaP%fpDlZD]X@gY5jT;nG!k+/n"G\GT=[VHj,!0#QG5WVjXeE6nVNa=o=2m=UaQ6@$WagQj&I^V?qWr3/D%(B4*l:A'+7')8:Ls6Gs$:(>2"VZ[#u>Me*SA#D0e]$4bpO/eX6V@/(Uo%t3!;BM_hj(8/%DhAaS5&P>@-6NSHMZK-]fYFLllfuMDAE#"<;-6+"4-K?c$3LigLA-)^(Z:d0F_io'u\Ok^9\X=uqo@]!9hqJb/_N(bDYLueHKa;9!09\g+,OEG:14aFRe05`p?&gZW?`.TnbGO1THk*K.#+$PkE!3N.2G%>AgRAuP7U$=f_;KVl%O7!i?n,'b0AL3+_^m+OI*4?9`h=s(HDQL<[T5h_iS]TbE&]JTo3p^IW6We*H&`H'hUHKqq_lXNg)2D8Eaq#+DJtc?\D:2GIFHMCkI-k[FjC9RH>g&B`lf6Uh,B#5R,hfiBY\f7!9b?FU7>1@T@b<SrEO)r5V40T-h[*;L6HH-/B9=LdHS?3AP"p%:#WQN73VU0:^N^K##FEgAd=$"`,\A3kqkD='U4JFfV,CU+Q',bUUF,`3,S(cl!gG&Q)=fFca$Dk]kHQ)eqBL4o;f(D,X\l(.nd4S1-?1[b5%FA]-Q48Wg/G)AkO9PB@g3dU\%cs.L7QOgKs3V59X\ao-.LG=3.$b):P=65cE=:Y?JJR,RQ]Be:U\Y-hR-qGm)/`g42TXE86$i@:_<j>]?NZj\R\p7RMie2j4_Hru+90D?l<KO!CR0eIqoEotqD`)#GE:=&H`H*'F^69hhc&_"UnO-70($@Et@'d3aQ)$>U'riS],^`TQ2p;$6`LBar\B\_?UX\72[DKs!Md'kaVaRn+qZ8nVJ#]K6g$kX,c@dOQqiaHomj*%^U%m_aYlVk%Ml3Ti&4r-T'mZuEJq6%(gtg[F-uCDUpW@A0H-kLD+JX;*RRS9!2!mBdkj_XQZolR'],s)>>J&Qe/Q@8fRr`D?Zh.G[L(rYUt0g>e7K0,dYJG-lpHc-.[DO-dn?KQf-jc_pAVKHJ"T^8@+I`<&@*G:VlkrDmd.=)<p`?ZXLHAMKYDXRIs#8L8>/.c25W,`LWcaK,,sQ=c-SX57]2Z`A."dYq783ghXL>:F7f3bb++!ICk(G8TOsPIq:MJ!O?UCI5QH?>fp'G.Jh"[8=:@e>V#@Q/%73<.n>pa3e]eHNY_B>7"$iE3m"WTnV<,5'-J!?&"Q_:)@YHdBga7-CsU2W6P!`\d0[+^ZT/fGES?]Y(c:MOd>DFSr^$<?DeT1pQo`=-[*#QgRR]emHrF_[k2g8p5\RJZ."9lbNRVb]$nt!%Wpl7hBTV\]&/kBRaKPho/=0q7S@sMiT0>3*)G`VP6IJhDWNsZbD*Fp?&QDXHf`tqmB+aT8/I:f?+Iim-_8pr3<&LP$WN;sG26NadLpHiBkp*mZO0/p9BKg4$\WsdRZ0gq/4nepleP<>T2"(r:+kB%DM$K2gH/5q`^ptCf/du.;,'$d]7B`jaVC0iPj8`lXpD3J4>bg\<dI3G`@*,=9Y[3t?b-f;e7$gLRhEQJ[sVe3h4`ZXD%b:LksFbeH?P\/V";8-[FotS&r"p8T%bMkd)'(r4`"d8/_cNrCd1PT\$O&5G8H#6[NV6lCSP)>DrZ6L]_U*L2'`X$%X>0DB6WT83oN09h=UWpoL%;,l?lC7a,V#(Z3BX#,=];X"56>[2U0((UL!D<7=0F6D;/>b4)*Vp5^O5,]8Lj\$/YdNZKEH_58<L\cM,&`8W<AOk7Tf:F#lVV%VR6_g,@j6/bWcLp=[AkmE%cE\EU:HZhr&pP0We3AA:nN-'J,]&aV1@hrd:N=":Ks$`I]YV"fJF(bj-7&6otM\NN0G_HHCEMDCg+jKXtrAW?@@nDLbK&qdJVIT/4m^T=(X$V+3"/=.ZMY4DBdcDSj5rrRd-hl!OQn</@&,TKfq;CkHp?044MARi$ie[ioBA=4rZc2/\dBtr#?47cFbbIFg+!V1q5Z<f"+.loL_Xd%MK/.8G'R>b.12k$XFb,iQ*%qQna08h\<b48NF9]:5P_@*!9Qg$b&?^@UUAT,IF%/pL(+CR2~>endstream
endobj
xref
0 14
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000632 00000 n
0000000828 00000 n
0000001024 00000 n
0000001093 00000 n
0000001373 00000 n
0000001445 00000 n
0000004582 00000 n
0000006738 00000 n
trailer
<<
/ID
[<dccedbca14e5931c52cd86c3e6456193><dccedbca14e5931c52cd86c3e6456193>]
% ReportLab generated PDF document -- digest (opensource)
/Info 9 0 R
/Root 8 0 R
/Size 14
>>
startxref
8981
%%EOF
+339
View File
@@ -0,0 +1,339 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fourge Portal Finance Summary</title>
<style>
:root {
--bg: #ffffff;
--text: #161616;
--muted: #5e5e5e;
--accent: #f5a523;
--border: #e5dccf;
--surface: #faf7f2;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.45;
}
.page {
width: 100%;
max-width: 920px;
margin: 0 auto;
padding: 44px 40px 56px;
}
.hero {
border: 1px solid var(--border);
background: linear-gradient(135deg, #fffaf2 0%, #ffffff 55%, #fff5e2 100%);
border-radius: 16px;
padding: 28px 30px;
margin-bottom: 28px;
}
h1 {
margin: 0 0 8px;
font-size: 30px;
line-height: 1.1;
font-weight: 700;
letter-spacing: -0.03em;
}
.sub {
margin: 0;
color: var(--muted);
font-size: 14px;
}
h2 {
margin: 28px 0 10px;
font-size: 19px;
line-height: 1.15;
}
h3 {
margin: 0 0 8px;
font-size: 15px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #7d6d58;
}
p { margin: 0 0 12px; }
ul {
margin: 0;
padding-left: 18px;
}
li { margin: 0 0 6px; }
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-top: 14px;
}
.card {
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
padding: 16px 16px 14px;
}
.card p:last-child,
.flow-box p:last-child { margin-bottom: 0; }
.flow {
display: grid;
gap: 10px;
margin-top: 12px;
}
.flow-box {
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 16px;
background: #fff;
position: relative;
}
.flow-box + .flow-box::before {
content: "↓";
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
color: var(--accent);
font-weight: 700;
}
.pill {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
background: #fff4dd;
border: 1px solid #f3d194;
font-size: 12px;
margin: 0 6px 6px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 13px;
}
th, td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
th {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 700;
}
.code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 14px;
white-space: pre-wrap;
}
.footer {
margin-top: 28px;
color: var(--muted);
font-size: 12px;
}
@media print {
.page { padding: 24px 24px 36px; }
.hero, .card, .flow-box { break-inside: avoid; }
h2, h3 { break-after: avoid; }
table { break-inside: avoid; }
}
</style>
</head>
<body>
<main class="page">
<section class="hero">
<h1>Fourge Portal Finance Summary</h1>
<p class="sub">Role cheat sheet, finance lanes, and data flow for team, client, and subcontractor users.</p>
</section>
<section>
<h2>1. Executive Summary</h2>
<p>The finance area is not one shared page for all roles. It is split into three lanes with different permissions and different page sets.</p>
<div class="grid">
<article class="card">
<h3>Team</h3>
<p>Full finance hub at <strong>/invoices</strong>.</p>
<p>Handles client invoices, expenses, subcontractor invoices, and subcontractor purchase orders.</p>
</article>
<article class="card">
<h3>Client</h3>
<p>Invoice-only lane at <strong>/my-invoices</strong>.</p>
<p>Clients can view invoices, filter by company, and download PDFs.</p>
</article>
<article class="card">
<h3>Subcontractor</h3>
<p>Two finance lanes: <strong>/my-invoices-sub</strong> and <strong>/my-purchase-orders</strong>.</p>
<p>They can submit invoices to Fourge and approve POs sent by Fourge.</p>
</article>
</div>
</section>
<section>
<h2>2. Role-by-Role Cheat Sheet</h2>
<table>
<thead>
<tr>
<th>Role</th>
<th>Main Routes</th>
<th>Can Do</th>
<th>Cannot Do</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Team</strong></td>
<td>/invoices</td>
<td>Create/send client invoices, track expenses, create POs, review subcontractor invoices, mark paid, export/send PDFs</td>
<td>Not limited in finance UI</td>
</tr>
<tr>
<td><strong>Client</strong></td>
<td>/my-invoices</td>
<td>View invoices, filter by company, download invoice PDFs</td>
<td>No expenses, no invoice editing, no POs, no subcontractor finance tools</td>
</tr>
<tr>
<td><strong>Subcontractor / External</strong></td>
<td>/my-invoices-sub<br>/my-purchase-orders</td>
<td>Create/submit invoices to Fourge, view status, download receipts after payment, approve POs</td>
<td>No team finance overview, no client invoice management, no internal expenses</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>3. Core Data Models</h2>
<div class="card">
<div class="pill">invoices</div>
<div class="pill">invoice_items</div>
<div class="pill">expenses</div>
<div class="pill">subcontractor_invoices</div>
<div class="pill">subcontractor_invoice_items</div>
<div class="pill">subcontractor_payments</div>
<div class="pill">subcontractor_po_items</div>
<div class="pill">tasks</div>
<div class="pill">submissions</div>
<div class="pill">companies</div>
<div class="pill">profiles</div>
<p style="margin-top:10px;">Simple mental model: <strong>invoices</strong> bill clients, <strong>expenses</strong> track internal costs, <strong>subcontractor_invoices</strong> let subcontractors bill Fourge, and <strong>subcontractor_payments</strong> are POs Fourge sends to subcontractors.</p>
</div>
</section>
<section>
<h2>4. System Flow</h2>
<div class="flow">
<div class="flow-box">
<h3>Client Billing Flow</h3>
<p>Approved client work reaches <strong>tasks.status = client_approved</strong>.</p>
<p>Team finance loads uninvoiced tasks and revisions, builds line items, saves an invoice, and emails the PDF to the client.</p>
<p>Later, team marks the invoice paid.</p>
</div>
<div class="flow-box">
<h3>Expense Flow</h3>
<p>Team logs expenses, optionally with receipt files.</p>
<p>Expenses feed overview charts, yearly summaries, and profit calculations.</p>
</div>
<div class="flow-box">
<h3>Subcontractor Invoice Flow</h3>
<p>Subcontractor creates an invoice from completed approved tasks assigned to them.</p>
<p>Fourge reviews it, then marks it paid and sends a receipt PDF back.</p>
</div>
<div class="flow-box">
<h3>Purchase Order Flow</h3>
<p>Team creates a PO for a subcontractor, optionally tied to project tasks.</p>
<p>Subcontractor reviews the PO and can approve it from their own portal view.</p>
</div>
</div>
</section>
<section>
<h2>5. Status Logic</h2>
<div class="grid">
<article class="card">
<h3>Client Invoice</h3>
<p><strong>draft</strong><strong>sent</strong><strong>paid</strong></p>
</article>
<article class="card">
<h3>Subcontractor Invoice</h3>
<p><strong>draft</strong><strong>submitted</strong><strong>paid</strong></p>
</article>
<article class="card">
<h3>Purchase Order</h3>
<p><strong>draft</strong><strong>sent</strong><strong>approved</strong><strong>ready_to_pay</strong><strong>paid</strong></p>
<p><strong>cancelled</strong> is exit state.</p>
</article>
</div>
</section>
<section>
<h2>6. Key Business Rules</h2>
<ul>
<li>Client invoices only pull approved, uninvoiced work.</li>
<li>Tasks and revisions are marked invoiced after invoice creation.</li>
<li>Deleting a team invoice rolls linked invoice flags back.</li>
<li>Team invoice status changes also push task status changes.</li>
<li>Revision billing rule: <strong>R00 = new work</strong>, <strong>R01 = free</strong>, <strong>R02+ = billable increments</strong>.</li>
<li>Subcontractor invoices are created by the external user and then reviewed/paid by team.</li>
<li>POs are created by team and approved by the subcontractor.</li>
</ul>
</section>
<section>
<h2>7. Database Relationship Chart</h2>
<div class="code">COMPANIES
└─ invoices
└─ invoice_items
├─ task_id → tasks
└─ submission_id → submissions
PROJECTS
└─ tasks
└─ submissions
PROFILES (external)
├─ subcontractor_invoices
│ └─ subcontractor_invoice_items
│ └─ task_id → tasks
└─ subcontractor_payments (POs)
└─ subcontractor_po_items
└─ task_id → tasks
EXPENSES
└─ standalone internal finance records</div>
</section>
<section>
<h2>8. Quick Route Map</h2>
<table>
<thead>
<tr>
<th>Surface</th>
<th>Route</th>
<th>User</th>
</tr>
</thead>
<tbody>
<tr><td>Finance Hub</td><td>/invoices</td><td>Team</td></tr>
<tr><td>Client Invoices</td><td>/my-invoices</td><td>Client</td></tr>
<tr><td>Subcontractor Invoices</td><td>/my-invoices-sub</td><td>External</td></tr>
<tr><td>Create Subcontractor Invoice</td><td>/my-invoices-sub/new</td><td>External</td></tr>
<tr><td>Purchase Orders</td><td>/my-purchase-orders</td><td>External</td></tr>
</tbody>
</table>
</section>
<p class="footer">Generated from current portal codebase summary on June 3, 2026.</p>
</main>
</body>
</html>
Binary file not shown.
+379
View File
@@ -0,0 +1,379 @@
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
const docsDir = new URL('.', import.meta.url);
const inputPath = new URL('./checked-sites-revision-audit-2026-06-04.json', docsDir);
const outputPath = new URL('./checked-sites-revision-audit-2026-06-04.html', docsDir);
const rows = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
const existingRows = rows.filter((row) => row.exists !== false);
const activeRows = existingRows.filter((row) => row.hasActiveRevision === 'Yes');
const noActiveRows = existingRows.filter((row) => row.hasActiveRevision !== 'Yes');
const r00BilledRows = existingRows.filter((row) => row.r00Billed !== 'No');
const r00UnbilledRows = existingRows.filter((row) => row.r00Billed === 'No');
const activeStatusCounts = activeRows.reduce((acc, row) => {
const key = row.activeStatus || 'unknown';
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const revisionDepthCounts = activeRows.reduce((acc, row) => {
const key = row.activeRevision || '—';
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const maxStatusCount = Math.max(1, ...Object.values(activeStatusCounts));
const maxDepthCount = Math.max(1, ...Object.values(revisionDepthCounts));
const esc = (value) =>
String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
function badgeClassForStatus(status) {
if (status === 'client_review') return 'review';
if (status === 'client_approved') return 'approved';
if (status === 'not_started') return 'todo';
if (status === 'in_progress') return 'progress';
return 'neutral';
}
function barChart(entries, maxValue) {
return entries
.map(([label, count]) => {
const pct = Math.max(4, Math.round((count / maxValue) * 100));
return `
<div class="bar-row">
<div class="bar-label">${esc(label)}</div>
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
<div class="bar-value">${count}</div>
</div>
`;
})
.join('');
}
function renderTableRows(items) {
return items
.map((row) => {
const billedClass = row.r00Billed === 'No' ? 'warn' : 'ok';
const activeClass = badgeClassForStatus(row.activeStatus);
return `
<tr>
<td class="mono">${esc(row.site)}</td>
<td>${esc(row.dbTitle)}</td>
<td class="mono">${esc(row.internalTaskNumber)}</td>
<td><span class="pill ${row.r00Completed === 'Yes' ? 'ok' : 'neutral'}">${esc(row.r00Completed)}</span></td>
<td><span class="pill ${billedClass}">${esc(row.r00Billed)}</span></td>
<td><span class="pill ${row.hasActiveRevision === 'Yes' ? 'review' : 'neutral'}">${esc(row.hasActiveRevision)}</span></td>
<td class="mono">${esc(row.activeRevision)}</td>
<td><span class="pill ${activeClass}">${esc(row.activeStatus)}</span></td>
<td>${esc(row.completedRevisions)}</td>
<td>${esc(row.billedVersions)}</td>
<td>${esc(row.versionSummary)}</td>
</tr>
`;
})
.join('');
}
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checked Sites Revision Audit</title>
<style>
:root {
--bg: #f5f1e8;
--panel: rgba(255,255,255,0.78);
--panel-strong: rgba(255,255,255,0.92);
--border: rgba(43,37,28,0.12);
--text: #231d14;
--muted: #6b6255;
--accent: #b8831f;
--accent-soft: rgba(184,131,31,0.18);
--ok: #1f8a4c;
--ok-soft: rgba(31,138,76,0.12);
--warn: #b45309;
--warn-soft: rgba(180,83,9,0.14);
--review: #2563eb;
--review-soft: rgba(37,99,235,0.13);
--todo: #7c3aed;
--todo-soft: rgba(124,58,237,0.12);
--shadow: 0 18px 40px rgba(35,29,20,0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(184,131,31,0.14), transparent 28%),
radial-gradient(circle at top right, rgba(37,99,235,0.09), transparent 24%),
linear-gradient(180deg, #fbf8f2 0%, var(--bg) 100%);
}
.wrap {
width: min(1400px, calc(100vw - 40px));
margin: 32px auto 48px;
}
.hero, .panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 18px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: var(--shadow);
}
.hero {
padding: 28px 30px;
margin-bottom: 24px;
}
.eyebrow {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
margin-bottom: 10px;
}
h1 {
margin: 0 0 10px;
font-size: 34px;
line-height: 1.05;
letter-spacing: -0.04em;
}
.subtitle {
margin: 0;
color: var(--muted);
font-size: 15px;
max-width: 900px;
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.card {
padding: 20px 22px;
}
.label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
margin-bottom: 8px;
}
.value {
font-size: 34px;
line-height: 1;
letter-spacing: -0.05em;
}
.sub {
margin-top: 8px;
font-size: 13px;
color: var(--muted);
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.panel {
padding: 20px 22px;
}
h2 {
margin: 0 0 14px;
font-size: 18px;
letter-spacing: -0.03em;
}
.bar-row {
display: grid;
grid-template-columns: 150px 1fr 36px;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.bar-label, .bar-value {
font-size: 13px;
}
.bar-track {
height: 10px;
border-radius: 999px;
background: rgba(35,29,20,0.08);
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent), #d7a84b);
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th, td {
padding: 10px 12px;
border-bottom: 1px solid rgba(35,29,20,0.08);
vertical-align: top;
text-align: left;
font-size: 13px;
}
th {
position: sticky;
top: 0;
background: var(--panel-strong);
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 11px;
z-index: 1;
}
.mono {
font-variant-numeric: tabular-nums;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 72px;
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
border: 1px solid transparent;
white-space: nowrap;
}
.ok { color: var(--ok); background: var(--ok-soft); border-color: rgba(31,138,76,0.2); }
.warn { color: var(--warn); background: var(--warn-soft); border-color: rgba(180,83,9,0.2); }
.review { color: var(--review); background: var(--review-soft); border-color: rgba(37,99,235,0.2); }
.todo { color: var(--todo); background: var(--todo-soft); border-color: rgba(124,58,237,0.18); }
.neutral { color: var(--muted); background: rgba(35,29,20,0.06); border-color: rgba(35,29,20,0.08); }
.section {
margin-bottom: 24px;
}
.small {
color: var(--muted);
font-size: 13px;
}
@media (max-width: 1100px) {
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.two-col { grid-template-columns: 1fr; }
}
@media (max-width: 700px) {
.wrap { width: min(100vw - 20px, 1400px); margin: 20px auto 32px; }
.hero, .panel { padding: 18px; border-radius: 14px; }
.grid { grid-template-columns: 1fr; }
h1 { font-size: 28px; }
}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<div class="eyebrow">Fourge Portal Audit</div>
<h1>Checked Sites Revision Audit</h1>
<p class="subtitle">This report treats your checkmark as “R00 was completed,” then separates that from billing and from any newer active revisions. It is designed to make client-billing comparison easier at a glance.</p>
</section>
<section class="grid">
<div class="panel card">
<div class="label">Checked Sites</div>
<div class="value">${existingRows.length}</div>
<div class="sub">Sites found in the database from your checked list.</div>
</div>
<div class="panel card">
<div class="label">Active Revisions</div>
<div class="value">${activeRows.length}</div>
<div class="sub">Checked sites that now have an active R01+.</div>
</div>
<div class="panel card">
<div class="label">R00 Billed</div>
<div class="value">${r00BilledRows.length}</div>
<div class="sub">Checked sites where the new-book unit is already on an invoice.</div>
</div>
<div class="panel card">
<div class="label">R00 Unbilled</div>
<div class="value">${r00UnbilledRows.length}</div>
<div class="sub">Checked sites completed at R00 but not yet billed.</div>
</div>
</section>
<section class="two-col">
<div class="panel">
<h2>Active Revision Status</h2>
${barChart(Object.entries(activeStatusCounts), maxStatusCount)}
</div>
<div class="panel">
<h2>Active Revision Depth</h2>
${barChart(Object.entries(revisionDepthCounts), maxDepthCount)}
</div>
</section>
<section class="panel section">
<h2>Sites With Active Revisions</h2>
<p class="small">These are the ones where R00 may be done, but a newer version is still active or awaiting review/approval.</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px;">Site #</th>
<th style="width:240px;">DB Title</th>
<th style="width:80px;">Internal #</th>
<th style="width:96px;">R00 Done</th>
<th style="width:180px;">R00 Billed</th>
<th style="width:90px;">Active R#</th>
<th style="width:120px;">Active Status</th>
<th style="width:170px;">Completed Revisions</th>
<th style="width:220px;">Billed Versions</th>
<th>Version Summary</th>
</tr>
</thead>
<tbody>
${renderTableRows(activeRows)}
</tbody>
</table>
</div>
</section>
<section class="panel section">
<h2>Checked Sites With No Active Revision</h2>
<p class="small">These are simpler to read: R00 completed, and no current revision chain is active right now.</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px;">Site #</th>
<th style="width:240px;">DB Title</th>
<th style="width:80px;">Internal #</th>
<th style="width:96px;">R00 Done</th>
<th style="width:180px;">R00 Billed</th>
<th style="width:90px;">Active R#</th>
<th style="width:120px;">Active Status</th>
<th style="width:170px;">Completed Revisions</th>
<th style="width:220px;">Billed Versions</th>
<th>Version Summary</th>
</tr>
</thead>
<tbody>
${renderTableRows(noActiveRows)}
</tbody>
</table>
</div>
</section>
</div>
</body>
</html>`;
fs.writeFileSync(outputPath, html);
console.log(fileURLToPath(outputPath));
+362
View File
@@ -0,0 +1,362 @@
from pathlib import Path
import json
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import landscape, legal
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import (
BaseDocTemplate,
Frame,
LongTable,
PageBreak,
PageTemplate,
Paragraph,
Spacer,
Table,
TableStyle,
)
DOCS_DIR = Path(__file__).resolve().parent
INPUT_PATH = DOCS_DIR / "checked-sites-revision-audit-2026-06-04.json"
OUTPUT_PATH = DOCS_DIR / "checked-sites-revision-audit-2026-06-04.pdf"
BG = colors.HexColor("#F5F1E8")
TEXT = colors.HexColor("#231D14")
MUTED = colors.HexColor("#6B6255")
ACCENT = colors.HexColor("#B8831F")
ACCENT_SOFT = colors.HexColor("#F1E3C2")
OK = colors.HexColor("#1F8A4C")
OK_SOFT = colors.HexColor("#E5F4EB")
WARN = colors.HexColor("#B45309")
WARN_SOFT = colors.HexColor("#F7E7D7")
REVIEW = colors.HexColor("#2563EB")
REVIEW_SOFT = colors.HexColor("#E6EEFF")
TODO = colors.HexColor("#7C3AED")
TODO_SOFT = colors.HexColor("#F0E8FF")
LINE = colors.HexColor("#D7CFC1")
def page_template(canvas, doc):
canvas.saveState()
canvas.setFillColor(BG)
canvas.rect(0, 0, doc.pagesize[0], doc.pagesize[1], fill=1, stroke=0)
canvas.setFillColor(MUTED)
canvas.setFont("Helvetica", 9)
canvas.drawRightString(doc.pagesize[0] - 36, 20, f"Page {doc.page}")
canvas.restoreState()
def pill(text, fg, bg):
return Paragraph(
(
f'<para alignment="center" backColor="{bg}" borderColor="{fg}" '
f'borderWidth="0.7" borderPadding="4" textColor="{fg}"><b>{escape(text)}</b></para>'
),
styles["pill"],
)
def escape(value):
return (
str(value or "")
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
def status_pill(value):
value = value or ""
if value == "client_review":
return pill(value, REVIEW, REVIEW_SOFT)
if value == "client_approved":
return pill(value, OK, OK_SOFT)
if value == "not_started":
return pill(value, TODO, TODO_SOFT)
if value == "in_progress":
return pill(value, ACCENT, ACCENT_SOFT)
if value == "Yes":
return pill(value, OK, OK_SOFT)
if value == "No":
return pill(value, WARN, WARN_SOFT)
return pill(value, MUTED, colors.white)
def text_para(value, style_name="cell"):
return Paragraph(escape(value), styles[style_name])
def build_summary_cards(rows):
existing_rows = [row for row in rows if row.get("exists", True) is not False]
active_rows = [row for row in existing_rows if row.get("hasActiveRevision") == "Yes"]
billed_rows = [row for row in existing_rows if row.get("r00Billed") != "No"]
unbilled_rows = [row for row in existing_rows if row.get("r00Billed") == "No"]
cards = [
("Checked Sites", str(len(existing_rows)), "Sites found in the database from your checked list."),
("Active Revisions", str(len(active_rows)), "Checked sites that now have an active R01+."),
("R00 Billed", str(len(billed_rows)), "Checked sites where the new-book unit is already billed."),
("R00 Unbilled", str(len(unbilled_rows)), "Checked sites where R00 is done but not billed yet."),
]
card_rows = []
for label, value, subtext in cards:
card_rows.append(
Table(
[
[Paragraph(label.upper(), styles["card_label"])],
[Paragraph(value, styles["card_value"])],
[Paragraph(subtext, styles["card_sub"])],
],
colWidths=[3.0 * inch],
style=TableStyle(
[
("BACKGROUND", (0, 0), (-1, -1), colors.white),
("BOX", (0, 0), (-1, -1), 0.8, LINE),
("INNERPADDING", (0, 0), (-1, -1), 10),
("ROUNDEDCORNERS", [10, 10, 10, 10]),
]
),
)
)
return Table([card_rows], colWidths=[3.1 * inch] * 4, hAlign="LEFT")
def build_table(rows, title, intro):
flow = [Paragraph(title, styles["section_title"]), Paragraph(intro, styles["small"]), Spacer(1, 10)]
header = [
text_para("Site", "th"),
text_para("DB Title", "th"),
text_para("Internal #", "th"),
text_para("R00 Done", "th"),
text_para("R00 Billed", "th"),
text_para("Active R#", "th"),
text_para("Active Status", "th"),
text_para("Completed Revisions", "th"),
text_para("Billed Versions", "th"),
text_para("Version Summary", "th"),
]
data = [header]
for row in rows:
data.append(
[
text_para(row.get("site", ""), "cell_mono"),
text_para(row.get("dbTitle", "")),
text_para(row.get("internalTaskNumber", ""), "cell_mono"),
status_pill(row.get("r00Completed", "No")),
status_pill(row.get("r00Billed", "No")),
text_para(row.get("activeRevision", ""), "cell_mono"),
status_pill(row.get("activeStatus", "")),
text_para(row.get("completedRevisions", "")),
text_para(row.get("billedVersions", "")),
text_para(row.get("versionSummary", "")),
]
)
table = LongTable(
data,
colWidths=[
0.78 * inch,
1.7 * inch,
0.72 * inch,
0.75 * inch,
0.85 * inch,
0.6 * inch,
1.0 * inch,
1.45 * inch,
1.2 * inch,
2.15 * inch,
],
repeatRows=1,
)
table.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.white),
("BOX", (0, 0), (-1, -1), 0.8, LINE),
("INNERGRID", (0, 0), (-1, -1), 0.4, LINE),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#FBF8F2")]),
]
)
)
flow.append(table)
return flow
styles = getSampleStyleSheet()
styles.add(
ParagraphStyle(
name="title_main",
fontName="Helvetica-Bold",
fontSize=24,
leading=28,
textColor=TEXT,
alignment=TA_LEFT,
)
)
styles.add(
ParagraphStyle(
name="eyebrow",
fontName="Helvetica-Bold",
fontSize=9,
leading=11,
textColor=MUTED,
)
)
styles.add(
ParagraphStyle(
name="subtitle",
fontName="Helvetica",
fontSize=11,
leading=15,
textColor=MUTED,
)
)
styles.add(
ParagraphStyle(
name="section_title",
fontName="Helvetica-Bold",
fontSize=15,
leading=18,
textColor=TEXT,
spaceAfter=2,
)
)
styles.add(
ParagraphStyle(
name="small",
fontName="Helvetica",
fontSize=9,
leading=12,
textColor=MUTED,
)
)
styles.add(
ParagraphStyle(
name="card_label",
fontName="Helvetica-Bold",
fontSize=8,
leading=10,
textColor=MUTED,
)
)
styles.add(
ParagraphStyle(
name="card_value",
fontName="Helvetica-Bold",
fontSize=24,
leading=26,
textColor=TEXT,
)
)
styles.add(
ParagraphStyle(
name="card_sub",
fontName="Helvetica",
fontSize=8.5,
leading=11,
textColor=MUTED,
)
)
styles.add(
ParagraphStyle(
name="th",
fontName="Helvetica-Bold",
fontSize=8,
leading=10,
textColor=MUTED,
)
)
styles.add(
ParagraphStyle(
name="cell",
fontName="Helvetica",
fontSize=8.2,
leading=10.5,
textColor=TEXT,
)
)
styles.add(
ParagraphStyle(
name="cell_mono",
fontName="Courier",
fontSize=8.1,
leading=10.3,
textColor=TEXT,
)
)
styles.add(
ParagraphStyle(
name="pill",
fontName="Helvetica-Bold",
fontSize=7.5,
leading=9,
alignment=1,
)
)
def main():
rows = json.loads(INPUT_PATH.read_text())
existing_rows = [row for row in rows if row.get("exists", True) is not False]
active_rows = [row for row in existing_rows if row.get("hasActiveRevision") == "Yes"]
no_active_rows = [row for row in existing_rows if row.get("hasActiveRevision") != "Yes"]
doc = BaseDocTemplate(
str(OUTPUT_PATH),
pagesize=landscape(legal),
leftMargin=36,
rightMargin=36,
topMargin=28,
bottomMargin=28,
)
frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="main")
doc.addPageTemplates([PageTemplate(id="default", frames=[frame], onPage=page_template)])
story = [
Paragraph("FOURGE PORTAL AUDIT", styles["eyebrow"]),
Spacer(1, 4),
Paragraph("Checked Sites Revision Audit", styles["title_main"]),
Spacer(1, 6),
Paragraph(
"This report treats your checkmark as “R00 was completed,” then separates that from billing and from any newer active revisions. "
"It is designed to make client-billing comparison easier at a glance.",
styles["subtitle"],
),
Spacer(1, 18),
build_summary_cards(rows),
Spacer(1, 18),
]
story.extend(
build_table(
active_rows,
"Checked Sites With Active Revisions",
"These are the sites where R00 is done, but there is still a newer revision active or awaiting billing.",
)
)
story.append(PageBreak())
story.extend(
build_table(
no_active_rows,
"Checked Sites With No Active Revision",
"These are the checked sites that currently do not have a newer active revision.",
)
)
doc.build(story)
print(OUTPUT_PATH)
if __name__ == "__main__":
main()
+301
View File
@@ -0,0 +1,301 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const outputPath = path.join(__dirname, 'finance-summary.pdf');
const pdf = await PDFDocument.create();
const fontRegular = await pdf.embedFont(StandardFonts.Helvetica);
const fontBold = await pdf.embedFont(StandardFonts.HelveticaBold);
const PAGE_W = 612;
const PAGE_H = 792;
const MARGIN = 42;
const ACCENT = rgb(0.96, 0.65, 0.14);
const TEXT = rgb(0.09, 0.09, 0.09);
const MUTED = rgb(0.36, 0.36, 0.36);
const BORDER = rgb(0.90, 0.86, 0.80);
const SURFACE = rgb(0.985, 0.97, 0.94);
const WHITE = rgb(1, 1, 1);
let page = pdf.addPage([PAGE_W, PAGE_H]);
let y = PAGE_H - MARGIN;
function ensureSpace(heightNeeded = 40) {
if (y - heightNeeded < MARGIN) {
page = pdf.addPage([PAGE_W, PAGE_H]);
y = PAGE_H - MARGIN;
}
}
function drawText(text, x, yy, size = 12, font = fontRegular, color = TEXT) {
page.drawText(text, { x, y: yy, size, font, color });
}
function wrapText(text, maxWidth, size = 12, font = fontRegular) {
const words = String(text).split(/\s+/);
const lines = [];
let line = '';
for (const word of words) {
const test = line ? `${line} ${word}` : word;
const width = font.widthOfTextAtSize(test, size);
if (width <= maxWidth || !line) line = test;
else {
lines.push(line);
line = word;
}
}
if (line) lines.push(line);
return lines;
}
function drawParagraph(text, x, maxWidth, size = 12, color = TEXT, lineGap = 4, font = fontRegular) {
const lines = wrapText(text, maxWidth, size, font);
const lineHeight = size + lineGap;
ensureSpace(lines.length * lineHeight + 4);
for (const line of lines) {
drawText(line, x, y, size, font, color);
y -= lineHeight;
}
y -= 4;
}
function drawSectionTitle(text) {
ensureSpace(30);
drawText(text, MARGIN, y, 18, fontBold, TEXT);
y -= 26;
}
function drawCard(x, yy, w, h, title, bodyLines) {
page.drawRectangle({ x, y: yy - h, width: w, height: h, color: SURFACE, borderColor: BORDER, borderWidth: 1, borderRadius: 12 });
drawText(title, x + 12, yy - 20, 11, fontBold, MUTED);
let innerY = yy - 40;
for (const line of bodyLines) {
const wrapped = wrapText(line, w - 24, 11, fontRegular);
for (const piece of wrapped) {
drawText(piece, x + 12, innerY, 11, fontRegular, TEXT);
innerY -= 15;
}
innerY -= 2;
}
}
function drawFlowBox(title, lines) {
const contentLines = lines.flatMap(line => wrapText(line, PAGE_W - MARGIN * 2 - 24, 11, fontRegular));
const h = 40 + contentLines.length * 15 + 10;
ensureSpace(h + 24);
page.drawRectangle({ x: MARGIN, y: y - h, width: PAGE_W - MARGIN * 2, height: h, color: WHITE, borderColor: BORDER, borderWidth: 1, borderRadius: 12 });
drawText(title, MARGIN + 14, y - 20, 11, fontBold, MUTED);
let innerY = y - 40;
for (const line of contentLines) {
drawText(line, MARGIN + 14, innerY, 11, fontRegular, TEXT);
innerY -= 15;
}
y -= h + 18;
if (y > MARGIN + 40) {
drawText("v", PAGE_W / 2 - 3, y + 3, 14, fontBold, ACCENT);
}
}
function drawBullet(text) {
const bulletX = MARGIN + 4;
const textX = MARGIN + 18;
const maxWidth = PAGE_W - textX - MARGIN;
const lines = wrapText(text, maxWidth, 11, fontRegular);
const lineHeight = 15;
ensureSpace(lines.length * lineHeight + 4);
drawText("•", bulletX, y, 12, fontBold, TEXT);
for (const line of lines) {
drawText(line, textX, y, 11, fontRegular, TEXT);
y -= lineHeight;
}
y -= 2;
}
function drawTable(headers, rows, colWidths) {
const tableW = colWidths.reduce((a, b) => a + b, 0);
const startX = MARGIN;
const rowPad = 8;
const headerH = 24;
ensureSpace(36);
page.drawRectangle({ x: startX, y: y - headerH, width: tableW, height: headerH, color: SURFACE, borderColor: BORDER, borderWidth: 1 });
let x = startX;
headers.forEach((head, i) => {
drawText(head, x + 6, y - 15, 10, fontBold, MUTED);
x += colWidths[i];
});
y -= headerH;
for (const row of rows) {
const wrappedCells = row.map((cell, i) => wrapText(cell, colWidths[i] - 12, 10, fontRegular));
const lines = Math.max(...wrappedCells.map(c => c.length));
const rowH = Math.max(22, lines * 13 + rowPad);
ensureSpace(rowH + 2);
page.drawRectangle({ x: startX, y: y - rowH, width: tableW, height: rowH, borderColor: BORDER, borderWidth: 1 });
let cellX = startX;
wrappedCells.forEach((cellLines, i) => {
let cellY = y - 14;
for (const line of cellLines) {
drawText(line, cellX + 6, cellY, 10, fontRegular, TEXT);
cellY -= 12;
}
cellX += colWidths[i];
});
y -= rowH;
}
y -= 10;
}
function newPage() {
page = pdf.addPage([PAGE_W, PAGE_H]);
y = PAGE_H - MARGIN;
}
// Page 1
page.drawRectangle({
x: MARGIN,
y: y - 88,
width: PAGE_W - MARGIN * 2,
height: 88,
color: SURFACE,
borderColor: BORDER,
borderWidth: 1.2,
borderRadius: 16,
});
drawText('Fourge Portal Finance Summary', MARGIN + 18, y - 30, 24, fontBold, TEXT);
drawText('Role cheat sheet, finance lanes, and data flow for team, client, and subcontractor users.', MARGIN + 18, y - 54, 12, fontRegular, MUTED);
y -= 112;
drawSectionTitle('1. Executive Summary');
drawParagraph('The finance area is split into three separate role lanes. Team gets the full finance hub, clients get invoice viewing only, and subcontractors get invoice submission plus purchase-order review.', MARGIN, PAGE_W - MARGIN * 2, 12, TEXT);
ensureSpace(140);
const gap = 12;
const cardW = (PAGE_W - MARGIN * 2 - gap * 2) / 3;
drawCard(MARGIN, y, cardW, 104, 'TEAM', [
'Main route: /invoices',
'Handles client invoices, expenses, subcontractor invoices, and POs.',
]);
drawCard(MARGIN + cardW + gap, y, cardW, 104, 'CLIENT', [
'Main route: /my-invoices',
'Can view invoices, filter by company, and download PDFs.',
]);
drawCard(MARGIN + (cardW + gap) * 2, y, cardW, 104, 'SUBCONTRACTOR', [
'Main routes: /my-invoices-sub and /my-purchase-orders',
'Can submit invoices to Fourge and approve POs.',
]);
y -= 126;
drawSectionTitle('2. Role-by-Role Cheat Sheet');
drawTable(
['ROLE', 'MAIN ROUTES', 'CAN DO', 'CANNOT DO'],
[
['Team', '/invoices', 'Client invoices, expenses, POs, subcontractor invoices, paid status, PDFs, email sends', 'No major finance restrictions in UI'],
['Client', '/my-invoices', 'View invoices, filter by company, download invoice PDFs', 'No expenses, no edits, no POs, no subcontractor finance tools'],
['Subcontractor', '/my-invoices-sub /my-purchase-orders', 'Submit invoices, view status, download receipt after paid, approve POs', 'No team finance overview, no client billing control'],
],
[72, 120, 205, 131],
);
drawSectionTitle('3. Core Data Models');
drawBullet('invoices + invoice_items: Fourge bills clients.');
drawBullet('expenses: Fourge internal finance records.');
drawBullet('subcontractor_invoices + subcontractor_invoice_items: subcontractors bill Fourge.');
drawBullet('subcontractor_payments + subcontractor_po_items: Fourge sends purchase orders to subcontractors.');
drawBullet('tasks and submissions connect work records to invoice line items.');
// Page 2
newPage();
drawSectionTitle('4. System Flow');
drawFlowBox('CLIENT BILLING FLOW', [
'Approved work reaches tasks.status = client_approved.',
'Team finance loads uninvoiced tasks and revisions and builds invoice line items.',
'Invoice is saved, PDF is generated, and email is sent to the client.',
'Later team marks the invoice paid.',
]);
drawFlowBox('EXPENSE FLOW', [
'Team logs internal expenses and can attach receipt files.',
'Expenses feed overview charts, yearly finance summaries, and profit calculations.',
]);
drawFlowBox('SUBCONTRACTOR INVOICE FLOW', [
'Subcontractor creates an invoice from completed approved tasks assigned to them.',
'Fourge reviews the invoice, marks it paid, and can send a receipt PDF back.',
]);
drawFlowBox('PURCHASE ORDER FLOW', [
'Team creates a PO for a subcontractor, optionally linked to project tasks.',
'Subcontractor reviews and approves the PO from their own portal view.',
]);
y -= 8;
drawSectionTitle('5. Status Logic');
drawBullet('Client invoice: draft -> sent -> paid');
drawBullet('Subcontractor invoice: draft -> submitted -> paid');
drawBullet('Purchase order: draft -> sent -> approved -> ready_to_pay -> paid');
drawBullet('Cancelled is the exit state for a PO.');
drawSectionTitle('6. Key Business Rules');
drawBullet('Client invoices only pull approved, uninvoiced work.');
drawBullet('Tasks and revisions are marked invoiced after invoice creation.');
drawBullet('Deleting a team invoice rolls linked invoice flags back.');
drawBullet('Team invoice status changes also push task status changes.');
drawBullet('Revision billing rule: R00 = new work, R01 = free, R02+ = billable increments.');
drawBullet('Subcontractor invoices are created by the external user and then reviewed/paid by team.');
drawBullet('POs are created by team and approved by the subcontractor.');
// Page 3
newPage();
drawSectionTitle('7. Database Relationship Chart');
drawParagraph('Simple relationship map:', MARGIN, PAGE_W - MARGIN * 2, 12, TEXT, 4, fontBold);
const diagram = [
'COMPANIES',
' -> invoices',
' -> invoice_items',
' -> task_id -> tasks',
' -> submission_id -> submissions',
'',
'PROJECTS',
' -> tasks',
' -> submissions',
'',
'PROFILES (external)',
' -> subcontractor_invoices',
' -> subcontractor_invoice_items',
' -> task_id -> tasks',
' -> subcontractor_payments (POs)',
' -> subcontractor_po_items',
' -> task_id -> tasks',
'',
'EXPENSES',
' -> standalone internal finance records',
];
page.drawRectangle({ x: MARGIN, y: y - 270, width: PAGE_W - MARGIN * 2, height: 270, color: SURFACE, borderColor: BORDER, borderWidth: 1, borderRadius: 12 });
let codeY = y - 20;
for (const line of diagram) {
drawText(line, MARGIN + 14, codeY, 11, fontRegular, TEXT);
codeY -= 14;
}
y -= 292;
drawSectionTitle('8. Quick Route Map');
drawTable(
['SURFACE', 'ROUTE', 'USER'],
[
['Finance Hub', '/invoices', 'Team'],
['Client Invoices', '/my-invoices', 'Client'],
['Subcontractor Invoices', '/my-invoices-sub', 'External'],
['Create Subcontractor Invoice', '/my-invoices-sub/new', 'External'],
['Purchase Orders', '/my-purchase-orders', 'External'],
],
[210, 230, 88],
);
drawParagraph('Generated from the current portal codebase summary on June 3, 2026.', MARGIN, PAGE_W - MARGIN * 2, 10, MUTED);
const bytes = await pdf.save();
await fs.writeFile(outputPath, bytes);
console.log(outputPath);
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
# Invoice Match Audit
Date: `2026-06-05`
Definition used: your checked sites mean `R00` was completed. This audit compares that list against live client invoice data.
- Checked sites: 26
- Sites found in DB: 39
- Checked sites with R00 complete but not billed: 3
- Checked sites with completed unbilled versions: 6
- Unchecked sites that are already billed: 0
## Main Finding
Client invoicing appears aligned with your checked list overall: there are **no unchecked sites currently billed** from this list.
## Checked But R00 Not Billed Yet
- `30094` 30094 Colorado Springs, CO -> R00, status `client_approved`
- `30261` 30261 NSA Clovis NM -> R00, status `client_approved`
- `68720` 68720 NSA Lancaster PA -> R00, status `client_approved`
## Completed Versions Still Unbilled
- `30094` 30094 Colorado Springs, CO -> unbilled: R00; billed: —; current R00 (client_approved)
- `30096` 30096 Eerie, CO -> unbilled: R01; billed: R00: INV-2026-006 (sent); current R02 (not_started)
- `30261` 30261 NSA Clovis NM -> unbilled: R00; billed: —; current R00 (client_approved)
- `68720` 68720 NSA Lancaster PA -> unbilled: R00; billed: —; current R00 (client_approved)
- `68721` 68721 NSA Mechanicsburg PA -> unbilled: R02; billed: R00: INV-2026-006 (sent) | R01: INV-2026-006 (sent); current R02 (client_approved)
- `68811` 68811 Kelso, WA -> unbilled: R01; billed: R00: INV-2026-006 (sent); current R02 (not_started)
## Checked Sites With Active Revisions
| Site | Title | Current | Status | Billed Versions | Completed Unbilled |
|---|---|---|---|---|---|
| 00286 | 00286 Riverside, CA | R01 | not_started | R00: INV-2026-006 (sent) | — |
| 30055 | 30055 Moreno Valley, CA | R01 | client_review | R00: INV-2026-006 (sent) | — |
| 30096 | 30096 Eerie, CO | R02 | not_started | R00: INV-2026-006 (sent) | R01 |
| 68639 | 68639 NSA Los Lunas NM | R01 | client_review | R00: INV-2026-006 (sent) | — |
| 68663 | 68663 NSA Oklahoma City OK | R01 | not_started | R00: INV-2026-006 (sent) | — |
| 68664 | 68664 NSA Oklahoma City OK | R01 | not_started | R00: INV-2026-006 (sent) | — |
| 68666 | 68666 NSA Oklahoma City, OK | R01 | not_started | R00: INV-2026-006 (sent) | — |
| 68669 | 68669 NSA Oklahoma City OK | R01 | not_started | R00: INV-2026-006 (sent) | — |
| 68670 | 68670 NSA Oklahoma City OK | R02 | client_review | R00: INV-2026-006 (sent)<br>R01: INV-2026-006 (sent) | — |
| 68671 | 68671 NSA Oklahoma City OK | R01 | client_review | R00: INV-2026-006 (sent) | — |
| 68673 | 68673 NSA Oklahoma City OK | R01 | client_review | R00: INV-2026-006 (sent) | — |
| 68721 | 68721 NSA Mechanicsburg PA | R02 | client_approved | R00: INV-2026-006 (sent)<br>R01: INV-2026-006 (sent) | R02 |
| 68808 | 68808 Camas, WA | R01 | not_started | R00: INV-2026-006 (sent) | — |
| 68809 | 68809 Centralia, WA | R02 | not_started | R00: INV-2026-006 (sent)<br>R01: INV-2026-006 (sent) | — |
| 68810 | 68810 Chehalis, WA | R02 | not_started | R00: INV-2026-006 (sent)<br>R01: INV-2026-006 (sent) | — |
| 68811 | 68811 Kelso, WA | R02 | not_started | R00: INV-2026-006 (sent) | R01 |
## Unchecked But Already Billed
- None
File diff suppressed because it is too large Load Diff
+185
View File
@@ -0,0 +1,185 @@
# Site Completion and Billing Audit
Generated: 2026-06-04T13:58:27.846Z
- Listed sites: 95
- Found in database: 39
- Missing from database: 56
- Checked vs DB mismatches: 15
## Missing From Database
- 00278
- 00279
- 00281
- 00282
- 00284
- 00290
- 00291
- 30051
- 30054
- 30056
- 30058
- 30061
- 30063
- 30068
- 30095
- 30098
- 30210
- 30262
- 30319
- 30342
- 00288
- 30344
- 30345
- 30348
- 30349
- 30350
- 30355
- 30357
- 30407
- 68175
- 30412
- 30413
- 68215
- 68216
- 30437
- 68220
- 68432
- 68516
- 68617
- 68620
- 68631
- 68636
- 68637
- 68638
- 68715
- 68723
- 68751
- 68776
- 68777
- 68778
- 68779
- 68780
- 68781
- 68782
- 68784
- 68785
## Checked vs Database Mismatches
- 00286 00286 Riverside, CA: you marked completed, DB status is not_started
- 30055 30055 Moreno Valley, CA: you marked completed, DB status is client_review
- 30096 30096 Eerie, CO: you marked completed, DB status is not_started
- 68639 68639 NSA Los Lunas NM: you marked completed, DB status is client_review
- 68663 68663 NSA Oklahoma City OK: you marked completed, DB status is not_started
- 68664 68664 NSA Oklahoma City OK: you marked completed, DB status is not_started
- 68666 68666 NSA Oklahoma City, OK: you marked completed, DB status is not_started
- 68669 68669 NSA Oklahoma City OK: you marked completed, DB status is not_started
- 68670 68670 NSA Oklahoma City OK: you marked completed, DB status is not_started
- 68671 68671 NSA Oklahoma City OK: you marked completed, DB status is not_started
- 68673 68673 NSA Oklahoma City OK: you marked completed, DB status is client_review
- 68808 68808 Camas, WA: you marked completed, DB status is not_started
- 68809 68809 Centralia, WA: you marked completed, DB status is not_started
- 68810 68810 Chehalis, WA: you marked completed, DB status is not_started
- 68811 68811 Kelso, WA: you marked completed, DB status is not_started
## Audit Table
| Site # | DB Title | Internal # | In DB | Your Check | DB Completed | Match | Current R# | Active Status | R00 Billed | Billed Revisions | Completed Unbilled Revisions | Version Status Summary |
|---|---|---:|---:|---:|---:|---:|---|---|---|---|---|---|
| 00278 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 00279 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 00281 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 00282 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 00284 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 00285 | 00285 NSA Moreno Valley CA | 65 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 00286 | 00286 Riverside, CA | 31 | Yes | Yes | No | No | R01 | not_started | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: not_started |
| 00290 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 00291 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30049 | 30049 Elk Grove, CA | 32 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 30051 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30054 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30055 | 30055 Moreno Valley, CA | 33 | Yes | Yes | No | No | R01 | client_review | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: client_review |
| 30056 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30058 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30060 | 30060 NSA Palmdale CA | 66 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 30061 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30062 | 30062 Riverside, CA | 34 | Yes | Yes | Yes | Yes | R00 | invoiced | Yes (INV-2026-006 (sent)) | — | — | R00: invoiced / billed |
| 30063 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30068 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30091 | 30091 NSA Colorado Springs CO | 67 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 30094 | 30094 Colorado Springs, CO | 35 | Yes | Yes | Yes | Yes | R00 | client_approved | No | — | — | R00: client_approved |
| 30095 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30096 | 30096 Eerie, CO | 36 | Yes | Yes | No | No | R02 | not_started | Yes (INV-2026-006 (sent)) | — | R01 | R00: completed / billed; R01: completed; R02: not_started |
| 30098 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30101 | 30101 Monument, CO | 37 | Yes | No | No | Yes | R00 | on_hold | No | — | — | R00: on_hold |
| 30210 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30211 | 30211 NSA Post Falls ID | 68 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 30212 | 30212 NSA Sandpoint ID | 69 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 30258 | 30258 Lebanon, NH | 38 | Yes | Yes | Yes | Yes | R00 | invoiced | Yes (INV-2026-006 (sent)) | — | — | R00: invoiced / billed |
| 30259 | 30259 Hamburg, NJ | 39 | Yes | Yes | Yes | Yes | R00 | invoiced | Yes (INV-2026-006 (sent)) | — | — | R00: invoiced / billed |
| 30261 | 30261 NSA Clovis NM | 70 | Yes | Yes | Yes | Yes | R00 | client_approved | No | — | — | R00: client_approved |
| 30262 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30319 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30342 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 00288 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30344 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30345 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30348 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30349 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30350 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30355 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30357 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30407 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68175 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30412 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30413 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68215 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68216 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 30437 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68220 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68432 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68452 | 68452 NSA Houston TX | 71 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 68453 | 68453 NSA Katy TX | 72 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 68454 | 68454 NSA Katy TX | 73 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 68516 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68617 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68620 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68631 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68636 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68637 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68638 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68639 | 68639 NSA Los Lunas NM | 40 | Yes | Yes | No | No | R01 | client_review | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: client_review |
| 68663 | 68663 NSA Oklahoma City OK | 41 | Yes | Yes | No | No | R01 | not_started | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: not_started |
| 68664 | 68664 NSA Oklahoma City OK | 42 | Yes | Yes | No | No | R01 | not_started | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: not_started |
| 68666 | 68666 NSA Oklahoma City, OK | 43 | Yes | Yes | No | No | R01 | not_started | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: not_started |
| 68669 | 68669 NSA Oklahoma City OK | 44 | Yes | Yes | No | No | R01 | not_started | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: not_started |
| 68670 | 68670 NSA Oklahoma City OK | 45 | Yes | Yes | No | No | R02 | not_started | Yes (INV-2026-006 (sent)) | R01 [INV-2026-006 (sent)] | — | R00: completed / billed; R01: completed / billed; R02: not_started |
| 68671 | 68671 NSA Oklahoma City OK | 46 | Yes | Yes | No | No | R01 | not_started | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: not_started |
| 68673 | 68673 NSA Oklahoma City OK | 47 | Yes | Yes | No | No | R01 | client_review | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: client_review |
| 68715 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68720 | 68720 NSA Lancaster PA | 74 | Yes | Yes | Yes | Yes | R00 | client_approved | No | — | — | R00: client_approved |
| 68721 | 68721 NSA Mechanicsburg PA | 48 | Yes | Yes | Yes | Yes | R02 | client_approved | Yes (INV-2026-006 (sent)) | R01 [INV-2026-006 (sent)] | R02 | R00: completed / billed; R01: completed / billed; R02: client_approved |
| 68723 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68724 | 68724 NSA York PA | 75 | Yes | Yes | Yes | Yes | R00 | invoiced | Yes (INV-2026-006 (sent)) | — | — | R00: invoiced / billed |
| 68751 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68752 | 68752 NSA Brownsville TX | 76 | Yes | Yes | Yes | Yes | R00 | invoiced | Yes (INV-2026-006 (sent)) | — | — | R00: invoiced / billed |
| 68753 | 68753 NSA Brownsville TX | 77 | Yes | Yes | Yes | Yes | R00 | invoiced | Yes (INV-2026-006 (sent)) | — | — | R00: invoiced / billed |
| 68754 | 68754 NSA Brownsville TX | 78 | Yes | No | No | Yes | R00 | in_progress | No | — | — | R00: in_progress |
| 68755 | 68755 NSA Brownsville TX | 79 | Yes | No | No | Yes | R00 | not_started | No | — | — | R00: not_started |
| 68756 | 68756 NSA Brownsville TX | 80 | Yes | No | No | Yes | R00 | in_progress | No | — | — | R00: in_progress |
| 68758 | 68758 NSA Brownsville TX | 81 | Yes | Yes | Yes | Yes | R00 | invoiced | Yes (INV-2026-006 (sent)) | — | — | R00: invoiced / billed |
| 68776 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68777 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68778 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68779 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68780 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68781 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68782 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68784 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68785 | — | — | No | No | — | — | — | — | — | — | — | Missing from DB |
| 68808 | 68808 Camas, WA | 49 | Yes | Yes | No | No | R01 | not_started | Yes (INV-2026-006 (sent)) | — | — | R00: completed / billed; R01: not_started |
| 68809 | 68809 Centralia, WA | 50 | Yes | Yes | No | No | R02 | not_started | Yes (INV-2026-006 (sent)) | R01 [INV-2026-006 (sent)] | — | R00: completed / billed; R01: completed / billed; R02: not_started |
| 68810 | 68810 Chehalis, WA | 51 | Yes | Yes | No | No | R02 | not_started | Yes (INV-2026-006 (sent)) | R01 [INV-2026-006 (sent)] | — | R00: completed / billed; R01: completed / billed; R02: not_started |
| 68811 | 68811 Kelso, WA | 52 | Yes | Yes | No | No | R02 | not_started | Yes (INV-2026-006 (sent)) | — | R01 | R00: completed / billed; R01: completed; R02: not_started |