feat: add Procedure Duration/Time Slot settings and custom appointment type

- Add Settings > Advanced > Procedure Duration/Time Slot page with three sections:
  1. Procedure Duration: CDT codes with durations (editable table, save per section)
  2. Doctor Time Slot: drag-to-block visual grid (A/B/C columns, 8 AM–9 PM, edit/delete slots)
  3. Hygienist Time Slot: procedure descriptions with durations
- Backend: ProcedureTimeslot Prisma model, storage, and GET/PUT /api/procedure-timeslot route
- DB migration: procedure_timeslot table
- Appointment form: when type is "Other", show a free-text input for custom description; saved as other:<description> and decoded for display on the schedule card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-05 23:08:34 -04:00
parent fea0dd4d59
commit ceb95f1915
11 changed files with 900 additions and 4 deletions

View File

@@ -28,6 +28,7 @@ import twilioRoutes from "./twilio";
import aiSettingsRoutes from "./ai-settings";
import officeHoursRoutes from "./office-hours";
import officeContactRoutes from "./office-contact";
import procedureTimeslotRoutes from "./procedure-timeslot";
const router = Router();
@@ -60,5 +61,6 @@ router.use("/twilio", twilioRoutes);
router.use("/ai", aiSettingsRoutes);
router.use("/office-hours", officeHoursRoutes);
router.use("/office-contact", officeContactRoutes);
router.use("/procedure-timeslot", procedureTimeslotRoutes);
export default router;

View File

@@ -0,0 +1,37 @@
import express, { Request, Response } from "express";
import { storage } from "../storage";
const router = express.Router();
// GET /api/procedure-timeslot
router.get("/", async (req: Request, res: Response): Promise<any> => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: "Unauthorized" });
const record = await storage.getProcedureTimeslot(userId);
return res.status(200).json(record ?? null);
} catch (err) {
return res.status(500).json({ error: "Failed to fetch procedure timeslot", details: String(err) });
}
});
// PUT /api/procedure-timeslot
router.put("/", async (req: Request, res: Response): Promise<any> => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: "Unauthorized" });
const { data } = req.body;
if (!data || typeof data !== "object") {
return res.status(400).json({ error: "Invalid data payload" });
}
const record = await storage.upsertProcedureTimeslot(userId, data);
return res.status(200).json(record);
} catch (err) {
return res.status(500).json({ error: "Failed to save procedure timeslot", details: String(err) });
}
});
export default router;

View File

@@ -21,6 +21,7 @@ import { twilioStorage } from "./twilio-storage";
import { aiSettingsStorage } from "./ai-settings-storage";
import { officeHoursStorage } from "./office-hours-storage";
import { officeContactStorage } from "./office-contact-storage";
import { procedureTimeslotStorage } from "./procedure-timeslot-storage";
export const storage = {
@@ -45,6 +46,7 @@ export const storage = {
...aiSettingsStorage,
...officeHoursStorage,
...officeContactStorage,
...procedureTimeslotStorage,
};

View File

@@ -0,0 +1,15 @@
import { prisma as db } from "@repo/db/client";
export const procedureTimeslotStorage = {
async getProcedureTimeslot(userId: number) {
return db.procedureTimeslot.findUnique({ where: { userId } });
},
async upsertProcedureTimeslot(userId: number, data: object) {
return db.procedureTimeslot.upsert({
where: { userId },
update: { data },
create: { userId, data },
});
},
};