feat(10-redo-C): CRM leads module — schema, services, UI (Phase 10 redo)

Stage C of staged Phase 10 redo. Restores dangling phase10-wip work:
- schema.ts: leads expansion + activities/reminders tables + relations
- lead-service.ts / lead-validators.ts service layer
- /admin/leads list + detail + actions, LeadTable/LeadDetail/LeadForm
- LogActivityModal, SendQuoteModal, FollowUpWidget on dashboard
- migration 0005 (applied to prod DB in Stage B, along with
  previously-missing 0001/0003/0004 — root cause of all post-deploy
  500s: prod DB had no migrations applied since 0000)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 20:58:14 +02:00
parent 5aa6614e41
commit 008a43469d
18 changed files with 1972 additions and 3 deletions
+79
View File
@@ -0,0 +1,79 @@
import postgres from "postgres";
import { readFileSync } from "fs";
import { join } from "path";
// Applies 0005_phase_10_crm_leads_activities_reminders.sql (additive-only:
// ALTER TABLE leads ADD COLUMN x8, CREATE TABLE activities/reminders, indexes).
// Idempotent: skips if already applied. Never drops or truncates anything.
async function push() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const sql = postgres(databaseUrl, { max: 1 });
try {
const cols = await sql`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'leads' ORDER BY ordinal_position
`;
const tables = await sql`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name IN ('activities', 'reminders')
`;
const colNames = cols.map((c) => c.column_name);
console.log("Pre-state — leads columns:", colNames.join(", "));
console.log(
"Pre-state — CRM tables:",
tables.map((t) => t.table_name).join(", ") || "none"
);
if (colNames.includes("status") && tables.length === 2) {
console.log("✓ Migration 0005 already applied (skipped)");
process.exit(0);
}
const migrationSql = readFileSync(
join(
__dirname,
"../src/db/migrations/0005_phase_10_crm_leads_activities_reminders.sql"
),
"utf-8"
);
console.log("Applying migration 0005 in a transaction...");
await sql.begin(async (tx) => {
await tx.unsafe(migrationSql);
});
const postCols = await sql`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'leads' ORDER BY ordinal_position
`;
const postTables = await sql`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name IN ('activities', 'reminders')
`;
console.log(
"Post-state — leads columns:",
postCols.map((c) => c.column_name).join(", ")
);
console.log(
"Post-state — CRM tables:",
postTables.map((t) => t.table_name).join(", ")
);
console.log("✓ Migration 0005 applied successfully");
process.exit(0);
} catch (err: unknown) {
console.error(
"Error applying migration:",
err instanceof Error ? err.message : err
);
process.exit(1);
}
}
push();