feat: client pipeline stages + notes system
- 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:
@@ -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
103
src/routes/notes.ts
Normal 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' }),
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user