feat: pg-boss job queue, notifications, client interactions, bulk email
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, communications, userProfiles } from '../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { eq, and, inArray } from 'drizzle-orm';
|
||||
import { generateEmail, generateSubject, generateBirthdayMessage, type AIProvider } from '../services/ai';
|
||||
import { sendEmail } from '../services/email';
|
||||
import type { User } from '../lib/auth';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export const emailRoutes = new Elysia({ prefix: '/emails' })
|
||||
// Generate email for a client
|
||||
@@ -294,6 +295,147 @@ export const emailRoutes = new Elysia({ prefix: '/emails' })
|
||||
}),
|
||||
})
|
||||
|
||||
// Bulk generate emails
|
||||
.post('/bulk-generate', async ({ body, user }: {
|
||||
body: { clientIds: string[]; purpose: string; provider?: AIProvider };
|
||||
user: User;
|
||||
}) => {
|
||||
const batchId = randomUUID();
|
||||
|
||||
// Get user profile
|
||||
const [profile] = await db.select()
|
||||
.from(userProfiles)
|
||||
.where(eq(userProfiles.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
const advisorInfo = {
|
||||
name: user.name,
|
||||
title: profile?.title || '',
|
||||
company: profile?.company || '',
|
||||
phone: profile?.phone || '',
|
||||
signature: profile?.emailSignature || '',
|
||||
};
|
||||
|
||||
// Get all selected clients
|
||||
const selectedClients = await db.select()
|
||||
.from(clients)
|
||||
.where(and(
|
||||
inArray(clients.id, body.clientIds),
|
||||
eq(clients.userId, user.id),
|
||||
));
|
||||
|
||||
if (selectedClients.length === 0) {
|
||||
throw new Error('No valid clients found');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const client of selectedClients) {
|
||||
try {
|
||||
const content = await generateEmail({
|
||||
advisorName: advisorInfo.name,
|
||||
advisorTitle: advisorInfo.title,
|
||||
advisorCompany: advisorInfo.company,
|
||||
advisorPhone: advisorInfo.phone,
|
||||
advisorSignature: advisorInfo.signature,
|
||||
clientName: client.firstName,
|
||||
interests: client.interests || [],
|
||||
notes: client.notes || '',
|
||||
purpose: body.purpose,
|
||||
provider: body.provider,
|
||||
});
|
||||
|
||||
const subject = await generateSubject(body.purpose, client.firstName, body.provider);
|
||||
|
||||
const [comm] = await db.insert(communications)
|
||||
.values({
|
||||
userId: user.id,
|
||||
clientId: client.id,
|
||||
type: 'email',
|
||||
subject,
|
||||
content,
|
||||
aiGenerated: true,
|
||||
aiModel: body.provider || 'anthropic',
|
||||
status: 'draft',
|
||||
batchId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
results.push({ clientId: client.id, email: comm, success: true });
|
||||
} catch (error: any) {
|
||||
results.push({ clientId: client.id, error: error.message, success: false });
|
||||
}
|
||||
}
|
||||
|
||||
return { batchId, results, total: selectedClients.length, generated: results.filter(r => r.success).length };
|
||||
}, {
|
||||
body: t.Object({
|
||||
clientIds: t.Array(t.String({ format: 'uuid' }), { minItems: 1 }),
|
||||
purpose: t.String({ minLength: 1 }),
|
||||
provider: t.Optional(t.Union([t.Literal('anthropic'), t.Literal('openai')])),
|
||||
}),
|
||||
})
|
||||
|
||||
// Bulk send all drafts in a batch
|
||||
.post('/bulk-send', async ({ body, user }: {
|
||||
body: { batchId: string };
|
||||
user: User;
|
||||
}) => {
|
||||
// Get all drafts in this batch
|
||||
const drafts = await db.select({
|
||||
email: communications,
|
||||
client: clients,
|
||||
})
|
||||
.from(communications)
|
||||
.innerJoin(clients, eq(communications.clientId, clients.id))
|
||||
.where(and(
|
||||
eq(communications.batchId, body.batchId),
|
||||
eq(communications.userId, user.id),
|
||||
eq(communications.status, 'draft'),
|
||||
));
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const { email, client } of drafts) {
|
||||
if (!client.email) {
|
||||
results.push({ id: email.id, success: false, error: 'Client has no email' });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to: client.email,
|
||||
subject: email.subject || 'Message from your advisor',
|
||||
content: email.content,
|
||||
});
|
||||
|
||||
await db.update(communications)
|
||||
.set({ status: 'sent', sentAt: new Date() })
|
||||
.where(eq(communications.id, email.id));
|
||||
|
||||
await db.update(clients)
|
||||
.set({ lastContactedAt: new Date() })
|
||||
.where(eq(clients.id, client.id));
|
||||
|
||||
results.push({ id: email.id, success: true });
|
||||
} catch (error: any) {
|
||||
results.push({ id: email.id, success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
batchId: body.batchId,
|
||||
total: drafts.length,
|
||||
sent: results.filter(r => r.success).length,
|
||||
failed: results.filter(r => !r.success).length,
|
||||
results,
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
batchId: t.String({ minLength: 1 }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Delete draft
|
||||
.delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
const [deleted] = await db.delete(communications)
|
||||
|
||||
Reference in New Issue
Block a user