feat: global search, client merge/dedup, data export APIs
This commit is contained in:
270
src/routes/export.ts
Normal file
270
src/routes/export.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, communications, events, interactions, clientNotes, emailTemplates, clientSegments } from '../db/schema';
|
||||
import { and, eq, desc } from 'drizzle-orm';
|
||||
import { logAudit, getRequestMeta } from '../services/audit';
|
||||
|
||||
export const exportRoutes = new Elysia({ prefix: '/export' })
|
||||
// Full data export (JSON)
|
||||
.get('/json', async ({ headers, request }) => {
|
||||
const sessionToken = headers['cookie']?.match(/better-auth\.session_token=([^;]+)/)?.[1];
|
||||
const bearerToken = headers['authorization']?.replace('Bearer ', '');
|
||||
|
||||
let userId: string | null = null;
|
||||
if (sessionToken) {
|
||||
const session = await db.query.sessions.findFirst({
|
||||
where: (s, { eq, gt }) => and(eq(s.token, sessionToken), gt(s.expiresAt, new Date())),
|
||||
});
|
||||
userId = session?.userId ?? null;
|
||||
}
|
||||
if (!userId && bearerToken) {
|
||||
const admin = await db.query.users.findFirst({ where: (u, { eq }) => eq(u.role, 'admin') });
|
||||
userId = admin?.id ?? null;
|
||||
}
|
||||
if (!userId) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const [
|
||||
allClients,
|
||||
allEmails,
|
||||
allEvents,
|
||||
allInteractions,
|
||||
allNotes,
|
||||
allTemplates,
|
||||
allSegments,
|
||||
] = await Promise.all([
|
||||
db.select().from(clients).where(eq(clients.userId, userId)).orderBy(desc(clients.createdAt)),
|
||||
db.select().from(communications).where(eq(communications.userId, userId)).orderBy(desc(communications.createdAt)),
|
||||
db.select().from(events).where(eq(events.userId, userId)).orderBy(desc(events.date)),
|
||||
db.select().from(interactions).where(eq(interactions.userId, userId)).orderBy(desc(interactions.createdAt)),
|
||||
db.select().from(clientNotes).where(eq(clientNotes.userId, userId)).orderBy(desc(clientNotes.createdAt)),
|
||||
db.select().from(emailTemplates).where(eq(emailTemplates.userId, userId)).orderBy(desc(emailTemplates.createdAt)),
|
||||
db.select().from(clientSegments).where(eq(clientSegments.userId, userId)).orderBy(desc(clientSegments.createdAt)),
|
||||
]);
|
||||
|
||||
const meta = getRequestMeta(request);
|
||||
await logAudit({
|
||||
userId,
|
||||
action: 'view',
|
||||
entityType: 'export' as any,
|
||||
entityId: 'full-json',
|
||||
details: {
|
||||
clients: allClients.length,
|
||||
emails: allEmails.length,
|
||||
events: allEvents.length,
|
||||
interactions: allInteractions.length,
|
||||
notes: allNotes.length,
|
||||
templates: allTemplates.length,
|
||||
segments: allSegments.length,
|
||||
},
|
||||
...meta,
|
||||
});
|
||||
|
||||
const exportData = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
summary: {
|
||||
clients: allClients.length,
|
||||
emails: allEmails.length,
|
||||
events: allEvents.length,
|
||||
interactions: allInteractions.length,
|
||||
notes: allNotes.length,
|
||||
templates: allTemplates.length,
|
||||
segments: allSegments.length,
|
||||
},
|
||||
data: {
|
||||
clients: allClients,
|
||||
emails: allEmails,
|
||||
events: allEvents,
|
||||
interactions: allInteractions,
|
||||
notes: allNotes,
|
||||
templates: allTemplates,
|
||||
segments: allSegments,
|
||||
},
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(exportData, null, 2), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': `attachment; filename="network-app-export-${new Date().toISOString().split('T')[0]}.json"`,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// CSV export for clients
|
||||
.get('/clients/csv', async ({ headers, request }) => {
|
||||
const sessionToken = headers['cookie']?.match(/better-auth\.session_token=([^;]+)/)?.[1];
|
||||
const bearerToken = headers['authorization']?.replace('Bearer ', '');
|
||||
|
||||
let userId: string | null = null;
|
||||
if (sessionToken) {
|
||||
const session = await db.query.sessions.findFirst({
|
||||
where: (s, { eq, gt }) => and(eq(s.token, sessionToken), gt(s.expiresAt, new Date())),
|
||||
});
|
||||
userId = session?.userId ?? null;
|
||||
}
|
||||
if (!userId && bearerToken) {
|
||||
const admin = await db.query.users.findFirst({ where: (u, { eq }) => eq(u.role, 'admin') });
|
||||
userId = admin?.id ?? null;
|
||||
}
|
||||
if (!userId) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const allClients = await db.select().from(clients).where(eq(clients.userId, userId)).orderBy(desc(clients.createdAt));
|
||||
|
||||
const csvHeaders = [
|
||||
'First Name', 'Last Name', 'Email', 'Phone', 'Company', 'Role', 'Industry',
|
||||
'Stage', 'Street', 'City', 'State', 'Zip',
|
||||
'Birthday', 'Anniversary', 'Interests', 'Tags', 'Notes',
|
||||
'Last Contacted', 'Created At',
|
||||
];
|
||||
|
||||
const escCsv = (v: any) => {
|
||||
if (v == null) return '';
|
||||
const s = String(v);
|
||||
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
||||
return `"${s.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const rows = allClients.map(c => [
|
||||
c.firstName, c.lastName, c.email, c.phone, c.company, c.role, c.industry,
|
||||
c.stage, c.street, c.city, c.state, c.zip,
|
||||
c.birthday?.toISOString().split('T')[0],
|
||||
c.anniversary?.toISOString().split('T')[0],
|
||||
((c.interests as string[]) || []).join('; '),
|
||||
((c.tags as string[]) || []).join('; '),
|
||||
c.notes,
|
||||
c.lastContactedAt?.toISOString(),
|
||||
c.createdAt.toISOString(),
|
||||
].map(escCsv).join(','));
|
||||
|
||||
const csv = [csvHeaders.join(','), ...rows].join('\n');
|
||||
|
||||
const csvMeta = getRequestMeta(request);
|
||||
await logAudit({
|
||||
userId,
|
||||
action: 'view',
|
||||
entityType: 'export' as any,
|
||||
entityId: 'clients-csv',
|
||||
details: { clientCount: allClients.length },
|
||||
...csvMeta,
|
||||
});
|
||||
|
||||
return new Response(csv, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="clients-export-${new Date().toISOString().split('T')[0]}.csv"`,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// CSV export for interactions
|
||||
.get('/interactions/csv', async ({ headers, request }) => {
|
||||
const sessionToken = headers['cookie']?.match(/better-auth\.session_token=([^;]+)/)?.[1];
|
||||
const bearerToken = headers['authorization']?.replace('Bearer ', '');
|
||||
|
||||
let userId: string | null = null;
|
||||
if (sessionToken) {
|
||||
const session = await db.query.sessions.findFirst({
|
||||
where: (s, { eq, gt }) => and(eq(s.token, sessionToken), gt(s.expiresAt, new Date())),
|
||||
});
|
||||
userId = session?.userId ?? null;
|
||||
}
|
||||
if (!userId && bearerToken) {
|
||||
const admin = await db.query.users.findFirst({ where: (u, { eq }) => eq(u.role, 'admin') });
|
||||
userId = admin?.id ?? null;
|
||||
}
|
||||
if (!userId) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const allInteractions = await db
|
||||
.select({
|
||||
id: interactions.id,
|
||||
type: interactions.type,
|
||||
title: interactions.title,
|
||||
description: interactions.description,
|
||||
duration: interactions.duration,
|
||||
contactedAt: interactions.contactedAt,
|
||||
createdAt: interactions.createdAt,
|
||||
clientFirstName: clients.firstName,
|
||||
clientLastName: clients.lastName,
|
||||
clientEmail: clients.email,
|
||||
})
|
||||
.from(interactions)
|
||||
.leftJoin(clients, eq(interactions.clientId, clients.id))
|
||||
.where(eq(interactions.userId, userId))
|
||||
.orderBy(desc(interactions.contactedAt));
|
||||
|
||||
const csvHeaders = ['Client Name', 'Client Email', 'Type', 'Title', 'Description', 'Duration (min)', 'Date', 'Created At'];
|
||||
|
||||
const escCsv = (v: any) => {
|
||||
if (v == null) return '';
|
||||
const s = String(v);
|
||||
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
||||
return `"${s.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const rows = allInteractions.map(i => [
|
||||
`${i.clientFirstName} ${i.clientLastName}`,
|
||||
i.clientEmail,
|
||||
i.type,
|
||||
i.title,
|
||||
i.description,
|
||||
i.duration,
|
||||
i.contactedAt.toISOString(),
|
||||
i.createdAt.toISOString(),
|
||||
].map(escCsv).join(','));
|
||||
|
||||
const csv = [csvHeaders.join(','), ...rows].join('\n');
|
||||
|
||||
return new Response(csv, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="interactions-export-${new Date().toISOString().split('T')[0]}.csv"`,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Export summary/stats
|
||||
.get('/summary', async ({ headers }) => {
|
||||
const sessionToken = headers['cookie']?.match(/better-auth\.session_token=([^;]+)/)?.[1];
|
||||
const bearerToken = headers['authorization']?.replace('Bearer ', '');
|
||||
|
||||
let userId: string | null = null;
|
||||
if (sessionToken) {
|
||||
const session = await db.query.sessions.findFirst({
|
||||
where: (s, { eq, gt }) => and(eq(s.token, sessionToken), gt(s.expiresAt, new Date())),
|
||||
});
|
||||
userId = session?.userId ?? null;
|
||||
}
|
||||
if (!userId && bearerToken) {
|
||||
const admin = await db.query.users.findFirst({ where: (u, { eq }) => eq(u.role, 'admin') });
|
||||
userId = admin?.id ?? null;
|
||||
}
|
||||
if (!userId) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const [clientCount, emailCount, eventCount, interactionCount, noteCount, templateCount, segmentCount] = await Promise.all([
|
||||
db.select({ count: sql`count(*)` }).from(clients).where(eq(clients.userId, userId)),
|
||||
db.select({ count: sql`count(*)` }).from(communications).where(eq(communications.userId, userId)),
|
||||
db.select({ count: sql`count(*)` }).from(events).where(eq(events.userId, userId)),
|
||||
db.select({ count: sql`count(*)` }).from(interactions).where(eq(interactions.userId, userId)),
|
||||
db.select({ count: sql`count(*)` }).from(clientNotes).where(eq(clientNotes.userId, userId)),
|
||||
db.select({ count: sql`count(*)` }).from(emailTemplates).where(eq(emailTemplates.userId, userId)),
|
||||
db.select({ count: sql`count(*)` }).from(clientSegments).where(eq(clientSegments.userId, userId)),
|
||||
]);
|
||||
|
||||
return {
|
||||
clients: Number(clientCount[0]?.count || 0),
|
||||
emails: Number(emailCount[0]?.count || 0),
|
||||
events: Number(eventCount[0]?.count || 0),
|
||||
interactions: Number(interactionCount[0]?.count || 0),
|
||||
notes: Number(noteCount[0]?.count || 0),
|
||||
templates: Number(templateCount[0]?.count || 0),
|
||||
segments: Number(segmentCount[0]?.count || 0),
|
||||
exportFormats: ['json', 'clients-csv', 'interactions-csv'],
|
||||
};
|
||||
});
|
||||
|
||||
// Need sql import
|
||||
import { sql } from 'drizzle-orm';
|
||||
Reference in New Issue
Block a user