import { authMiddleware } from '../middleware/auth'; import { Elysia, t } from 'elysia'; import { db } from '../db'; import { clientSegments, clients } from '../db/schema'; import { eq, and, desc, or, ilike, inArray, gte, lte, isNotNull, isNull, sql } from 'drizzle-orm'; import type { User } from '../lib/auth'; type SegmentFilters = { stages?: string[]; tags?: string[]; industries?: string[]; cities?: string[]; states?: string[]; lastContactedBefore?: string; lastContactedAfter?: string; createdBefore?: string; createdAfter?: string; hasEmail?: boolean; hasPhone?: boolean; search?: string; }; function buildClientConditions(filters: SegmentFilters, userId: string) { const conditions = [eq(clients.userId, userId)]; if (filters.stages?.length) { conditions.push(inArray(clients.stage, filters.stages)); } if (filters.industries?.length) { conditions.push(inArray(clients.industry, filters.industries)); } if (filters.cities?.length) { conditions.push(inArray(clients.city, filters.cities)); } if (filters.states?.length) { conditions.push(inArray(clients.state, filters.states)); } if (filters.tags?.length) { // Check if client tags JSONB array contains any of the filter tags const tagConditions = filters.tags.map(tag => sql`${clients.tags}::jsonb @> ${JSON.stringify([tag])}::jsonb` ); conditions.push(or(...tagConditions)!); } if (filters.lastContactedBefore) { conditions.push(lte(clients.lastContactedAt, new Date(filters.lastContactedBefore))); } if (filters.lastContactedAfter) { conditions.push(gte(clients.lastContactedAt, new Date(filters.lastContactedAfter))); } if (filters.createdBefore) { conditions.push(lte(clients.createdAt, new Date(filters.createdBefore))); } if (filters.createdAfter) { conditions.push(gte(clients.createdAt, new Date(filters.createdAfter))); } if (filters.hasEmail === true) { conditions.push(isNotNull(clients.email)); } else if (filters.hasEmail === false) { conditions.push(isNull(clients.email)); } if (filters.hasPhone === true) { conditions.push(isNotNull(clients.phone)); } else if (filters.hasPhone === false) { conditions.push(isNull(clients.phone)); } if (filters.search) { const q = `%${filters.search}%`; conditions.push(or( ilike(clients.firstName, q), ilike(clients.lastName, q), ilike(clients.email, q), ilike(clients.company, q), )!); } return conditions; } export const segmentRoutes = new Elysia({ prefix: '/segments' }) .use(authMiddleware) // List saved segments .get('/', async ({ user }: { user: User }) => { return db.select() .from(clientSegments) .where(eq(clientSegments.userId, user.id)) .orderBy(desc(clientSegments.pinned), desc(clientSegments.updatedAt)); }) // Preview segment (apply filters, return matching clients) .post('/preview', async ({ body, user }: { body: { filters: SegmentFilters }; user: User }) => { const conditions = buildClientConditions(body.filters, user.id); const results = await db.select() .from(clients) .where(and(...conditions)) .orderBy(clients.lastName); return { count: results.length, clients: results }; }, { body: t.Object({ filters: t.Object({ stages: t.Optional(t.Array(t.String())), tags: t.Optional(t.Array(t.String())), industries: t.Optional(t.Array(t.String())), cities: t.Optional(t.Array(t.String())), states: t.Optional(t.Array(t.String())), lastContactedBefore: t.Optional(t.String()), lastContactedAfter: t.Optional(t.String()), createdBefore: t.Optional(t.String()), createdAfter: t.Optional(t.String()), hasEmail: t.Optional(t.Boolean()), hasPhone: t.Optional(t.Boolean()), search: t.Optional(t.String()), }), }), }) // Get filter options (unique values for dropdowns) .get('/filter-options', async ({ user }: { user: User }) => { const allClients = await db.select({ industry: clients.industry, city: clients.city, state: clients.state, tags: clients.tags, stage: clients.stage, }) .from(clients) .where(eq(clients.userId, user.id)); const industries = [...new Set(allClients.map(c => c.industry).filter(Boolean))] as string[]; const cities = [...new Set(allClients.map(c => c.city).filter(Boolean))] as string[]; const states = [...new Set(allClients.map(c => c.state).filter(Boolean))] as string[]; const tags = [...new Set(allClients.flatMap(c => (c.tags as string[]) || []))]; const stages = [...new Set(allClients.map(c => c.stage).filter(Boolean))] as string[]; return { industries: industries.sort(), cities: cities.sort(), states: states.sort(), tags: tags.sort(), stages: stages.sort() }; }) // Create segment .post('/', async ({ body, user }: { body: { name: string; description?: string; filters: SegmentFilters; color?: string; pinned?: boolean }; user: User; }) => { const [segment] = await db.insert(clientSegments) .values({ userId: user.id, name: body.name, description: body.description, filters: body.filters, color: body.color || '#3b82f6', pinned: body.pinned || false, }) .returning(); return segment; }, { body: t.Object({ name: t.String({ minLength: 1 }), description: t.Optional(t.String()), filters: t.Object({ stages: t.Optional(t.Array(t.String())), tags: t.Optional(t.Array(t.String())), industries: t.Optional(t.Array(t.String())), cities: t.Optional(t.Array(t.String())), states: t.Optional(t.Array(t.String())), lastContactedBefore: t.Optional(t.String()), lastContactedAfter: t.Optional(t.String()), createdBefore: t.Optional(t.String()), createdAfter: t.Optional(t.String()), hasEmail: t.Optional(t.Boolean()), hasPhone: t.Optional(t.Boolean()), search: t.Optional(t.String()), }), color: t.Optional(t.String()), pinned: t.Optional(t.Boolean()), }), }) // Get segment + matching clients .get('/:id', async ({ params, user }: { params: { id: string }; user: User }) => { const [segment] = await db.select() .from(clientSegments) .where(and(eq(clientSegments.id, params.id), eq(clientSegments.userId, user.id))) .limit(1); if (!segment) throw new Error('Segment not found'); const conditions = buildClientConditions(segment.filters as SegmentFilters, user.id); const matchingClients = await db.select() .from(clients) .where(and(...conditions)) .orderBy(clients.lastName); return { ...segment, clientCount: matchingClients.length, clients: matchingClients }; }, { params: t.Object({ id: t.String({ format: 'uuid' }) }), }) // Update segment .put('/:id', async ({ params, body, user }: { params: { id: string }; body: { name?: string; description?: string; filters?: SegmentFilters; color?: string; pinned?: boolean }; user: User; }) => { const updateData: Record = { updatedAt: new Date() }; if (body.name !== undefined) updateData.name = body.name; if (body.description !== undefined) updateData.description = body.description; if (body.filters !== undefined) updateData.filters = body.filters; if (body.color !== undefined) updateData.color = body.color; if (body.pinned !== undefined) updateData.pinned = body.pinned; const [segment] = await db.update(clientSegments) .set(updateData) .where(and(eq(clientSegments.id, params.id), eq(clientSegments.userId, user.id))) .returning(); if (!segment) throw new Error('Segment not found'); return segment; }, { params: t.Object({ id: t.String({ format: 'uuid' }) }), body: t.Object({ name: t.Optional(t.String({ minLength: 1 })), description: t.Optional(t.String()), filters: t.Optional(t.Object({ stages: t.Optional(t.Array(t.String())), tags: t.Optional(t.Array(t.String())), industries: t.Optional(t.Array(t.String())), cities: t.Optional(t.Array(t.String())), states: t.Optional(t.Array(t.String())), lastContactedBefore: t.Optional(t.String()), lastContactedAfter: t.Optional(t.String()), createdBefore: t.Optional(t.String()), createdAfter: t.Optional(t.String()), hasEmail: t.Optional(t.Boolean()), hasPhone: t.Optional(t.Boolean()), search: t.Optional(t.String()), })), color: t.Optional(t.String()), pinned: t.Optional(t.Boolean()), }), }) // Delete segment .delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => { const [deleted] = await db.delete(clientSegments) .where(and(eq(clientSegments.id, params.id), eq(clientSegments.userId, user.id))) .returning({ id: clientSegments.id }); if (!deleted) throw new Error('Segment not found'); return { success: true, id: deleted.id }; }, { params: t.Object({ id: t.String({ format: 'uuid' }) }), });