feat: org-scoped appointments backend, wire appointments page

Add the appointments table, validation, service and CRUD routes
(/api/appointments, RBAC-gated) and the matching frontend data module.
The appointments page now loads and persists real data; KPIs are computed
from it and the schedule/calendar anchor to the real current date.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-07 19:33:37 +03:00
parent 128ce36df2
commit dec04ec506
14 changed files with 2004 additions and 142 deletions
+81
View File
@@ -0,0 +1,81 @@
import { Router } from "express";
import { appointmentInputSchema } from "../lib/appointment-validation.js";
import { HttpError } from "../lib/http-error.js";
import {
requireAuth,
requireOrg,
requirePermission,
} from "../middleware/auth.js";
import * as service from "../services/appointments.js";
export const appointmentsRouter = Router();
// Appointments are clinic-wide records, gated by the caller's role like patients.
appointmentsRouter.use(requireAuth, requireOrg);
appointmentsRouter.get(
"/",
requirePermission({ appointment: ["read"] }),
async (req, res, next) => {
try {
res.json(await service.listAppointments(req.organizationId!));
} catch (err) {
next(err);
}
},
);
appointmentsRouter.post(
"/",
requirePermission({ appointment: ["write"] }),
async (req, res, next) => {
try {
const input = appointmentInputSchema.parse(req.body);
const created = await service.createAppointment(
req.organizationId!,
req.user!.id,
input,
);
res.status(201).json(created);
} catch (err) {
next(err);
}
},
);
appointmentsRouter.put(
"/:id",
requirePermission({ appointment: ["write"] }),
async (req, res, next) => {
try {
const input = appointmentInputSchema.parse(req.body);
const updated = await service.updateAppointment(
req.organizationId!,
req.params.id as string,
input,
);
if (!updated) throw new HttpError(404, "Appointment not found.");
res.json(updated);
} catch (err) {
next(err);
}
},
);
appointmentsRouter.delete(
"/:id",
requirePermission({ appointment: ["delete"] }),
async (req, res, next) => {
try {
const ok = await service.deleteAppointment(
req.organizationId!,
req.params.id as string,
);
if (!ok) throw new HttpError(404, "Appointment not found.");
res.status(204).end();
} catch (err) {
next(err);
}
},
);