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:
+65
-3
@@ -362,14 +362,62 @@ export const quotes = pgTable("quotes", {
|
||||
.defaultNow(),
|
||||
});
|
||||
|
||||
// ============ LEADS TABLE (CRM — Phase 10, but needed for quotes FK) ============
|
||||
// Placeholder table for Phase 10 CRM integration
|
||||
// ============ LEADS TABLE (CRM — Phase 10) ============
|
||||
export const leads = pgTable("leads", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
email: text("email"),
|
||||
phone: text("phone"),
|
||||
company: text("company"),
|
||||
status: text("status")
|
||||
.notNull()
|
||||
.default("contacted"), // contacted | qualified | proposal_sent | negotiating | won | lost
|
||||
last_contact_date: timestamp("last_contact_date", { withTimezone: true }),
|
||||
next_action: text("next_action"),
|
||||
next_action_date: timestamp("next_action_date", { withTimezone: true }),
|
||||
notes: text("notes"),
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updated_at: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
});
|
||||
|
||||
// ============ ACTIVITIES TABLE (CRM interaction history) ============
|
||||
export const activities = pgTable("activities", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
lead_id: text("lead_id")
|
||||
.notNull()
|
||||
.references(() => leads.id, { onDelete: "cascade" }),
|
||||
type: text("type")
|
||||
.notNull(), // call | email | meeting | note
|
||||
duration_minutes: integer("duration_minutes"),
|
||||
notes: text("notes").notNull(),
|
||||
activity_date: timestamp("activity_date", { withTimezone: true })
|
||||
.notNull(),
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
});
|
||||
|
||||
// ============ REMINDERS TABLE (CRM follow-up reminders) ============
|
||||
export const reminders = pgTable("reminders", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
lead_id: text("lead_id")
|
||||
.notNull()
|
||||
.references(() => leads.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
due_date: timestamp("due_date", { withTimezone: true })
|
||||
.notNull(),
|
||||
completed_at: timestamp("completed_at", { withTimezone: true }),
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
@@ -497,6 +545,16 @@ export const offerPhaseServicesRelations = relations(offer_phase_services, ({ on
|
||||
|
||||
export const leadsRelations = relations(leads, ({ many }) => ({
|
||||
quotes: many(quotes),
|
||||
activities: many(activities),
|
||||
reminders: many(reminders),
|
||||
}));
|
||||
|
||||
export const activitiesRelations = relations(activities, ({ one }) => ({
|
||||
lead: one(leads, { fields: [activities.lead_id], references: [leads.id] }),
|
||||
}));
|
||||
|
||||
export const remindersRelations = relations(reminders, ({ one }) => ({
|
||||
lead: one(leads, { fields: [reminders.lead_id], references: [leads.id] }),
|
||||
}));
|
||||
|
||||
export const quotesRelations = relations(quotes, ({ one, many }) => ({
|
||||
@@ -553,4 +611,8 @@ export type NewOfferPhaseService = typeof offer_phase_services.$inferInsert;
|
||||
export type Quote = typeof quotes.$inferSelect;
|
||||
export type NewQuote = typeof quotes.$inferInsert;
|
||||
export type Lead = typeof leads.$inferSelect;
|
||||
export type NewLead = typeof leads.$inferInsert;
|
||||
export type NewLead = typeof leads.$inferInsert;
|
||||
export type Activity = typeof activities.$inferSelect;
|
||||
export type NewActivity = typeof activities.$inferInsert;
|
||||
export type Reminder = typeof reminders.$inferSelect;
|
||||
export type NewReminder = typeof reminders.$inferInsert;
|
||||
Reference in New Issue
Block a user