feat: customers management UI with paginated table and full delete
All checks were successful
Build & Release / build (push) Successful in 12s
All checks were successful
Build & Release / build (push) Successful in 12s
- Fix SSH key missing trailing newline (error in libcrypto) - Pass env with SSH command through all git operations - Add customers table (modules, start/expiration dates, created/updated timestamps) - Idempotent ALTER TABLE for existing deployments - GET /customers with pagination, search, and sort - POST /customers persists slug with modules and dates to DB - DELETE /customers/:slug removes ArgoCD chart, DO DB, pgbouncer pool, and manager record - Redesigned frontend: dark slate theme, customers table page with search/sort/pagination, delete confirm dialog, module checkboxes, slate buttons
This commit is contained in:
@@ -4,13 +4,52 @@ import { createDatabase, createDatabaseUser, deleteDatabase, deleteDatabaseUser
|
||||
import { addCustomerToPool, removeCustomerFromPool } from "../services/pgbouncer";
|
||||
import { addCustomerChart, removeCustomerChart } from "../services/git";
|
||||
import { setupCustomerDatabase, teardownCustomerDatabase } from "../services/db";
|
||||
import { db } from "../db/manager";
|
||||
|
||||
const MODULES = ["pos", "inventory", "rentals", "scheduling", "repairs", "accounting"] as const;
|
||||
|
||||
const ProvisionSchema = z.object({
|
||||
name: z.string().min(2).max(32).regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only"),
|
||||
appVersion: z.string().default("latest"),
|
||||
modules: z.array(z.enum(MODULES)).default([]),
|
||||
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).default(() => new Date().toISOString().slice(0, 10)),
|
||||
expirationDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().default(null),
|
||||
});
|
||||
|
||||
const ListQuerySchema = z.object({
|
||||
page: z.coerce.number().int().min(1).default(1),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(25),
|
||||
q: z.string().optional(),
|
||||
sort: z.enum(["slug", "start_date", "expiration_date", "created_at", "updated_at"]).default("created_at"),
|
||||
order: z.enum(["asc", "desc"]).default("desc"),
|
||||
});
|
||||
|
||||
export async function customerRoutes(app: FastifyInstance) {
|
||||
app.get("/customers", async (req, reply) => {
|
||||
const query = ListQuerySchema.parse(req.query);
|
||||
const offset = (query.page - 1) * query.limit;
|
||||
const search = query.q ? `%${query.q}%` : null;
|
||||
|
||||
const [rows, [{ count }]] = await Promise.all([
|
||||
search
|
||||
? db`SELECT * FROM customers WHERE slug ILIKE ${search} ORDER BY ${db(query.sort)} ${db.unsafe(query.order)} LIMIT ${query.limit} OFFSET ${offset}`
|
||||
: db`SELECT * FROM customers ORDER BY ${db(query.sort)} ${db.unsafe(query.order)} LIMIT ${query.limit} OFFSET ${offset}`,
|
||||
search
|
||||
? db`SELECT COUNT(*)::int FROM customers WHERE slug ILIKE ${search}`
|
||||
: db`SELECT COUNT(*)::int FROM customers`,
|
||||
]);
|
||||
|
||||
return reply.send({
|
||||
data: rows,
|
||||
pagination: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / query.limit),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/customers", async (req, reply) => {
|
||||
const body = ProvisionSchema.parse(req.body);
|
||||
const slug = body.name;
|
||||
@@ -26,6 +65,16 @@ export async function customerRoutes(app: FastifyInstance) {
|
||||
await addCustomerToPool(slug, user.password);
|
||||
addCustomerChart(slug, body.appVersion);
|
||||
|
||||
await db`
|
||||
INSERT INTO customers (slug, modules, start_date, expiration_date)
|
||||
VALUES (${slug}, ${body.modules}, ${body.startDate}, ${body.expirationDate})
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
modules = EXCLUDED.modules,
|
||||
start_date = EXCLUDED.start_date,
|
||||
expiration_date = EXCLUDED.expiration_date,
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
app.log.info({ slug }, "customer provisioned");
|
||||
return reply.code(201).send({ slug, status: "provisioned" });
|
||||
});
|
||||
@@ -43,6 +92,8 @@ export async function customerRoutes(app: FastifyInstance) {
|
||||
deleteDatabaseUser(slug),
|
||||
]);
|
||||
|
||||
await db`DELETE FROM customers WHERE slug = ${slug}`;
|
||||
|
||||
app.log.info({ slug }, "customer deprovisioned");
|
||||
return reply.code(204).send();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user