mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
2 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4204b1fe0e |
feat(ara-projects): import — contractor/stage columns + setup gate
Extends the #414 upload-and-parse work (does not rewrite it) with a per-row contractor, an optional stage, and a setup-completeness gate on the import page. New ADR-0019 records the design. Still writes nothing. CHANGE 1 — template + parser expected fields Final column order: Property identifier, Workstream, Contractor, Stage, Work order reference (optional), Forecast end date (optional). Contractor is required; Stage is optional (blank falls back to the workstream's first stage at commit). buildImportTemplateCsv, its worked example and the field docs are updated. The raw parser stays header-agnostic — required/optional is enforced in mapping (#415) and validation/commit (#417), not here — with a test proving a file missing the required Contractor column still parses. CHANGE 2 — setup-completeness gate (the substantive feature) src/lib/projects/import/readiness.ts: a pure computeImportReadiness(facts) plus a read-only loadImportReadiness(projectId) round trip. Rule: every project_workstream must have >=1 project_workstream_stage AND >=1 project_workstream_contractor, and the project must have >=1 workstream. Evidence requirements (#412) are advisory and excluded from the gate. The helper returns per-workstream detail (which are incomplete, and why) and is built to be reused by #413's final wizard step — noted in a comment. The import page loads readiness and, when not ready, renders an ImportSetupIncomplete state (lists the incomplete workstreams with the reason, links to project setup) instead of the drop zone — not a 404, not a silent empty zone. When ready, the drop zone renders as before. The gate applies on every entry: the route stays reachable standalone, and a comment notes #413 will also route the wizard into it (that wiring is out of scope here). CHANGE 3 — ADR-0019 Records: import is the final, setup-gated wizard step; contractor is supplied per row and must match a contractor already assigned to the workstream (contractor creation is out of scope — the existing "invite company to Ara" flow owns it); stage is optional per row with first-stage fallback. States the trade-off vs the replaced #417 design, which derived the contractor from the workstream's single assignee — abandoned because project_workstream_contractor is a collection, so one workstream's properties can be split across contractors, which a single-assignee lookup cannot express. Notes on the readiness query count(distinct project_workstream_stage.id) / (…contractor.id) over two left joins — the distinct is load-bearing because joining both children at once makes a stages x contractors cartesian product a plain count(*) would over-count. count(distinct) returns a bigint that node-postgres hands back as a string, so each count is coerced with Number(); a loader test drives a mocked db to prove that coercion. Production-DB safety No migrations, seeds or writes. loadImportReadiness is read-only SELECTs and is not executed here. Unit tests mock @/app/db/db (vi.mock) and open no connection; the pure readiness logic is tested with in-memory fixtures. Full suite: 575 passing. tsc and eslint clean. authz modules and src/app/projects/page.tsx untouched (other branches own them). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9cfde2767f |
feat(ara-projects): work-order import step 1 — upload and parse
Adds `/projects/[projectId]/import`: a CSV/XLSX drop zone that uploads to a route handler, parses server-side with SheetJS (already a dependency — the Bulk tag assignment flow is the in-app parsing precedent, not the FastAPI BulkUpload pipeline), and returns headers plus a first-N preview. Nothing is written to the database at this step. Insert happens at commit (#417). Files: src/lib/projects/import/parse.ts pure, DB-free parser + template src/lib/projects/import/parse.test.ts 23 unit tests, no DB, no fixtures .../api/projects/[projectId]/import/parse/route.ts POST multipart .../api/projects/[projectId]/import/template/route.ts GET template CSV .../projects/[projectId]/import/page.tsx server component .../import/components/ImportUpload.tsx drop zone + preview Persistence choice: hold rows client-side, not S3-and-reparse ------------------------------------------------------------- The ticket asks for a choice between uploading the raw file to S3 via the presigned-URL pattern in `src/lib/bulkUpload/client.ts` and re-parsing it per step, versus holding the parsed rows in the client and POSTing them at commit. Chosen: hold the rows client-side. The parse route therefore returns every row, not only the preview, and the mapping (#415) and validation (#416) steps read them from `ImportUpload`'s mutation state. Reasoning: 1. S3-and-reparse needs somewhere to keep the object key and the import's status between steps, and the Ara Projects schema (PR #404) has no import table. Adding one means designing a second BulkUpload-shaped lifecycle — hard to reverse, and ADR-worthy — for a v1 that is insert-only with no diffing (deferred to #425). Not worth it to carry a file across three steps of one sitting. 2. The scale does not call for it. This import declares which workstreams the properties of a single project take — thousands of rows, not the hundreds-of-thousands the BulkUpload property pipeline handles. The 20k row cap bounds the commit POST to a few MB of JSON. 3. Re-parsing per step parses the same bytes three or four times and lets the steps disagree if the parser ever changes underneath them. Client-held rows are by construction the exact rows the user previewed and will validate. 4. It matches the existing precedent: Bulk tag assignment (ADR-0013) parses a small spreadsheet and POSTs the extracted identifiers at commit, with no S3 round trip. The cost is that a page refresh loses progress and the commit POST carries the payload. Both are acceptable for a single-sitting, insert-only v1; if the import later needs to be resumable or to exceed this scale, S3 staging plus an import table is the upgrade path, and the parser is already isolated behind `parseImportFile` so only the transport changes. Notes ----- - The parser is deliberately header-agnostic: it reports the headers it finds rather than demanding the template's. Mapping arbitrary client headings is #415's job, and rejecting unrecognised headings here would make the mapping step unreachable for exactly the files that need it. - Authorization uses `canManageProject` from `src/lib/projects/authz.ts` with facts from the #408 repository, so importing is internal/client-org only and never a contractor. Unreachable projects 404 rather than 403. The page applies the same check so a contractor never sees the drop zone. - `raw: false` on the SheetJS read keeps leading zeros on numeric-looking property identifiers (covered by a test). - Size is capped at 10MB client- and server-side. The wireframe says 50MB; 10MB is far past what four narrow columns can plausibly reach. - Known limitation, documented in a characterisation test: SheetJS does not throw on loose bytes, so a multi-line non-spreadsheet renamed to .csv parses into nonsense headers rather than being rejected. Nothing is written, the extension allowlist catches the honestly-named case, and the user sees the garbage in the preview. Content sniffing here would be guesswork. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |