Add user profile API with name, title, company, phone, signature
This commit is contained in:
@@ -12,6 +12,18 @@ export const users = pgTable('users', {
|
||||
updatedAt: timestamp('updated_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
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// BetterAuth session table
|
||||
export const sessions = pgTable('sessions', {
|
||||
id: text('id').primaryKey(),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { auth } from './lib/auth';
|
||||
import { clientRoutes } from './routes/clients';
|
||||
import { emailRoutes } from './routes/emails';
|
||||
import { eventRoutes } from './routes/events';
|
||||
import { profileRoutes } from './routes/profile';
|
||||
import type { User } from './lib/auth';
|
||||
|
||||
const app = new Elysia()
|
||||
@@ -41,6 +42,7 @@ const app = new Elysia()
|
||||
.use(clientRoutes)
|
||||
.use(emailRoutes)
|
||||
.use(eventRoutes)
|
||||
.use(profileRoutes)
|
||||
)
|
||||
|
||||
// Error handler
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, communications } from '../db/schema';
|
||||
import { clients, communications, userProfiles } from '../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { generateEmail, generateSubject, generateBirthdayMessage, type AIProvider } from '../services/ai';
|
||||
import { sendEmail } from '../services/email';
|
||||
@@ -26,6 +26,21 @@ export const emailRoutes = new Elysia({ prefix: '/emails' })
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
// Get user profile for signature
|
||||
const [profile] = await db.select()
|
||||
.from(userProfiles)
|
||||
.where(eq(userProfiles.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
// Build advisor info
|
||||
const advisorInfo = {
|
||||
name: user.name,
|
||||
title: profile?.title || '',
|
||||
company: profile?.company || '',
|
||||
phone: profile?.phone || '',
|
||||
signature: profile?.emailSignature || '',
|
||||
};
|
||||
|
||||
// Generate email content
|
||||
console.log(`[${new Date().toISOString()}] Generating email for client ${client.firstName}, purpose: ${body.purpose}`);
|
||||
let content: string;
|
||||
@@ -33,7 +48,11 @@ export const emailRoutes = new Elysia({ prefix: '/emails' })
|
||||
|
||||
try {
|
||||
content = await generateEmail({
|
||||
advisorName: user.name,
|
||||
advisorName: advisorInfo.name,
|
||||
advisorTitle: advisorInfo.title,
|
||||
advisorCompany: advisorInfo.company,
|
||||
advisorPhone: advisorInfo.phone,
|
||||
advisorSignature: advisorInfo.signature,
|
||||
clientName: client.firstName,
|
||||
interests: client.interests || [],
|
||||
notes: client.notes || '',
|
||||
|
||||
99
src/routes/profile.ts
Normal file
99
src/routes/profile.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { users, userProfiles } from '../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const profileRoutes = new Elysia({ prefix: '/profile' })
|
||||
// Get current user's profile
|
||||
.get('/', async ({ user }: { user: User }) => {
|
||||
// Get user and profile
|
||||
const [profile] = await db.select()
|
||||
.from(userProfiles)
|
||||
.where(eq(userProfiles.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
title: profile?.title || null,
|
||||
company: profile?.company || null,
|
||||
phone: profile?.phone || null,
|
||||
emailSignature: profile?.emailSignature || null,
|
||||
};
|
||||
})
|
||||
|
||||
// Update profile
|
||||
.put('/', async ({ body, user }: {
|
||||
body: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
company?: string;
|
||||
phone?: string;
|
||||
emailSignature?: string;
|
||||
};
|
||||
user: User;
|
||||
}) => {
|
||||
// Update user name if provided
|
||||
if (body.name) {
|
||||
await db.update(users)
|
||||
.set({ name: body.name, updatedAt: new Date() })
|
||||
.where(eq(users.id, user.id));
|
||||
}
|
||||
|
||||
// Upsert profile
|
||||
const profileData = {
|
||||
title: body.title,
|
||||
company: body.company,
|
||||
phone: body.phone,
|
||||
emailSignature: body.emailSignature,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const [existing] = await db.select()
|
||||
.from(userProfiles)
|
||||
.where(eq(userProfiles.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
await db.update(userProfiles)
|
||||
.set(profileData)
|
||||
.where(eq(userProfiles.userId, user.id));
|
||||
} else {
|
||||
await db.insert(userProfiles).values({
|
||||
userId: user.id,
|
||||
...profileData,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
// Return updated profile
|
||||
const [profile] = await db.select()
|
||||
.from(userProfiles)
|
||||
.where(eq(userProfiles.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
const [updatedUser] = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, user.id))
|
||||
.limit(1);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: updatedUser?.name || user.name,
|
||||
email: user.email,
|
||||
title: profile?.title || null,
|
||||
company: profile?.company || null,
|
||||
phone: profile?.phone || null,
|
||||
emailSignature: profile?.emailSignature || null,
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.Optional(t.String({ minLength: 1 })),
|
||||
title: t.Optional(t.String()),
|
||||
company: t.Optional(t.String()),
|
||||
phone: t.Optional(t.String()),
|
||||
emailSignature: t.Optional(t.String()),
|
||||
}),
|
||||
});
|
||||
@@ -29,18 +29,30 @@ const emailPrompt = ChatPromptTemplate.fromMessages([
|
||||
['system', `You are a professional wealth advisor writing to a valued client.
|
||||
Maintain a warm but professional tone. Incorporate personal details naturally.
|
||||
Keep emails concise (3-4 paragraphs max).
|
||||
Do not include subject line - just the body.`],
|
||||
['human', `Advisor: {advisorName}
|
||||
Do not include subject line - just the body.
|
||||
Always sign off with the advisor's actual name and details provided. Never use placeholders like [Your Name].`],
|
||||
['human', `Advisor Info:
|
||||
- Name: {advisorName}
|
||||
- Title: {advisorTitle}
|
||||
- Company: {advisorCompany}
|
||||
- Phone: {advisorPhone}
|
||||
- Custom signature: {advisorSignature}
|
||||
|
||||
Client: {clientName}
|
||||
Their interests: {interests}
|
||||
Recent notes: {notes}
|
||||
Purpose: {purpose}
|
||||
|
||||
Generate a personalized email that feels genuine, not templated.`],
|
||||
Purpose of email: {purpose}
|
||||
|
||||
Generate a personalized email that feels genuine, not templated. End with an appropriate signature using the advisor's real name and details.`],
|
||||
]);
|
||||
|
||||
export interface GenerateEmailParams {
|
||||
advisorName: string;
|
||||
advisorTitle?: string;
|
||||
advisorCompany?: string;
|
||||
advisorPhone?: string;
|
||||
advisorSignature?: string;
|
||||
clientName: string;
|
||||
interests: string[];
|
||||
notes: string;
|
||||
@@ -55,6 +67,10 @@ export async function generateEmail(params: GenerateEmailParams): Promise<string
|
||||
|
||||
const response = await chain.invoke({
|
||||
advisorName: params.advisorName,
|
||||
advisorTitle: params.advisorTitle || 'Wealth Advisor',
|
||||
advisorCompany: params.advisorCompany || '',
|
||||
advisorPhone: params.advisorPhone || '',
|
||||
advisorSignature: params.advisorSignature || '',
|
||||
clientName: params.clientName,
|
||||
interests: params.interests.join(', ') || 'not specified',
|
||||
notes: params.notes || 'No recent notes',
|
||||
|
||||
Reference in New Issue
Block a user