From c40dfb552e88c6d6b23f447dc97f0443f29aa537 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 30 May 2026 16:01:20 -0500 Subject: [PATCH] Auto-save intake data to server after every step Intake data now persists to DB after each step completion (non-blocking). If browser crashes, data is recoverable from compliance_orders.intake_data. Partial saves (_partial: true) only update intake_data without changing payment_status or marking intake_data_validated. Final submit still triggers the full validation + worker dispatch flow. Co-Authored-By: Claude Opus 4.6 (1M context) --- api/src/routes/compliance-orders.ts | 28 ++++++++++++++++--------- site/src/components/intake/Wizard.astro | 25 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts index a7daf28..3d55de9 100644 --- a/api/src/routes/compliance-orders.ts +++ b/api/src/routes/compliance-orders.ts @@ -1758,7 +1758,7 @@ router.get("/api/v1/compliance-orders/my-orders", async (req, res) => { */ router.put("/api/v1/compliance-orders/:id/intake", async (req, res) => { const id = req.params.id; - const { intake_data, entity, officers } = req.body ?? {}; + const { intake_data, entity, officers, _partial } = req.body ?? {}; if (!intake_data) { res.status(400).json({ error: "intake_data required" }); @@ -1798,15 +1798,23 @@ router.put("/api/v1/compliance-orders/:id/intake", async (req, res) => { } // Update the order with merged intake data + entity link - await pool.query( - `UPDATE compliance_orders - SET intake_data = $1, - payment_status = CASE WHEN payment_status = 'pending_intake' THEN 'paid' ELSE payment_status END, - intake_data_validated = TRUE, - telecom_entity_id = COALESCE($3, telecom_entity_id) - WHERE order_number = $2`, - [JSON.stringify(merged), id, telecomEntityId], - ); + // Partial saves (auto-save between steps) only update intake_data, not status + if (_partial) { + await pool.query( + `UPDATE compliance_orders SET intake_data = $1 WHERE order_number = $2`, + [JSON.stringify(merged), id], + ); + } else { + await pool.query( + `UPDATE compliance_orders + SET intake_data = $1, + payment_status = CASE WHEN payment_status = 'pending_intake' THEN 'paid' ELSE payment_status END, + intake_data_validated = TRUE, + telecom_entity_id = COALESCE($3, telecom_entity_id) + WHERE order_number = $2`, + [JSON.stringify(merged), id, telecomEntityId], + ); + } // If entity data was provided, update the telecom_entity too if (entity && entity.frn) { diff --git a/site/src/components/intake/Wizard.astro b/site/src/components/intake/Wizard.astro index 9da381b..b998311 100644 --- a/site/src/components/intake/Wizard.astro +++ b/site/src/components/intake/Wizard.astro @@ -484,12 +484,37 @@ const STEP_LABELS: Record = { if (cancelEvt.defaultPrevented) return; if (s.step_index < steps.length - 1) { renderStep(s.step_index + 1); + // Auto-save intake data to server after every step (non-blocking) + autoSaveIntake(loadState()); } else { // Last step — submit the intake data submitIntake(s); } }); + // Auto-save intake data to server after each step (non-blocking, fire-and-forget) + function autoSaveIntake(state: IntakeState) { + const params = new URLSearchParams(window.location.search); + const token = params.get("token"); + const orderNumber = state.intake_data?.order_number || params.get("order") || ""; + const API = (window as any).__PW_API || ""; + if (!orderNumber || !API) return; + + fetch(`${API}/api/v1/compliance-orders/${orderNumber}/intake`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...(token ? { "Authorization": `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + intake_data: state.intake_data, + entity: state.entity, + officers: state.officers, + _partial: true, // flag that this is an intermediate save, not final + }), + }).catch(() => {}); // silent — don't block the user + } + async function submitIntake(state: IntakeState) { const nextBtn = document.getElementById("pw-next") as HTMLButtonElement; nextBtn.disabled = true;