- New client_documents table with file upload/download/delete API - New client_goals table with full CRUD and dashboard overview - New referrals table with stats (top referrers, conversion rate) - All routes follow existing auth middleware pattern
446 lines
17 KiB
TypeScript
446 lines
17 KiB
TypeScript
import { pgTable, text, timestamp, uuid, boolean, jsonb, integer, numeric } from 'drizzle-orm/pg-core';
|
|
import { relations } from 'drizzle-orm';
|
|
|
|
// Users table (managed by BetterAuth - uses text IDs)
|
|
export const users = pgTable('users', {
|
|
id: text('id').primaryKey(),
|
|
email: text('email').notNull().unique(),
|
|
name: text('name').notNull(),
|
|
emailVerified: boolean('email_verified').default(false),
|
|
image: text('image'),
|
|
role: text('role').default('user'), // 'admin' | 'user'
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Invites table
|
|
export const invites = pgTable('invites', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
email: text('email').notNull(),
|
|
name: text('name').notNull(),
|
|
role: text('role').default('user').notNull(),
|
|
token: text('token').notNull().unique(),
|
|
invitedBy: text('invited_by').references(() => users.id, { onDelete: 'set null' }),
|
|
status: text('status').default('pending').notNull(), // 'pending' | 'accepted' | 'expired'
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Password reset tokens
|
|
export const passwordResetTokens = pgTable('password_reset_tokens', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
token: text('token').notNull().unique(),
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
usedAt: timestamp('used_at'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// User profile (additional settings beyond BetterAuth)
|
|
export const userProfiles = pgTable('user_profiles', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull().unique(),
|
|
title: text('title'), // e.g., "Senior Wealth Advisor"
|
|
company: text('company'), // e.g., "ABC Financial Group"
|
|
phone: text('phone'),
|
|
emailSignature: text('email_signature'), // Custom signature block
|
|
communicationStyle: jsonb('communication_style').$type<{
|
|
tone?: 'formal' | 'friendly' | 'casual';
|
|
greeting?: string;
|
|
signoff?: string;
|
|
writingSamples?: string[];
|
|
avoidWords?: string[];
|
|
}>(),
|
|
onboardingComplete: boolean('onboarding_complete').default(false),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// BetterAuth session table
|
|
export const sessions = pgTable('sessions', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
token: text('token').notNull().unique(),
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
ipAddress: text('ip_address'),
|
|
userAgent: text('user_agent'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// BetterAuth account table (for OAuth providers)
|
|
export const accounts = pgTable('accounts', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
accountId: text('account_id').notNull(),
|
|
providerId: text('provider_id').notNull(),
|
|
accessToken: text('access_token'),
|
|
refreshToken: text('refresh_token'),
|
|
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
|
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
|
scope: text('scope'),
|
|
idToken: text('id_token'),
|
|
password: text('password'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// BetterAuth verification table
|
|
export const verifications = pgTable('verifications', {
|
|
id: text('id').primaryKey(),
|
|
identifier: text('identifier').notNull(),
|
|
value: text('value').notNull(),
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Clients table
|
|
export const clients = pgTable('clients', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
|
|
// Basic info
|
|
firstName: text('first_name').notNull(),
|
|
lastName: text('last_name').notNull(),
|
|
email: text('email'),
|
|
phone: text('phone'),
|
|
|
|
// Address
|
|
street: text('street'),
|
|
city: text('city'),
|
|
state: text('state'),
|
|
zip: text('zip'),
|
|
|
|
// Professional
|
|
company: text('company'),
|
|
role: text('role'),
|
|
industry: text('industry'),
|
|
|
|
// Personal
|
|
birthday: timestamp('birthday'),
|
|
anniversary: timestamp('anniversary'),
|
|
interests: jsonb('interests').$type<string[]>().default([]),
|
|
family: jsonb('family').$type<{ spouse?: string; children?: string[] }>(),
|
|
notes: text('notes'),
|
|
|
|
// Organization
|
|
tags: jsonb('tags').$type<string[]>().default([]),
|
|
stage: text('stage').default('lead'), // 'lead' | 'prospect' | 'onboarding' | 'active' | 'inactive'
|
|
|
|
// Tracking
|
|
lastContactedAt: timestamp('last_contacted_at'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Events table (birthdays, anniversaries, follow-ups)
|
|
export const events = pgTable('events', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
|
|
type: text('type').notNull(), // 'birthday' | 'anniversary' | 'followup' | 'custom'
|
|
title: text('title').notNull(),
|
|
date: timestamp('date').notNull(),
|
|
recurring: boolean('recurring').default(false),
|
|
reminderDays: integer('reminder_days').default(7),
|
|
lastTriggered: timestamp('last_triggered'),
|
|
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Communications table (emails, messages)
|
|
export const communications = pgTable('communications', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
|
|
type: text('type').notNull(), // 'email' | 'birthday' | 'followup'
|
|
subject: text('subject'),
|
|
content: text('content').notNull(),
|
|
aiGenerated: boolean('ai_generated').default(false),
|
|
aiModel: text('ai_model'), // Which model was used
|
|
status: text('status').default('draft'), // 'draft' | 'approved' | 'sent'
|
|
sentAt: timestamp('sent_at'),
|
|
batchId: text('batch_id'), // for grouping bulk sends
|
|
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Notifications table
|
|
export const notifications = pgTable('notifications', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
type: text('type').notNull(), // 'event_reminder' | 'interaction' | 'system'
|
|
title: text('title').notNull(),
|
|
message: text('message').notNull(),
|
|
read: boolean('read').default(false),
|
|
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'set null' }),
|
|
eventId: uuid('event_id').references(() => events.id, { onDelete: 'set null' }),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Interactions table (touchpoint logging)
|
|
export const interactions = pgTable('interactions', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
type: text('type').notNull(), // 'call' | 'meeting' | 'email' | 'note' | 'other'
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
duration: integer('duration'), // in minutes
|
|
contactedAt: timestamp('contacted_at').notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Client notes table
|
|
export const clientNotes = pgTable('client_notes', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
content: text('content').notNull(),
|
|
pinned: boolean('pinned').default(false),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Email templates table
|
|
export const emailTemplates = pgTable('email_templates', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
name: text('name').notNull(),
|
|
category: text('category').notNull(), // 'follow-up' | 'birthday' | 'introduction' | 'check-in' | 'thank-you' | 'custom'
|
|
subject: text('subject').notNull(),
|
|
content: text('content').notNull(), // supports {{firstName}}, {{lastName}}, {{company}} placeholders
|
|
isDefault: boolean('is_default').default(false), // mark as default for category
|
|
usageCount: integer('usage_count').default(0),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Saved client filters / segments
|
|
export const clientSegments = pgTable('client_segments', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
filters: jsonb('filters').$type<{
|
|
stages?: string[];
|
|
tags?: string[];
|
|
industries?: string[];
|
|
cities?: string[];
|
|
states?: string[];
|
|
lastContactedBefore?: string; // ISO date
|
|
lastContactedAfter?: string;
|
|
createdBefore?: string;
|
|
createdAfter?: string;
|
|
hasEmail?: boolean;
|
|
hasPhone?: boolean;
|
|
search?: string;
|
|
}>().notNull(),
|
|
color: text('color').default('#3b82f6'), // badge color
|
|
pinned: boolean('pinned').default(false),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Audit logs table (compliance)
|
|
export const auditLogs = pgTable('audit_logs', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
|
|
action: text('action').notNull(), // 'create' | 'update' | 'delete' | 'view' | 'send' | 'login' | 'logout' | 'password_change'
|
|
entityType: text('entity_type').notNull(), // 'client' | 'email' | 'event' | 'template' | 'segment' | 'user' | 'auth' | etc
|
|
entityId: text('entity_id'), // ID of the affected entity
|
|
details: jsonb('details').$type<Record<string, unknown>>(), // what changed
|
|
ipAddress: text('ip_address'),
|
|
userAgent: text('user_agent'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Client Documents table (file attachments)
|
|
export const clientDocuments = pgTable('client_documents', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
name: text('name').notNull(), // user-facing display name
|
|
filename: text('filename').notNull(), // actual filename on disk
|
|
mimeType: text('mime_type').notNull(),
|
|
size: integer('size').notNull(), // bytes
|
|
category: text('category').default('other').notNull(), // 'contract' | 'agreement' | 'id' | 'statement' | 'correspondence' | 'other'
|
|
path: text('path').notNull(), // filesystem path
|
|
notes: text('notes'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Client Goals / Financial Objectives table
|
|
export const clientGoals = pgTable('client_goals', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
category: text('category').default('other').notNull(), // 'retirement' | 'investment' | 'savings' | 'insurance' | 'estate' | 'education' | 'debt' | 'other'
|
|
targetAmount: numeric('target_amount', { precision: 15, scale: 2 }),
|
|
currentAmount: numeric('current_amount', { precision: 15, scale: 2 }).default('0'),
|
|
targetDate: timestamp('target_date'),
|
|
status: text('status').default('on-track').notNull(), // 'on-track' | 'at-risk' | 'behind' | 'completed'
|
|
priority: text('priority').default('medium').notNull(), // 'high' | 'medium' | 'low'
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Referrals table
|
|
export const referrals = pgTable('referrals', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
referrerId: uuid('referrer_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
referredId: uuid('referred_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
|
|
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
|
type: text('type').default('client').notNull(), // 'client' | 'partner' | 'event'
|
|
notes: text('notes'),
|
|
status: text('status').default('pending').notNull(), // 'pending' | 'contacted' | 'converted' | 'lost'
|
|
value: numeric('value', { precision: 15, scale: 2 }), // estimated deal value
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
});
|
|
|
|
// Relations
|
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
clients: many(clients),
|
|
events: many(events),
|
|
communications: many(communications),
|
|
notifications: many(notifications),
|
|
interactions: many(interactions),
|
|
sessions: many(sessions),
|
|
accounts: many(accounts),
|
|
emailTemplates: many(emailTemplates),
|
|
clientSegments: many(clientSegments),
|
|
}));
|
|
|
|
export const emailTemplatesRelations = relations(emailTemplates, ({ one }) => ({
|
|
user: one(users, {
|
|
fields: [emailTemplates.userId],
|
|
references: [users.id],
|
|
}),
|
|
}));
|
|
|
|
export const clientSegmentsRelations = relations(clientSegments, ({ one }) => ({
|
|
user: one(users, {
|
|
fields: [clientSegments.userId],
|
|
references: [users.id],
|
|
}),
|
|
}));
|
|
|
|
export const clientsRelations = relations(clients, ({ one, many }) => ({
|
|
user: one(users, {
|
|
fields: [clients.userId],
|
|
references: [users.id],
|
|
}),
|
|
events: many(events),
|
|
communications: many(communications),
|
|
notes: many(clientNotes),
|
|
interactions: many(interactions),
|
|
documents: many(clientDocuments),
|
|
goals: many(clientGoals),
|
|
referralsMade: many(referrals, { relationName: 'referrer' }),
|
|
referralsReceived: many(referrals, { relationName: 'referred' }),
|
|
}));
|
|
|
|
export const clientDocumentsRelations = relations(clientDocuments, ({ one }) => ({
|
|
client: one(clients, {
|
|
fields: [clientDocuments.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
user: one(users, {
|
|
fields: [clientDocuments.userId],
|
|
references: [users.id],
|
|
}),
|
|
}));
|
|
|
|
export const clientGoalsRelations = relations(clientGoals, ({ one }) => ({
|
|
client: one(clients, {
|
|
fields: [clientGoals.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
user: one(users, {
|
|
fields: [clientGoals.userId],
|
|
references: [users.id],
|
|
}),
|
|
}));
|
|
|
|
export const referralsRelations = relations(referrals, ({ one }) => ({
|
|
referrer: one(clients, {
|
|
fields: [referrals.referrerId],
|
|
references: [clients.id],
|
|
relationName: 'referrer',
|
|
}),
|
|
referred: one(clients, {
|
|
fields: [referrals.referredId],
|
|
references: [clients.id],
|
|
relationName: 'referred',
|
|
}),
|
|
user: one(users, {
|
|
fields: [referrals.userId],
|
|
references: [users.id],
|
|
}),
|
|
}));
|
|
|
|
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
|
user: one(users, {
|
|
fields: [notifications.userId],
|
|
references: [users.id],
|
|
}),
|
|
client: one(clients, {
|
|
fields: [notifications.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
event: one(events, {
|
|
fields: [notifications.eventId],
|
|
references: [events.id],
|
|
}),
|
|
}));
|
|
|
|
export const interactionsRelations = relations(interactions, ({ one }) => ({
|
|
user: one(users, {
|
|
fields: [interactions.userId],
|
|
references: [users.id],
|
|
}),
|
|
client: one(clients, {
|
|
fields: [interactions.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
}));
|
|
|
|
export const clientNotesRelations = relations(clientNotes, ({ one }) => ({
|
|
client: one(clients, {
|
|
fields: [clientNotes.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
user: one(users, {
|
|
fields: [clientNotes.userId],
|
|
references: [users.id],
|
|
}),
|
|
}));
|
|
|
|
export const eventsRelations = relations(events, ({ one }) => ({
|
|
user: one(users, {
|
|
fields: [events.userId],
|
|
references: [users.id],
|
|
}),
|
|
client: one(clients, {
|
|
fields: [events.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
}));
|
|
|
|
export const communicationsRelations = relations(communications, ({ one }) => ({
|
|
user: one(users, {
|
|
fields: [communications.userId],
|
|
references: [users.id],
|
|
}),
|
|
client: one(clients, {
|
|
fields: [communications.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
}));
|