Folder sync failed silently whenever FileBrowser was unreachable: every
call site swallows the error with console.error, so projects and tasks
were created in the portal with no matching folder on the drive.
Add api/folder-reconcile.js, a secret-protected endpoint that walks
companies/projects/tasks/subcontractors and creates only the folders that
are missing. It never deletes or renames. Folders on disk with no portal
record are reported as orphans for manual review, since they predate the
portal and hold real client work.
Extract shared FileBrowser helpers into api/_lib/filebrowser.js. Note that
this build returns directory listings under a `folders` key rather than
`items`; reading the wrong key yields an empty list and makes the whole
tree look missing, so listDirectories handles both shapes.
Reconcile lists before writing and creates leaves directly rather than
re-walking each path from the root, which keeps a full run at a few
seconds instead of timing out at the 300s function limit.
Schedule hourly via pg_cron: Vercel Hobby caps crons at once per day.
Requires app.folder_reconcile_secret to be set on the database separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lint went from 84 problems to 0. Build passes.
Dead code:
- TeamInvoices: drop the orphaned Subcontractor PO block (updateSubcontractorPO
plus send/ready-to-pay/paid/reopen/cancel/delete/download/CPA-export handlers
and ~25 derived vars). That feature moved to the routed
TeamCreateSubcontractorPO / TeamSubcontractorPODetail pages in the Layout2
redesign; these were leftovers. 2100 -> 1880 lines.
- Tasks: drop unrendered project sort/tab/completion logic in TasksPageShell.
- Remove 59 unused vars, imports, and handlers across 12 other files.
Removed wasted queries:
- TeamInvoices ran a nested subcontractor_payments join (profiles, projects,
companies, PO items, tasks) on every page load into state nothing read.
- TeamInvoices and ExternalMyInvoices each fetched a task-type map on invoice
open and discarded it; the ExternalMyInvoices one blocked the detail popup.
- RequestForm queried sign_families on mount into unread state.
Dependencies:
- Drop heic2any (zero imports). heic-to is the one in use and already lazy-loads.
Effects and exports:
- RequestForm: remove 4 effects. Sign-row sizing and company-change resets move
into their change handlers; single-company selection is derived during render
rather than written back into form state.
- FilterDropdown: measure position on click instead of in an effect, which also
removes a one-frame stale-position flicker.
- BrandBook: latest-ref pattern so the 900ms address debounce is not reset by
handler identity.
- ClientMyInvoices: hoist MONTHS to module scope.
- Split non-component exports: POPUP_FIELD_LABEL moves to lib/popupStyles.js;
resolveProfileAvatar and useLiveClock are internal-only; getGreeting deleted
as nothing referenced it.
Companies.jsx keeps one scoped eslint-disable with a reason: load() only
setStates after awaiting its queries, so set-state-in-effect is a false positive
there, and load() is shared with the create/delete handlers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Report was flagging every unbilled delivered version as "Not Invoiced"
for sub billing, even when a team member delivered it — team work is
billed to the client directly and is never sub-invoiced. Now traces
each version's actual deliverer and shows N/A when it's a team member,
regardless of who delivered earlier versions on the same task.
Also: SubcontractorInvoiceForm no longer masks fetch errors as "No
completed tasks available to invoice" — the real error now shows in
the Completed Tasks panel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clicking the dimmed area outside a modal (+Task, +Invoice, expense,
review, etc.) accidentally dismissed it and lost in-progress input.
Every popup already has an explicit Cancel/Close/✕ button, so the
backdrop click handler is removed; also drops the now-dead
onClose/blockClose prop plumbing that only existed to support it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completed Tasks list scoped by task.assigned_to meant a reassigned task
made the original sub's already-delivered versions permanently unbillable
by anyone. Now scoped by deliveries.sent_by directly, so each version
stays billable by whoever actually delivered it, independent of who
currently owns the task.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename in 91045a1 was only partially reverted by later commits, leaving
~20 UI spots calling the project entity "Client" while table headers
already said "Project". Reverted all of them plus the /clients/:id
route (now /projects/:id canonical again, nothing was live yet).
Also: stop tracking .env.backfill (had a live admin password/token
committed - rotate those credentials), remove stale pre-redesign
layout.md superseded by REDESIGN-LAYOUT2.md.
Draft invoices and orphaned invoice line items (invoice deleted → null)
no longer count as Invoiced on the billing report, matching what the
Finances page shows as issued. Not-invoiced/invoiced/paid buckets still
sum to total version rows. No-op on current data (no drafts/orphans);
guards future drafts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rename "Project" → "Client" across UI (labels + /projects→/clients routes with redirect); normalize company-meaning labels to "Company"
- Finance tabs: merge dual cards into one per tab, add header column filters (expenses/subs/invoices), overview as 3-section card, move +Expense/+Invoice to card right edge
- Tasks: responsive mobile stats grid, progressive column hiding (min Status+Name+Assigned), sidebar mobile footer (profile/theme/signout)
- Hide Company column for single-company users; +Task shows disabled Company field instead of hiding
- Sticky table headers site-wide with opaque card background
- Pin dev server to port 5173
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design system (Layout2):
- Solid token-driven theme (no animated grid); single-source tokens for bg,
cards (bg/border/radius), popups, fonts (Inter), and full semantic +
data-viz color palette. Swept ~168 hardcoded colors to tokens.
Dashboard:
- Reflow: To Do + Calendar row (fixed right rail) over Activity / In-Progress /
Team Performance; CSS height-match (To Do = Calendar); responsive stacking.
- 'Tasks Not Started' -> 'To Do' card with R#/Assigned columns; compact money fmt.
Tasks page:
- Combined tabs into Tasks + Completed with a Status column.
- Column-header sort + filter (Status, R#/new-vs-revision, Project, Task Type,
Company) via portaled, scrollable FilterDropdown; removed toolbar filters and
the projects side panel; headers stay visible when empty; renamed to 'Tasks'.
Perf:
- FK/filter index migration; removed redundant client/external scope waterfall
(rely on RLS); parallel follow-up queries.
Mobile:
- Responsive grids; mobile topbar = hamburger only, blends with bg, proper offset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add sync_project_status trigger so projects flip active<->completed with their tasks; backfill existing rows.
- handleSubmitReject now aborts (and alerts) if the rejection-reason submission fails to save, instead of silently flipping status to in_progress and logging a reason-less rejection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Invoice numbers now come from DB function next_invoice_number()
(max+1 per year under advisory lock) with a unique index; the old
row-count method reused numbers after deletes and raced concurrent
creates, which broke public pay links
- Remove dead standalone TeamCreateInvoice page; the TeamInvoices
modal is the single create path (page had already drifted)
- Invoice delete now asks for confirmation and only un-bills tasks/
submissions not billed on another invoice
- Created invoice shows correct "sent" status in list without reload
- Invoice/due dates computed at save time, not module load
- Reopening a paid invoice clears stale stripe_fee
- Stripe webhook markPaid is idempotent (no double receipts)
- subcontractor_invoice_items.version_number column stores billing
version explicitly; description parsing kept as legacy fallback
- Drop unused buildInvoiceStatusByKey/deriveVersionStatus
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourge's own error revisions should never be charged to the client.
getRevisionChargeQuantity now returns 0 for revision_type=fourge_error
regardless of version number. Applied at all 4 invoice picker call sites.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review-shadow submissions are type=initial with blank service_type; the
picker grabbed the first initial found, hitting a shadow -> null service ->
$0 price. New pickInitialServiceType() skips shadows and prefers a real
service_type. Fixes Harlingen sites showing $0 on invoice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tasks/projects page: reverted back to one row per task (work status only,
no billing state on that page). Per-version R## tracking lives in invoicing
only (invoice_items, sub invoice form).
DB fix: 7 Public Storage tasks had pending revision versions (R01/R02)
with no delivery but were stuck as 'paid' from old invoice lifecycle sync.
Reset to 'not_started' so they appear in the Revisions tab for work:
68664, 68666, 68721 (R02), 68808, 68809 (R02), 68810 (R02), 68811 (R02)
Removed unused invoiceItems state/fetch from Tasks + ProjectDetail.
invoiceVersionRules helpers retained for invoicing use.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each revision (R00, R01, R02) is now an independent trackable unit:
invoiceVersionRules: add deriveVersionStatus, buildInvoiceStatusByKey,
parseVersionFromItemDescription for per-version status derivation
Tasks.jsx + ProjectDetail.jsx:
- Fetch invoice_items joined with invoice.status (team only)
- allRows/rows now flatMap per version (one row per R##)
- Status derived from: invoice_items > past version > task.status
- R00 paid shows as paid; R01 in_progress shows separately
- tr keys use rowKey (taskId:version) to avoid duplicate key warnings
Invoice lifecycle (TeamInvoiceDetail, TeamInvoices, TeamCreateInvoice):
- Remove task.status sync on invoice paid/sent/created
- Keep invoiced:true flag for double-billing prevention
- Task work status (not_started → client_approved) stays work-flow only
ProjectDetail completion bar uses task.status === client_approved only
(no longer polluted by invoiced/paid status from invoice sync)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevent overwriting task status when task has moved to a new revision
cycle after being invoiced. Now uses .eq('status', guardStatus) to only
update tasks still at the expected prior state:
- invoice creation: only update tasks at client_approved → invoiced
- mark sent: only client_approved → invoiced
- mark paid: only invoiced → paid
- reopen: only paid → client_approved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rgba(255,255,255,0.06) with backdrop blur renders lighter in Chrome.
rgba(0,0,0,0.35) gives consistent dark panel across all browsers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 'PURCHASE ORDER' → 'SUBCONTRACTOR INVOICE'
- Items loop now calls addPage() when hitting safeBottom (792-80pt)
- Continuation pages get header with invoice number + '(continued)'
- Summary/Notes sections also page-break if needed
- Footer text updated to match invoice context
- Footer line position dynamic (tracks y), not hardcoded at 704
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Company dropdown and Email To input only editable on draft invoices.
Sent and paid invoices show read-only text values instead.
Applied to both popup (embedded) and standalone page views.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace SubcontractorInvoiceDetailView (card-based) with flat layout
matching the original sub-invoice popup: meta strip + raw table + notes.
- InvoiceDetailPopup: add metaContent/metaActions/metaCols props + export POPUP_FIELD_LABEL
- New InvoicePopupTable: flat sortable 5-col table, no card wrapper
- All 4 popups (client, team invoice, team sub-invoice, external): use
flat meta strip (4-col grid, borderBottom) and InvoicePopupTable
- Sub-invoice + external: Submitted | Paid/Created | Items | Total
- Client invoice: Date | Due | Status | Total
- Team invoice: Date | Due | Company | Email | Total (+ stripe if paid)
with Edit Dates button in metaActions slot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Team performance: count each delivery record individually (sent_by + version_number), not per task — R00/R01/R02 by different people all count separately
- Performance uses deliveries table directly, EST timezone bucketing, month-gated tabs
- TaskDetail Submissions tab: reads from deliveries table, shows R## version, sent_by, sent_at
- Submit for Review: writes to deliveries + delivery_files (deliveries bucket), stores version_number
- Revision request popup: uses FileAttachment drag-drop component
- Task table: only Name and Project columns highlight on hover
- deliveries.version_number column added and backfilled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Expense detail popup: 80vw×80vh, inline editing, save gated on dirty state, Download Receipt in footer
- Add expense form: same layout with FileAttachment drag-drop on right
- New invoice popup: 80vw×80vh inline form replacing navigate-away page
- Tasks table: suppress row hover background, only Name/Project table-links highlight
- TaskDetail Submissions tab: reads from deliveries table, shows R## version, sent_by, message
- Submit for Review: writes to deliveries + delivery_files (deliveries bucket)
- deliveries.version_number column added and backfilled from revision history
- Port 4173 for dev server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>