diff --git a/api/src/routes/admin.ts b/api/src/routes/admin.ts index 6ee911e..2b536c4 100644 --- a/api/src/routes/admin.ts +++ b/api/src/routes/admin.ts @@ -611,6 +611,74 @@ router.post("/api/v1/admin/compliance-orders/:order_number/rearm-intake", requir } }); +/** + * POST /api/v1/admin/compliance-orders/:order_number/mark-filed + * Advance an order's fulfillment_status after a human has handled it. Used for + * admin-assisted services (UCR, MC authority, etc.) that have no automated + * filing path: once you've filed it on the government portal, mark it + * filed_waiting_state (submitted, awaiting the agency) or completed (done). + * body: { status: 'filed_waiting_state' | 'completed', note?, confirmation? } + */ +router.post("/api/v1/admin/compliance-orders/:order_number/mark-filed", requireAdmin, async (req, res) => { + const id = req.params.order_number; + const ALLOWED = new Set(["filed_waiting_state", "completed"]); + try { + const status = String(req.body?.status || ""); + if (!ALLOWED.has(status)) { + res.status(400).json({ error: "status must be filed_waiting_state or completed." }); + return; + } + const { rows } = await pool.query( + `SELECT order_number, fulfillment_status FROM compliance_orders WHERE order_number = $1`, + [id], + ); + const order = rows[0]; + if (!order) { res.status(404).json({ error: "Order not found." }); return; } + + const confirmation = (req.body?.confirmation as string) || ""; + const note = (req.body?.note as string) + || `Marked ${status} by admin${confirmation ? ` (confirmation: ${confirmation})` : ""}`; + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + await client.query( + `UPDATE compliance_orders + SET fulfillment_status = $2, fulfillment_status_at = now(), updated_at = now() + WHERE order_number = $1`, + [id, status], + ); + // Record the confirmation number into intake_data.filing_status when given. + if (confirmation) { + await client.query( + `UPDATE compliance_orders + SET intake_data = jsonb_set( + jsonb_set(COALESCE(intake_data, '{}'::jsonb), '{filing_status}', + COALESCE(intake_data->'filing_status', '{}'::jsonb)), + '{filing_status,manual_confirmation}', to_jsonb($2::text)) + WHERE order_number = $1`, + [id, confirmation], + ); + } + await client.query( + `INSERT INTO order_audit_log (order_type, order_id, order_number, action, from_status, to_status, actor_type, actor_id, actor_name, note) + VALUES ('compliance', 0, $1, 'marked_filed', $2, $3, 'admin', $4, $5, $6)`, + [id, order.fulfillment_status, status, req.admin!.id, req.admin!.username, note], + ); + await client.query("COMMIT"); + } catch (txErr) { + await client.query("ROLLBACK").catch(() => {}); + throw txErr; + } finally { + client.release(); + } + res.json({ success: true, order_number: id, status }); + } catch (err) { + console.error(`[admin/compliance-orders] mark-filed error for ${id}:`, err); + res.status(500).json({ error: "Mark-filed failed." }); + } +}); + // ── Document discovery + viewing ───────────────────────────────────────────── // // Every prepared/signed filing PDF lives in MinIO. We surface them so you can diff --git a/site/public/admin/compliance-orders/index.html b/site/public/admin/compliance-orders/index.html index 487c06d..49d579b 100644 --- a/site/public/admin/compliance-orders/index.html +++ b/site/public/admin/compliance-orders/index.html @@ -345,6 +345,18 @@ refresh(); } catch (e) { alert("Re-arm failed: " + e.message); } } + async function markFiled(orderNumber, status) { + const label = status === "completed" ? "completed" : "filed (waiting on the agency)"; + const conf = ($("drawer-confirmation") && $("drawer-confirmation").value.trim()) || ""; + if (!confirm(`Mark ${orderNumber} as ${label}?` + (conf ? `\nConfirmation #: ${conf}` : ""))) return; + try { + await api("/api/v1/admin/compliance-orders/" + encodeURIComponent(orderNumber) + "/mark-filed", + { method: "POST", body: JSON.stringify({ status, confirmation: conf }) }); + alert(`Marked ${label}.`); + $("drawer").classList.add("hidden"); + refresh(); + } catch (e) { alert("Mark failed: " + e.message); } + } async function openDetail(orderNumber) { $("drawer").classList.remove("hidden"); @@ -377,6 +389,16 @@ + ``) : "") + ((order.payment_status === "paid" && !order.intake_data_validated) ? `` : "") + + // Manual filing controls: once an order is approved/awaiting agency, let + // the admin record that they've filed it (admin-assisted services have + // no automated submission, so this is how they reach completed). + (["authorization_signed", "ready_to_file", "filed_waiting_state"].includes(order.fulfillment_status) + ? `
Manual filing
` + + `` + + (order.fulfillment_status !== "filed_waiting_state" + ? `` : "") + + `` + : "") + `
Intake data
${esc(JSON.stringify(intake, null, 2))}
` + `
Documents
Loading documents…
` + `
Audit log
${auditHtml}`; @@ -384,6 +406,10 @@ if (da) da.addEventListener("click", () => approveOrder(order.order_number, order.service_name || order.service_slug, !!order.intake_data_validated)); const dr = $("drawer-rearm"); if (dr) dr.addEventListener("click", () => rearmIntake(order.order_number)); + const mw = $("drawer-mark-waiting"); + if (mw) mw.addEventListener("click", () => markFiled(order.order_number, "filed_waiting_state")); + const mc = $("drawer-mark-completed"); + if (mc) mc.addEventListener("click", () => markFiled(order.order_number, "completed")); loadDocuments(order.order_number); } catch (e) { $("drawer-body").innerHTML = `
Failed to load: ${esc(e.message)}
`; } }