feat: client disable/deletion with soft-delete (#69)
* feat: add client disable/deletion with soft-delete (#67) Add soft-delete support for clients: disable is the default action (hiding from client list and booking flow), with permanent deletion requiring explicit type-to-confirm. Disabled clients remain in reporting and can be re-enabled by staff. - Add client_status enum (active/disabled) and disabled_at column - API defaults GET /api/clients to active-only, ?includeDisabled=true shows all - PATCH /api/clients/:id accepts status field for disable/enable - DELETE requires ?confirm=true query param - Booking flow skips disabled clients - Frontend: show disabled toggle, disable/enable buttons, delete confirmation modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove unused updateClientSchema (lint error) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Groom Book CTO <cto@groombook.app> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #69.
This commit is contained in:
committed by
GitHub
parent
b6b4bc21a0
commit
19e0f5e3ca
@@ -160,11 +160,11 @@ bookRouter.post(
|
||||
);
|
||||
}
|
||||
|
||||
// Find or create client by email
|
||||
// Find or create client by email (skip disabled clients)
|
||||
let [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.email, body.clientEmail));
|
||||
.where(and(eq(clients.email, body.clientEmail), eq(clients.status, "active")));
|
||||
|
||||
if (!client) {
|
||||
const inserted = await db
|
||||
|
||||
@@ -13,12 +13,15 @@ const createClientSchema = z.object({
|
||||
notes: z.string().max(2000).optional(),
|
||||
});
|
||||
|
||||
const updateClientSchema = createClientSchema.partial();
|
||||
|
||||
// List all clients
|
||||
// List clients — defaults to active only, ?includeDisabled=true shows all
|
||||
clientsRouter.get("/", async (c) => {
|
||||
const db = getDb();
|
||||
const rows = await db.select().from(clients).orderBy(clients.name);
|
||||
const includeDisabled = c.req.query("includeDisabled") === "true";
|
||||
const query = includeDisabled
|
||||
? db.select().from(clients).orderBy(clients.name)
|
||||
: db.select().from(clients).where(eq(clients.status, "active")).orderBy(clients.name);
|
||||
const rows = await query;
|
||||
return c.json(rows);
|
||||
});
|
||||
|
||||
@@ -41,16 +44,31 @@ clientsRouter.post("/", zValidator("json", createClientSchema), async (c) => {
|
||||
return c.json(row, 201);
|
||||
});
|
||||
|
||||
// Update a client
|
||||
// Update a client (including status changes)
|
||||
const patchClientSchema = createClientSchema.partial().extend({
|
||||
status: z.enum(["active", "disabled"]).optional(),
|
||||
});
|
||||
|
||||
clientsRouter.patch(
|
||||
"/:id",
|
||||
zValidator("json", updateClientSchema),
|
||||
zValidator("json", patchClientSchema),
|
||||
async (c) => {
|
||||
const db = getDb();
|
||||
const body = c.req.valid("json");
|
||||
const now = new Date();
|
||||
|
||||
const setValues: Record<string, unknown> = { ...body, updatedAt: now };
|
||||
|
||||
// When disabling, set disabledAt; when re-enabling, clear it
|
||||
if (body.status === "disabled") {
|
||||
setValues.disabledAt = now;
|
||||
} else if (body.status === "active") {
|
||||
setValues.disabledAt = null;
|
||||
}
|
||||
|
||||
const [row] = await db
|
||||
.update(clients)
|
||||
.set({ ...body, updatedAt: new Date() })
|
||||
.set(setValues)
|
||||
.where(eq(clients.id, c.req.param("id")))
|
||||
.returning();
|
||||
if (!row) return c.json({ error: "Not found" }, 404);
|
||||
@@ -58,8 +76,16 @@ clientsRouter.patch(
|
||||
}
|
||||
);
|
||||
|
||||
// Delete a client
|
||||
// Delete a client — requires ?confirm=true query param
|
||||
clientsRouter.delete("/:id", async (c) => {
|
||||
const confirm = c.req.query("confirm");
|
||||
if (confirm !== "true") {
|
||||
return c.json(
|
||||
{ error: "Permanent deletion requires ?confirm=true. Consider disabling the client instead." },
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const [row] = await db
|
||||
.delete(clients)
|
||||
|
||||
Reference in New Issue
Block a user