feat: client pipeline stages + notes system
Some checks failed
CI/CD / check (push) Failing after 19s
CI/CD / deploy (push) Has been skipped

- Added 'stage' column to clients (lead/prospect/onboarding/active/inactive)
- New client_notes table with CRUD API at /clients/:id/notes
- Notes support pinning, editing, and deletion
- Stage field in create/update client endpoints
- Fixed flaky email test (env var interference)
This commit is contained in:
2026-01-30 00:35:49 +00:00
parent 33a0e1d110
commit bb87ba169a
5 changed files with 135 additions and 0 deletions

View File

@@ -78,6 +78,7 @@ const clientSchema = t.Object({
})),
notes: t.Optional(t.String()),
tags: t.Optional(t.Array(t.String())),
stage: t.Optional(t.String()),
});
const updateClientSchema = t.Partial(clientSchema);
@@ -157,6 +158,7 @@ export const clientRoutes = new Elysia({ prefix: '/clients' })
family: body.family,
notes: body.notes,
tags: body.tags || [],
stage: body.stage || 'lead',
})
.returning();
@@ -192,6 +194,7 @@ export const clientRoutes = new Elysia({ prefix: '/clients' })
if (body.family !== undefined) updateData.family = body.family;
if (body.notes !== undefined) updateData.notes = body.notes;
if (body.tags !== undefined) updateData.tags = body.tags;
if (body.stage !== undefined) updateData.stage = body.stage;
const [client] = await db.update(clients)
.set(updateData)

103
src/routes/notes.ts Normal file
View File

@@ -0,0 +1,103 @@
import { Elysia, t } from 'elysia';
import { db } from '../db';
import { clientNotes, clients } from '../db/schema';
import { eq, and, desc } from 'drizzle-orm';
import type { User } from '../lib/auth';
export const notesRoutes = new Elysia({ prefix: '/clients/:clientId/notes' })
// List notes for a client
.get('/', async ({ params, user }: { params: { clientId: string }; user: User }) => {
// Verify client belongs to user
const [client] = await db.select({ id: clients.id })
.from(clients)
.where(and(eq(clients.id, params.clientId), eq(clients.userId, user.id)))
.limit(1);
if (!client) throw new Error('Client not found');
const notes = await db.select()
.from(clientNotes)
.where(eq(clientNotes.clientId, params.clientId))
.orderBy(desc(clientNotes.pinned), desc(clientNotes.createdAt));
return notes;
}, {
params: t.Object({
clientId: t.String({ format: 'uuid' }),
}),
})
// Create note
.post('/', async ({ params, body, user }: { params: { clientId: string }; body: { content: string }; user: User }) => {
// Verify client belongs to user
const [client] = await db.select({ id: clients.id })
.from(clients)
.where(and(eq(clients.id, params.clientId), eq(clients.userId, user.id)))
.limit(1);
if (!client) throw new Error('Client not found');
const [note] = await db.insert(clientNotes)
.values({
clientId: params.clientId,
userId: user.id,
content: body.content,
})
.returning();
return note;
}, {
params: t.Object({
clientId: t.String({ format: 'uuid' }),
}),
body: t.Object({
content: t.String({ minLength: 1 }),
}),
})
// Update note
.put('/:noteId', async ({ params, body, user }: { params: { clientId: string; noteId: string }; body: { content?: string; pinned?: boolean }; user: User }) => {
const updateData: Record<string, unknown> = { updatedAt: new Date() };
if (body.content !== undefined) updateData.content = body.content;
if (body.pinned !== undefined) updateData.pinned = body.pinned;
const [note] = await db.update(clientNotes)
.set(updateData)
.where(and(
eq(clientNotes.id, params.noteId),
eq(clientNotes.clientId, params.clientId),
eq(clientNotes.userId, user.id),
))
.returning();
if (!note) throw new Error('Note not found');
return note;
}, {
params: t.Object({
clientId: t.String({ format: 'uuid' }),
noteId: t.String({ format: 'uuid' }),
}),
body: t.Object({
content: t.Optional(t.String({ minLength: 1 })),
pinned: t.Optional(t.Boolean()),
}),
})
// Delete note
.delete('/:noteId', async ({ params, user }: { params: { clientId: string; noteId: string }; user: User }) => {
const [deleted] = await db.delete(clientNotes)
.where(and(
eq(clientNotes.id, params.noteId),
eq(clientNotes.clientId, params.clientId),
eq(clientNotes.userId, user.id),
))
.returning({ id: clientNotes.id });
if (!deleted) throw new Error('Note not found');
return { success: true, id: deleted.id };
}, {
params: t.Object({
clientId: t.String({ format: 'uuid' }),
noteId: t.String({ format: 'uuid' }),
}),
});