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
+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);