From 9879b82e5f1ca6c62688be23bfaed2de5e7e720c Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 17 Jun 2026 18:03:47 +0200 Subject: [PATCH] feat(inquiries): website_submissions tracking + display columns; capture populates contact name/email Co-Authored-By: Claude Fable 5 --- src/app/api/public/website-inquiries/route.ts | 7 +++++ .../0093_website_submissions_inquiry_cols.sql | 31 +++++++++++++++++++ src/lib/db/schema/website-submissions.ts | 12 +++++++ 3 files changed, 50 insertions(+) create mode 100644 src/lib/db/migrations/0093_website_submissions_inquiry_cols.sql diff --git a/src/app/api/public/website-inquiries/route.ts b/src/app/api/public/website-inquiries/route.ts index 30430a9b..a247a659 100644 --- a/src/app/api/public/website-inquiries/route.ts +++ b/src/app/api/public/website-inquiries/route.ts @@ -25,6 +25,7 @@ import { autoPromoteWebsiteBerthInquiry, isWebsiteBerthAutopromoteEnabled, } from '@/lib/services/website-intake-promote.service'; +import { extractInquiryFields } from '@/lib/services/website-intake-fields'; /** * POST /api/public/website-inquiries @@ -149,6 +150,10 @@ export async function POST(req: NextRequest) { // hits, `returning()` yields zero rows and we look up the existing row to // return its id, mirroring the first-delivery shape so the website never // sees a difference between fresh and dup. + // Extract contact name/email into real columns so the inquiry list can + // search/sort/display without digging into the JSONB payload per row. + const fields = extractInquiryFields(parsed.payload); + const insertResult = await db .insert(websiteSubmissions) .values({ @@ -157,6 +162,8 @@ export async function POST(req: NextRequest) { kind: parsed.kind, payload: parsed.payload, legacyNocodbId: parsed.legacy_nocodb_id ?? null, + contactName: fields.fullName || null, + contactEmail: fields.email || null, sourceIp: ip, userAgent: req.headers.get('user-agent') ?? null, utmSource: parsed.utm_source ?? null, diff --git a/src/lib/db/migrations/0093_website_submissions_inquiry_cols.sql b/src/lib/db/migrations/0093_website_submissions_inquiry_cols.sql new file mode 100644 index 00000000..88e89f5a --- /dev/null +++ b/src/lib/db/migrations/0093_website_submissions_inquiry_cols.sql @@ -0,0 +1,31 @@ +-- 0093_website_submissions_inquiry_cols.sql +-- ---------------------------------------------------------------------------- +-- Inquiries workbench: tracking + display columns on website_submissions. +-- converted_client_id / converted_interest_id - set when an operator converts +-- an inquiry into CRM entities (FK to clients/interests). +-- contact_name / contact_email - extracted from the JSONB payload at capture +-- time so the list view can search/sort/display via real columns. +-- +-- Idempotent: ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS + a +-- COALESCE backfill that only fills nulls. Safe to re-run. + +ALTER TABLE website_submissions + ADD COLUMN IF NOT EXISTS converted_client_id text REFERENCES clients(id), + ADD COLUMN IF NOT EXISTS converted_interest_id text REFERENCES interests(id), + ADD COLUMN IF NOT EXISTS contact_name text, + ADD COLUMN IF NOT EXISTS contact_email text; + +CREATE INDEX IF NOT EXISTS idx_ws_contact_email + ON website_submissions (port_id, contact_email); + +-- Backfill display columns from existing payloads (only where still null). +UPDATE website_submissions +SET contact_email = COALESCE(contact_email, NULLIF(payload->>'email', '')), + contact_name = COALESCE( + contact_name, + NULLIF(TRIM(CONCAT_WS(' ', payload->>'first_name', payload->>'last_name')), ''), + NULLIF(payload->>'name', ''), + NULLIF(payload->>'fullName', ''), + NULLIF(payload->>'full_name', '') + ) +WHERE contact_email IS NULL OR contact_name IS NULL; diff --git a/src/lib/db/schema/website-submissions.ts b/src/lib/db/schema/website-submissions.ts index 44028ba0..e4d0b4c8 100644 --- a/src/lib/db/schema/website-submissions.ts +++ b/src/lib/db/schema/website-submissions.ts @@ -51,6 +51,18 @@ export const websiteSubmissions = pgTable( * same form submission. Useful for reconciling: pick any submission * here, look up the matching NocoDB row, confirm both halves agree. */ legacyNocodbId: text('legacy_nocodb_id'), + /** Contact name + email extracted from `payload` at capture time so the + * inquiry list can search/sort/display via real columns (payload stays + * JSONB and isn't searched directly). Populated by the capture endpoint + * and backfilled in migration 0093. */ + contactName: text('contact_name'), + contactEmail: text('contact_email'), + /** Set when an operator converts this inquiry into CRM entities. FK enforced + * at the DB level (migration 0093); typed as plain text here to avoid a + * circular schema import — `clients`/`interests` already reference + * `website_submissions`. */ + convertedClientId: text('converted_client_id'), + convertedInterestId: text('converted_interest_id'), /** Capture-time metadata for debugging. */ sourceIp: text('source_ip'), userAgent: text('user_agent'),