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>
This commit is contained in:
@@ -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
|
let [client] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.email, body.clientEmail));
|
.where(and(eq(clients.email, body.clientEmail), eq(clients.status, "active")));
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
const inserted = await db
|
const inserted = await db
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ const createClientSchema = z.object({
|
|||||||
|
|
||||||
const updateClientSchema = createClientSchema.partial();
|
const updateClientSchema = createClientSchema.partial();
|
||||||
|
|
||||||
// List all clients
|
// List clients — defaults to active only, ?includeDisabled=true shows all
|
||||||
clientsRouter.get("/", async (c) => {
|
clientsRouter.get("/", async (c) => {
|
||||||
const db = getDb();
|
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);
|
return c.json(rows);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,16 +45,31 @@ clientsRouter.post("/", zValidator("json", createClientSchema), async (c) => {
|
|||||||
return c.json(row, 201);
|
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(
|
clientsRouter.patch(
|
||||||
"/:id",
|
"/:id",
|
||||||
zValidator("json", updateClientSchema),
|
zValidator("json", patchClientSchema),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const body = c.req.valid("json");
|
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
|
const [row] = await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({ ...body, updatedAt: new Date() })
|
.set(setValues)
|
||||||
.where(eq(clients.id, c.req.param("id")))
|
.where(eq(clients.id, c.req.param("id")))
|
||||||
.returning();
|
.returning();
|
||||||
if (!row) return c.json({ error: "Not found" }, 404);
|
if (!row) return c.json({ error: "Not found" }, 404);
|
||||||
@@ -58,8 +77,16 @@ clientsRouter.patch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete a client
|
// Delete a client — requires ?confirm=true query param
|
||||||
clientsRouter.delete("/:id", async (c) => {
|
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 db = getDb();
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.delete(clients)
|
.delete(clients)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const MOCK_CLIENTS = [
|
|||||||
phone: "555-0101",
|
phone: "555-0101",
|
||||||
address: null,
|
address: null,
|
||||||
notes: null,
|
notes: null,
|
||||||
|
emailOptOut: false,
|
||||||
|
status: "active",
|
||||||
|
disabledAt: null,
|
||||||
createdAt: "2026-01-01T00:00:00.000Z",
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
},
|
},
|
||||||
@@ -24,6 +27,9 @@ const MOCK_CLIENTS = [
|
|||||||
phone: null,
|
phone: null,
|
||||||
address: null,
|
address: null,
|
||||||
notes: null,
|
notes: null,
|
||||||
|
emailOptOut: false,
|
||||||
|
status: "active",
|
||||||
|
disabledAt: null,
|
||||||
createdAt: "2026-01-02T00:00:00.000Z",
|
createdAt: "2026-01-02T00:00:00.000Z",
|
||||||
updatedAt: "2026-01-02T00:00:00.000Z",
|
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||||
},
|
},
|
||||||
|
|||||||
+140
-15
@@ -64,6 +64,10 @@ export function ClientsPage() {
|
|||||||
const [savingPet, setSavingPet] = useState(false);
|
const [savingPet, setSavingPet] = useState(false);
|
||||||
const [deletingPetId, setDeletingPetId] = useState<string | null>(null);
|
const [deletingPetId, setDeletingPetId] = useState<string | null>(null);
|
||||||
const [deletingClient, setDeletingClient] = useState(false);
|
const [deletingClient, setDeletingClient] = useState(false);
|
||||||
|
const [disablingClient, setDisablingClient] = useState(false);
|
||||||
|
const [showDisabled, setShowDisabled] = useState(false);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [deleteConfirmName, setDeleteConfirmName] = useState("");
|
||||||
|
|
||||||
// Visit log
|
// Visit log
|
||||||
const [logPetId, setLogPetId] = useState<string | null>(null);
|
const [logPetId, setLogPetId] = useState<string | null>(null);
|
||||||
@@ -74,17 +78,18 @@ export function ClientsPage() {
|
|||||||
const [logFormError, setLogFormError] = useState<string | null>(null);
|
const [logFormError, setLogFormError] = useState<string | null>(null);
|
||||||
const [savingLog, setSavingLog] = useState(false);
|
const [savingLog, setSavingLog] = useState(false);
|
||||||
|
|
||||||
async function loadClients() {
|
async function loadClients(includeDisabled = false) {
|
||||||
const r = await fetch("/api/clients");
|
const url = includeDisabled ? "/api/clients?includeDisabled=true" : "/api/clients";
|
||||||
|
const r = await fetch(url);
|
||||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
setClients((await r.json()) as Client[]);
|
setClients((await r.json()) as Client[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadClients()
|
loadClients(showDisabled)
|
||||||
.catch((e: unknown) => setError(e instanceof Error ? e.message : "Unknown error"))
|
.catch((e: unknown) => setError(e instanceof Error ? e.message : "Unknown error"))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, [showDisabled]);
|
||||||
|
|
||||||
async function loadPets(clientId: string) {
|
async function loadPets(clientId: string) {
|
||||||
setPetsLoading(true);
|
setPetsLoading(true);
|
||||||
@@ -146,7 +151,7 @@ export function ClientsPage() {
|
|||||||
}
|
}
|
||||||
const updated = (await res.json()) as Client;
|
const updated = (await res.json()) as Client;
|
||||||
setShowClientForm(false);
|
setShowClientForm(false);
|
||||||
await loadClients();
|
await loadClients(showDisabled);
|
||||||
if (editingClient) setSelectedClient(updated);
|
if (editingClient) setSelectedClient(updated);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setClientFormError(e instanceof Error ? e.message : "Failed to save");
|
setClientFormError(e instanceof Error ? e.message : "Failed to save");
|
||||||
@@ -198,18 +203,64 @@ export function ClientsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function disableClient(clientId: string) {
|
||||||
|
if (!window.confirm("Disable this client? They will be hidden from the client list and booking flow.")) return;
|
||||||
|
setDisablingClient(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/clients/${clientId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: "disabled" }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = (await res.json()) as { error?: string };
|
||||||
|
throw new Error(err.error ?? `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const updated = (await res.json()) as Client;
|
||||||
|
setSelectedClient(updated);
|
||||||
|
await loadClients(showDisabled);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
alert(e instanceof Error ? e.message : "Failed to disable client");
|
||||||
|
} finally {
|
||||||
|
setDisablingClient(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableClient(clientId: string) {
|
||||||
|
setDisablingClient(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/clients/${clientId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: "active" }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = (await res.json()) as { error?: string };
|
||||||
|
throw new Error(err.error ?? `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const updated = (await res.json()) as Client;
|
||||||
|
setSelectedClient(updated);
|
||||||
|
await loadClients(showDisabled);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
alert(e instanceof Error ? e.message : "Failed to re-enable client");
|
||||||
|
} finally {
|
||||||
|
setDisablingClient(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteClient(clientId: string) {
|
async function deleteClient(clientId: string) {
|
||||||
if (!window.confirm("Delete this client and all their pets? This cannot be undone.")) return;
|
|
||||||
setDeletingClient(true);
|
setDeletingClient(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/clients/${clientId}`, { method: "DELETE" });
|
const res = await fetch(`/api/clients/${clientId}?confirm=true`, { method: "DELETE" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = (await res.json()) as { error?: string };
|
const err = (await res.json()) as { error?: string };
|
||||||
throw new Error(err.error ?? `HTTP ${res.status}`);
|
throw new Error(err.error ?? `HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
setSelectedClient(null);
|
setSelectedClient(null);
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setDeleteConfirmName("");
|
||||||
setPets([]);
|
setPets([]);
|
||||||
await loadClients();
|
await loadClients(showDisabled);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
alert(e instanceof Error ? e.message : "Failed to delete client");
|
alert(e instanceof Error ? e.message : "Failed to delete client");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -324,8 +375,16 @@ export function ClientsPage() {
|
|||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
style={{ ...inputStyle, marginBottom: "0.75rem" }}
|
style={{ ...inputStyle, marginBottom: "0.5rem" }}
|
||||||
/>
|
/>
|
||||||
|
<label style={{ display: "flex", alignItems: "center", gap: "0.4rem", fontSize: 12, color: "#6b7280", marginBottom: "0.75rem", cursor: "pointer" }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showDisabled}
|
||||||
|
onChange={(e) => setShowDisabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Show disabled clients
|
||||||
|
</label>
|
||||||
{filtered.length === 0 && <p style={{ color: "#6b7280", fontSize: 14 }}>No clients found.</p>}
|
{filtered.length === 0 && <p style={{ color: "#6b7280", fontSize: 14 }}>No clients found.</p>}
|
||||||
{filtered.map((c) => (
|
{filtered.map((c) => (
|
||||||
<div
|
<div
|
||||||
@@ -337,7 +396,14 @@ export function ClientsPage() {
|
|||||||
border: selectedClient?.id === c.id ? "1px solid #bfdbfe" : "1px solid transparent",
|
border: selectedClient?.id === c.id ? "1px solid #bfdbfe" : "1px solid transparent",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14 }}>{c.name}</div>
|
<div style={{ fontWeight: 600, fontSize: 14, display: "flex", alignItems: "center", gap: "0.4rem" }}>
|
||||||
|
{c.name}
|
||||||
|
{c.status === "disabled" && (
|
||||||
|
<span style={{ fontSize: 10, background: "#fef2f2", color: "#dc2626", padding: "0.1rem 0.4rem", borderRadius: 4, fontWeight: 500 }}>
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{c.email && <div style={{ fontSize: 12, color: "#6b7280" }}>{c.email}</div>}
|
{c.email && <div style={{ fontSize: 12, color: "#6b7280" }}>{c.email}</div>}
|
||||||
{c.phone && <div style={{ fontSize: 12, color: "#6b7280" }}>{c.phone}</div>}
|
{c.phone && <div style={{ fontSize: 12, color: "#6b7280" }}>{c.phone}</div>}
|
||||||
</div>
|
</div>
|
||||||
@@ -349,7 +415,14 @@ export function ClientsPage() {
|
|||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ display: "flex", alignItems: "flex-start", marginBottom: "1rem" }}>
|
<div style={{ display: "flex", alignItems: "flex-start", marginBottom: "1rem" }}>
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ margin: "0 0 0.2rem" }}>{selectedClient.name}</h2>
|
<h2 style={{ margin: "0 0 0.2rem", display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
|
{selectedClient.name}
|
||||||
|
{selectedClient.status === "disabled" && (
|
||||||
|
<span style={{ fontSize: 12, background: "#fef2f2", color: "#dc2626", padding: "0.15rem 0.5rem", borderRadius: 4, fontWeight: 500 }}>
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
{selectedClient.email && <div style={{ fontSize: 14, color: "#6b7280" }}>{selectedClient.email}</div>}
|
{selectedClient.email && <div style={{ fontSize: 14, color: "#6b7280" }}>{selectedClient.email}</div>}
|
||||||
{selectedClient.phone && <div style={{ fontSize: 14, color: "#6b7280" }}>{selectedClient.phone}</div>}
|
{selectedClient.phone && <div style={{ fontSize: 14, color: "#6b7280" }}>{selectedClient.phone}</div>}
|
||||||
{selectedClient.address && <div style={{ fontSize: 13, color: "#6b7280" }}>{selectedClient.address}</div>}
|
{selectedClient.address && <div style={{ fontSize: 13, color: "#6b7280" }}>{selectedClient.address}</div>}
|
||||||
@@ -364,17 +437,33 @@ export function ClientsPage() {
|
|||||||
href={`/?impersonate=true&clientName=${encodeURIComponent(selectedClient.name)}&staffName=${encodeURIComponent("Staff")}&reason=${encodeURIComponent(`Support view for ${selectedClient.name}`)}`}
|
href={`/?impersonate=true&clientName=${encodeURIComponent(selectedClient.name)}&staffName=${encodeURIComponent("Staff")}&reason=${encodeURIComponent(`Support view for ${selectedClient.name}`)}`}
|
||||||
style={{ ...btnStyle, backgroundColor: "#fef3c7", color: "#92400e", borderColor: "#fde68a", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "0.3rem" }}
|
style={{ ...btnStyle, backgroundColor: "#fef3c7", color: "#92400e", borderColor: "#fde68a", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "0.3rem" }}
|
||||||
>
|
>
|
||||||
👁 View as Customer
|
View as Customer
|
||||||
</a>
|
</a>
|
||||||
<button onClick={() => openEditClient(selectedClient)} style={btnStyle}>
|
<button onClick={() => openEditClient(selectedClient)} style={btnStyle}>
|
||||||
Edit client
|
Edit client
|
||||||
</button>
|
</button>
|
||||||
|
{selectedClient.status === "active" ? (
|
||||||
|
<button
|
||||||
|
onClick={() => { void disableClient(selectedClient.id); }}
|
||||||
|
disabled={disablingClient}
|
||||||
|
style={{ ...btnStyle, color: "#d97706", borderColor: "#fde68a" }}
|
||||||
|
>
|
||||||
|
{disablingClient ? "Disabling…" : "Disable client"}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => { void enableClient(selectedClient.id); }}
|
||||||
|
disabled={disablingClient}
|
||||||
|
style={{ ...btnStyle, color: "#059669", borderColor: "#6ee7b7" }}
|
||||||
|
>
|
||||||
|
{disablingClient ? "Enabling…" : "Re-enable client"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => { void deleteClient(selectedClient.id); }}
|
onClick={() => { setShowDeleteConfirm(true); setDeleteConfirmName(""); }}
|
||||||
disabled={deletingClient}
|
|
||||||
style={{ ...btnStyle, color: "#dc2626", borderColor: "#fca5a5" }}
|
style={{ ...btnStyle, color: "#dc2626", borderColor: "#fca5a5" }}
|
||||||
>
|
>
|
||||||
{deletingClient ? "Deleting…" : "Delete client"}
|
Delete permanently
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -651,6 +740,42 @@ export function ClientsPage() {
|
|||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Delete confirmation modal ── */}
|
||||||
|
{showDeleteConfirm && selectedClient && (
|
||||||
|
<Modal onClose={() => setShowDeleteConfirm(false)}>
|
||||||
|
<h2 style={{ marginTop: 0, color: "#dc2626" }}>Permanently Delete Client</h2>
|
||||||
|
<p style={{ fontSize: 14, color: "#374151" }}>
|
||||||
|
This will permanently delete <strong>{selectedClient.name}</strong> and all their pets. This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<p style={{ fontSize: 14, color: "#374151" }}>
|
||||||
|
Consider disabling the client instead, which preserves their data for reporting.
|
||||||
|
</p>
|
||||||
|
<Field label={`Type "${selectedClient.name}" to confirm`}>
|
||||||
|
<input
|
||||||
|
value={deleteConfirmName}
|
||||||
|
onChange={(e) => setDeleteConfirmName(e.target.value)}
|
||||||
|
style={inputStyle}
|
||||||
|
placeholder={selectedClient.name}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem", marginTop: "1rem" }}>
|
||||||
|
<button
|
||||||
|
onClick={() => { void deleteClient(selectedClient.id); }}
|
||||||
|
disabled={deletingClient || deleteConfirmName !== selectedClient.name}
|
||||||
|
style={{
|
||||||
|
...btnStyle,
|
||||||
|
backgroundColor: deleteConfirmName === selectedClient.name ? "#dc2626" : "#f3f4f6",
|
||||||
|
color: deleteConfirmName === selectedClient.name ? "#fff" : "#9ca3af",
|
||||||
|
borderColor: deleteConfirmName === selectedClient.name ? "#dc2626" : "#d1d5db",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deletingClient ? "Deleting…" : "Delete permanently"}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setShowDeleteConfirm(false)} style={btnStyle}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add client status (soft-delete support)
|
||||||
|
CREATE TYPE "client_status" AS ENUM ('active', 'disabled');
|
||||||
|
|
||||||
|
ALTER TABLE "clients"
|
||||||
|
ADD COLUMN "status" "client_status" NOT NULL DEFAULT 'active',
|
||||||
|
ADD COLUMN "disabled_at" timestamp;
|
||||||
@@ -57,6 +57,20 @@
|
|||||||
"when": 1773820800000,
|
"when": 1773820800000,
|
||||||
"tag": "0007_tip_splitting",
|
"tag": "0007_tip_splitting",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773907200000,
|
||||||
|
"tag": "0008_business_settings",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773993600000,
|
||||||
|
"tag": "0009_client_soft_delete",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,11 @@ export const paymentMethodEnum = pgEnum("payment_method", [
|
|||||||
"other",
|
"other",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const clientStatusEnum = pgEnum("client_status", [
|
||||||
|
"active",
|
||||||
|
"disabled",
|
||||||
|
]);
|
||||||
|
|
||||||
// ─── Tables ───────────────────────────────────────────────────────────────────
|
// ─── Tables ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const clients = pgTable("clients", {
|
export const clients = pgTable("clients", {
|
||||||
@@ -53,6 +58,8 @@ export const clients = pgTable("clients", {
|
|||||||
notes: text("notes"),
|
notes: text("notes"),
|
||||||
// Set to true if the client has opted out of email reminders/notifications
|
// Set to true if the client has opted out of email reminders/notifications
|
||||||
emailOptOut: boolean("email_opt_out").notNull().default(false),
|
emailOptOut: boolean("email_opt_out").notNull().default(false),
|
||||||
|
status: clientStatusEnum("status").notNull().default("active"),
|
||||||
|
disabledAt: timestamp("disabled_at"),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export type AppointmentStatus =
|
|||||||
| "cancelled"
|
| "cancelled"
|
||||||
| "no_show";
|
| "no_show";
|
||||||
|
|
||||||
|
export type ClientStatus = "active" | "disabled";
|
||||||
|
|
||||||
export interface Client {
|
export interface Client {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -16,6 +18,8 @@ export interface Client {
|
|||||||
address: string | null;
|
address: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
emailOptOut: boolean;
|
emailOptOut: boolean;
|
||||||
|
status: ClientStatus;
|
||||||
|
disabledAt: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user