From 2296566e85183d81717f3727428fe955239f1109 Mon Sep 17 00:00:00 2001 From: justin Date: Mon, 15 Jun 2026 23:57:05 -0500 Subject: [PATCH] admin: compliance-orders dashboard (view, approve-to-file, re-arm intake) The admin SPA only managed formation_orders; compliance service orders (telecom/DOT/healthcare) had no admin surface, so you couldn't see what was paid, what was stuck on intake, or approve a prepared filing for submission. API (api/src/routes/admin.ts), all requireAdmin: GET /api/v1/admin/compliance-orders list, grouped by batch, filters GET /api/v1/admin/compliance-orders/stats queue overview counts GET /api/v1/admin/compliance-orders/:id full detail + audit log POST /api/v1/admin/compliance-orders/:id/approve approve ready_to_file + dispatch worker POST /api/v1/admin/compliance-orders/:id/rearm-intake clear reminder stamp so daily nudge resumes UI: new static page /admin/compliance-orders/ (self-contained, CSP-safe inline CSS, no external JS framework) reusing the existing pw_admin_token session. Cards group multi-service batches, flag paid+intake-incomplete in red, show reminder counts, and expose Approve & Re-arm buttons. Linked from the main /admin top bar. Every approve/re-arm writes an order_audit_log entry. --- api/src/routes/admin.ts | 279 +++++++++++++ .../public/admin/compliance-orders/index.html | 389 ++++++++++++++++++ site/public/admin/index.html | 2 +- 3 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 site/public/admin/compliance-orders/index.html diff --git a/api/src/routes/admin.ts b/api/src/routes/admin.ts index 4972d14..a5ed72d 100644 --- a/api/src/routes/admin.ts +++ b/api/src/routes/admin.ts @@ -301,4 +301,283 @@ router.get("/api/v1/admin/audit", requireAdmin, async (req, res) => { } }); +// ===================================================================== +// Compliance Orders (telecom / DOT / healthcare service fulfillment) +// +// The legacy admin SPA only manages formation_orders. Compliance service +// orders (compliance_orders) had no admin surface at all -- you couldn't see +// what was paid, what was stuck on intake, or approve a prepared filing for +// submission. These endpoints back the /admin/compliance-orders page. +// ===================================================================== + +/** GET /api/v1/admin/compliance-orders — list, grouped by batch, with filters. */ +router.get("/api/v1/admin/compliance-orders", requireAdmin, async (req, res) => { + try { + const payment = (req.query.payment as string) || ""; + const fulfillment = (req.query.fulfillment as string) || ""; + const intake = (req.query.intake as string) || ""; // 'incomplete' | 'complete' + const q = ((req.query.q as string) || "").trim().toLowerCase(); + const limit = Math.min(parseInt(req.query.limit as string, 10) || 200, 500); + + const where: string[] = ["1=1"]; + const params: any[] = []; + let i = 1; + + if (payment) { where.push(`co.payment_status = $${i++}`); params.push(payment); } + if (fulfillment === "none") { where.push(`co.fulfillment_status IS NULL`); } + else if (fulfillment) { where.push(`co.fulfillment_status = $${i++}`); params.push(fulfillment); } + if (intake === "incomplete") { where.push(`COALESCE(co.intake_data_validated, FALSE) = FALSE`); } + else if (intake === "complete") { where.push(`co.intake_data_validated = TRUE`); } + if (q) { + where.push(`(lower(co.customer_email) LIKE $${i} OR lower(co.customer_name) LIKE $${i} OR lower(co.order_number) LIKE $${i} OR lower(COALESCE(co.batch_id,'')) LIKE $${i})`); + params.push(`%${q}%`); i++; + } + + params.push(limit); + const { rows } = await pool.query( + `SELECT co.order_number, co.batch_id, co.service_slug, co.service_name, + co.customer_email, co.customer_name, co.customer_phone, + co.payment_status, co.payment_method, co.paid_at, + co.service_fee_cents, co.gov_fee_cents, co.surcharge_cents, co.discount_cents, + co.fulfillment_status, co.fulfillment_status_at, + COALESCE(co.intake_data_validated, FALSE) AS intake_data_validated, + co.intake_reminder_count, co.intake_reminder_last_at, + co.erpnext_sales_order, co.created_at + FROM compliance_orders co + WHERE ${where.join(" AND ")} + ORDER BY co.created_at DESC + LIMIT $${i}`, + params, + ); + + // Group multi-service batches (batch_id) into a single card; standalone + // orders (no batch_id) become their own one-item group keyed by order_number. + type Grp = { + group_id: string; + is_batch: boolean; + customer_name: string; + customer_email: string; + customer_phone: string | null; + payment_status: string; + payment_method: string | null; + paid_at: string | null; + created_at: string; + total_cents: number; + intake_all_complete: boolean; + intake_any_incomplete: boolean; + max_reminder_count: number; + last_reminded_at: string | null; + services: any[]; + }; + const groups = new Map(); + for (const r of rows as any[]) { + const key = r.batch_id || r.order_number; + let g = groups.get(key); + if (!g) { + g = { + group_id: key, + is_batch: !!r.batch_id, + customer_name: r.customer_name, + customer_email: r.customer_email, + customer_phone: r.customer_phone, + payment_status: r.payment_status, + payment_method: r.payment_method, + paid_at: r.paid_at, + created_at: r.created_at, + total_cents: 0, + intake_all_complete: true, + intake_any_incomplete: false, + max_reminder_count: 0, + last_reminded_at: null, + services: [], + }; + groups.set(key, g); + } + g.total_cents += Number(r.service_fee_cents || 0) + Number(r.gov_fee_cents || 0) + + Number(r.surcharge_cents || 0) - Number(r.discount_cents || 0); + if (!r.intake_data_validated) { g.intake_all_complete = false; g.intake_any_incomplete = true; } + g.max_reminder_count = Math.max(g.max_reminder_count, Number(r.intake_reminder_count || 0)); + if (r.intake_reminder_last_at && (!g.last_reminded_at || r.intake_reminder_last_at > g.last_reminded_at)) { + g.last_reminded_at = r.intake_reminder_last_at; + } + g.services.push({ + order_number: r.order_number, + service_slug: r.service_slug, + service_name: r.service_name || r.service_slug, + fulfillment_status: r.fulfillment_status, + fulfillment_status_at: r.fulfillment_status_at, + intake_data_validated: r.intake_data_validated, + erpnext_sales_order: r.erpnext_sales_order, + ready_to_approve: r.fulfillment_status === "ready_to_file", + }); + } + + res.json({ groups: Array.from(groups.values()) }); + } catch (err) { + console.error("[admin/compliance-orders] Error:", err); + res.status(500).json({ error: "Could not load compliance orders." }); + } +}); + +/** GET /api/v1/admin/compliance-orders/stats — queue overview counts. */ +router.get("/api/v1/admin/compliance-orders/stats", requireAdmin, async (_req, res) => { + try { + const { rows } = await pool.query(` + SELECT + COUNT(*) FILTER (WHERE payment_status = 'paid') AS paid, + COUNT(*) FILTER (WHERE payment_status = 'pending_payment') AS pending_payment, + COUNT(*) FILTER (WHERE payment_status = 'paid' AND COALESCE(intake_data_validated, FALSE) = FALSE) AS paid_intake_incomplete, + COUNT(*) FILTER (WHERE fulfillment_status = 'ready_to_file') AS ready_to_file, + COUNT(*) FILTER (WHERE fulfillment_status = 'awaiting_intake') AS awaiting_intake, + COUNT(*) FILTER (WHERE fulfillment_status = 'completed') AS completed, + COUNT(*) AS total + FROM compliance_orders + `); + res.json(rows[0]); + } catch (err) { + console.error("[admin/compliance-orders/stats] Error:", err); + res.status(500).json({ error: "Could not load stats." }); + } +}); + +/** GET /api/v1/admin/compliance-orders/:order_number — single order full detail. */ +router.get("/api/v1/admin/compliance-orders/:order_number", requireAdmin, async (req, res) => { + try { + const { rows } = await pool.query( + `SELECT co.*, te.legal_name AS entity_name, te.frn AS entity_frn + FROM compliance_orders co + LEFT JOIN telecom_entities te ON te.id = co.telecom_entity_id + WHERE co.order_number = $1`, + [req.params.order_number], + ); + if (rows.length === 0) { res.status(404).json({ error: "Order not found." }); return; } + const order = rows[0]; + const audit = await pool.query( + `SELECT * FROM order_audit_log + WHERE order_number = $1 AND order_type IN ('compliance', 'compliance_batch') + ORDER BY created_at DESC`, + [order.order_number], + ); + res.json({ order, audit_log: audit.rows }); + } catch (err) { + console.error("[admin/compliance-orders/:id] Error:", err); + res.status(500).json({ error: "Could not load order." }); + } +}); + +/** + * POST /api/v1/admin/compliance-orders/:order_number/approve + * Approve a prepared filing held at fulfillment_status='ready_to_file' and + * dispatch the worker to actually submit it to the government system. + */ +router.post("/api/v1/admin/compliance-orders/:order_number/approve", requireAdmin, async (req, res) => { + const id = req.params.order_number; + try { + const { rows } = await pool.query( + `SELECT order_number, service_slug, 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; } + if (order.fulfillment_status !== "ready_to_file") { + res.status(409).json({ + error: `Order is not awaiting submission approval (status=${order.fulfillment_status ?? "none"}).`, + }); + return; + } + + await pool.query( + `UPDATE compliance_orders + SET fulfillment_status = 'authorization_signed', fulfillment_status_at = now(), updated_at = now() + WHERE order_number = $1`, + [id], + ); + await pool.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, 'approved_for_submission', 'ready_to_file', 'authorization_signed', 'admin', $2, $3, $4)`, + [id, req.admin!.id, req.admin!.username, (req.body?.note as string) || "Approved + dispatched for government submission"], + ); + + const workerUrl = process.env.WORKER_URL || "http://workers:8090"; + let dispatched = false; + try { + const r = await fetch(`${workerUrl}/jobs`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "process_compliance_service", + order_name: id, + order_number: id, + service_slug: order.service_slug, + admin_approved: true, + }), + }); + dispatched = r.ok; + } catch (err) { + console.error(`[admin/compliance-orders] approve dispatch failed for ${id}:`, err); + } + res.json({ success: true, order_number: id, dispatched }); + } catch (err) { + console.error(`[admin/compliance-orders] approve error for ${id}:`, err); + res.status(500).json({ error: "Approve failed." }); + } +}); + +/** + * POST /api/v1/admin/compliance-orders/:order_number/rearm-intake + * Re-arm the daily intake reminder for a paid+incomplete order whose reminders + * went quiet (clears intake_reminder_last_at so the next daily run nudges it). + * Optionally resets the count back to 0 with { reset_count: true }. + */ +router.post("/api/v1/admin/compliance-orders/:order_number/rearm-intake", requireAdmin, async (req, res) => { + const id = req.params.order_number; + try { + const resetCount = req.body?.reset_count === true; + const { rows } = await pool.query( + `SELECT order_number, batch_id, payment_status, + COALESCE(intake_data_validated, FALSE) AS intake_data_validated + FROM compliance_orders WHERE order_number = $1`, + [id], + ); + const order = rows[0]; + if (!order) { res.status(404).json({ error: "Order not found." }); return; } + if (order.payment_status !== "paid") { + res.status(409).json({ error: "Only paid orders can be re-armed." }); + return; + } + if (order.intake_data_validated) { + res.status(409).json({ error: "Intake is already complete for this order." }); + return; + } + + // Re-arm the whole batch when this order belongs to one, so a multi-service + // customer gets a single consolidated nudge (matches the worker grouping). + const filter = order.batch_id + ? { clause: "batch_id = $1", val: order.batch_id } + : { clause: "order_number = $1", val: id }; + const updated = await pool.query( + `UPDATE compliance_orders + SET intake_reminder_last_at = NULL + ${resetCount ? ", intake_reminder_count = 0" : ""}, + updated_at = now() + WHERE ${filter.clause} + AND payment_status = 'paid' + AND COALESCE(intake_data_validated, FALSE) = FALSE + RETURNING order_number`, + [filter.val], + ); + await pool.query( + `INSERT INTO order_audit_log (order_type, order_id, order_number, action, actor_type, actor_id, actor_name, note) + VALUES ('compliance', 0, $1, 'intake_reminder_rearmed', 'admin', $2, $3, $4)`, + [id, req.admin!.id, req.admin!.username, + `Re-armed intake reminder for ${updated.rowCount} order(s)${resetCount ? " (count reset to 0)" : ""}`], + ); + res.json({ success: true, rearmed: updated.rowCount }); + } catch (err) { + console.error(`[admin/compliance-orders] rearm-intake error for ${id}:`, err); + res.status(500).json({ error: "Re-arm failed." }); + } +}); + export default router; diff --git a/site/public/admin/compliance-orders/index.html b/site/public/admin/compliance-orders/index.html new file mode 100644 index 0000000..aeb9982 --- /dev/null +++ b/site/public/admin/compliance-orders/index.html @@ -0,0 +1,389 @@ + + + + + + + Compliance Orders | Performance West Admin + + + + + + + + + + + + + + + + + + diff --git a/site/public/admin/index.html b/site/public/admin/index.html index dba3bde..8ff7c51 100644 --- a/site/public/admin/index.html +++ b/site/public/admin/index.html @@ -7,7 +7,7 @@ })(); Admin Dashboard | Performance West Inc.

Admin Login

Performance West Operations

Stay ahead of compliance changes

Regulatory updates, enforcement trends, and compliance tips. No spam.

Stay ahead of compliance changes

Regulatory updates, enforcement trends, and compliance tips. No spam.