feat: add CSV import, activity timeline, and insights endpoints
- POST /api/clients/import/preview - CSV preview with auto column mapping - POST /api/clients/import - Import clients from CSV with custom mapping - GET /api/clients/:id/activity - Activity timeline for client - GET /api/insights - Dashboard AI insights (stale clients, birthdays, follow-ups)
This commit is contained in:
121
src/routes/activity.ts
Normal file
121
src/routes/activity.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, events, communications } from '../db/schema';
|
||||
import { eq, and, desc } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export interface ActivityItem {
|
||||
id: string;
|
||||
type: 'email_sent' | 'email_drafted' | 'event_created' | 'client_contacted' | 'client_created' | 'client_updated';
|
||||
title: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const activityRoutes = new Elysia({ prefix: '/clients' })
|
||||
// Get activity timeline for a client
|
||||
.get('/:id/activity', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
// Verify client belongs to user
|
||||
const [client] = await db.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.id, params.id), eq(clients.userId, user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Client not found');
|
||||
}
|
||||
|
||||
const activities: ActivityItem[] = [];
|
||||
|
||||
// Client creation
|
||||
activities.push({
|
||||
id: `created-${client.id}`,
|
||||
type: 'client_created',
|
||||
title: 'Client added to network',
|
||||
date: client.createdAt.toISOString(),
|
||||
});
|
||||
|
||||
// Client updated (if different from created)
|
||||
if (client.updatedAt.getTime() - client.createdAt.getTime() > 60000) {
|
||||
activities.push({
|
||||
id: `updated-${client.id}`,
|
||||
type: 'client_updated',
|
||||
title: 'Client profile updated',
|
||||
date: client.updatedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Last contacted
|
||||
if (client.lastContactedAt) {
|
||||
activities.push({
|
||||
id: `contacted-${client.id}`,
|
||||
type: 'client_contacted',
|
||||
title: 'Marked as contacted',
|
||||
date: client.lastContactedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Communications (emails)
|
||||
const comms = await db.select()
|
||||
.from(communications)
|
||||
.where(and(
|
||||
eq(communications.clientId, params.id),
|
||||
eq(communications.userId, user.id),
|
||||
))
|
||||
.orderBy(desc(communications.createdAt));
|
||||
|
||||
for (const comm of comms) {
|
||||
if (comm.status === 'sent' && comm.sentAt) {
|
||||
activities.push({
|
||||
id: `email-sent-${comm.id}`,
|
||||
type: 'email_sent',
|
||||
title: `Email sent: ${comm.subject || 'No subject'}`,
|
||||
description: comm.content.substring(0, 150) + (comm.content.length > 150 ? '...' : ''),
|
||||
date: comm.sentAt.toISOString(),
|
||||
metadata: { emailId: comm.id, aiGenerated: comm.aiGenerated },
|
||||
});
|
||||
}
|
||||
|
||||
// Also show drafts
|
||||
if (comm.status === 'draft') {
|
||||
activities.push({
|
||||
id: `email-draft-${comm.id}`,
|
||||
type: 'email_drafted',
|
||||
title: `Email drafted: ${comm.subject || 'No subject'}`,
|
||||
description: comm.content.substring(0, 150) + (comm.content.length > 150 ? '...' : ''),
|
||||
date: comm.createdAt.toISOString(),
|
||||
metadata: { emailId: comm.id, aiGenerated: comm.aiGenerated },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Events
|
||||
const clientEvents = await db.select()
|
||||
.from(events)
|
||||
.where(and(
|
||||
eq(events.clientId, params.id),
|
||||
eq(events.userId, user.id),
|
||||
))
|
||||
.orderBy(desc(events.createdAt));
|
||||
|
||||
for (const event of clientEvents) {
|
||||
activities.push({
|
||||
id: `event-${event.id}`,
|
||||
type: 'event_created',
|
||||
title: `Event: ${event.title}`,
|
||||
description: `${event.type}${event.recurring ? ' (recurring)' : ''}`,
|
||||
date: event.createdAt.toISOString(),
|
||||
metadata: { eventId: event.id, eventType: event.type, eventDate: event.date.toISOString() },
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by date descending
|
||||
activities.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
return activities;
|
||||
}, {
|
||||
params: t.Object({
|
||||
id: t.String({ format: 'uuid' }),
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user