feat: add schedule column labels, office hours enforcement, and appointment move fix
- Schedule columns default to labels A–F (localStorage, per-browser, click to rename) - Settings → Advanced → Office Hours: configure Doctors (A-C) and Hygienists (D-F) AM/PM hours per weekday - Gray out schedule slots outside office hours; override dialog for manual exceptions - Override Office Hours toggle: select specific dates where all slots are open - Fix appointment move: send only real DB fields to avoid Zod strict-mode rejection of computed fields (hasProcedures, hasClaimWithNumber) - Fix backend PUT /appointments: safe error logging to prevent Prisma error crashing Node inspect - Add OfficeHours Prisma model and GET/PUT /api/office-hours route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -381,34 +381,37 @@ router.put(
|
||||
const newYMD = new Date(date).toISOString().slice(0, 10);
|
||||
const isDateChanged = oldYMD !== newYMD;
|
||||
|
||||
const updatePayload = {
|
||||
...appointmentData,
|
||||
...(isDateChanged ? { eligibilityStatus: "UNKNOWN" as const } : {}),
|
||||
};
|
||||
// Only pass the fields that are safe to update; never overwrite patientId/userId via this route
|
||||
const updatePayload: Record<string, unknown> = {};
|
||||
if (appointmentData.staffId !== undefined) updatePayload.staffId = appointmentData.staffId;
|
||||
if (appointmentData.title !== undefined) updatePayload.title = appointmentData.title;
|
||||
if (appointmentData.date !== undefined) updatePayload.date = appointmentData.date;
|
||||
if (appointmentData.startTime !== undefined) updatePayload.startTime = appointmentData.startTime;
|
||||
if (appointmentData.endTime !== undefined) updatePayload.endTime = appointmentData.endTime;
|
||||
if (appointmentData.type !== undefined) updatePayload.type = appointmentData.type;
|
||||
if (appointmentData.status !== undefined) updatePayload.status = appointmentData.status;
|
||||
if (appointmentData.notes !== undefined) updatePayload.notes = appointmentData.notes;
|
||||
if (isDateChanged) updatePayload.eligibilityStatus = "UNKNOWN";
|
||||
|
||||
// Update appointment
|
||||
const updatedAppointment = await storage.updateAppointment(
|
||||
appointmentId,
|
||||
updatePayload
|
||||
updatePayload as any
|
||||
);
|
||||
return res.json(updatedAppointment);
|
||||
} catch (error) {
|
||||
console.error("Error updating appointment:", error);
|
||||
// Prisma error objects crash Node's util.inspect — always log as string
|
||||
const msg = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||
console.error("Error updating appointment:", msg);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
console.log(
|
||||
"Validation error details:",
|
||||
JSON.stringify(error.format(), null, 2)
|
||||
);
|
||||
return res.status(400).json({
|
||||
message: "Validation error",
|
||||
errors: error.format(),
|
||||
});
|
||||
const fieldErrors = error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(" | ");
|
||||
console.error("Zod validation in PUT /appointments:", fieldErrors);
|
||||
return res.status(400).json({ message: fieldErrors });
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
message: "Failed to update appointment",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
return res.status(500).json({
|
||||
message: msg,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import exportPaymentsReportsRoutes from "./export-payments-reports";
|
||||
import jobMonitorRoutes from "./job-monitor";
|
||||
import twilioRoutes from "./twilio";
|
||||
import aiSettingsRoutes from "./ai-settings";
|
||||
import officeHoursRoutes from "./office-hours";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -56,5 +57,6 @@ router.use("/export-payments-reports", exportPaymentsReportsRoutes);
|
||||
router.use("/job-monitor", jobMonitorRoutes);
|
||||
router.use("/twilio", twilioRoutes);
|
||||
router.use("/ai", aiSettingsRoutes);
|
||||
router.use("/office-hours", officeHoursRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
37
apps/Backend/src/routes/office-hours.ts
Normal file
37
apps/Backend/src/routes/office-hours.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import { storage } from "../storage";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET /api/office-hours
|
||||
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.getOfficeHours(userId);
|
||||
return res.status(200).json(record ? record.data : null);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to fetch office hours", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/office-hours
|
||||
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({ message: "Invalid office hours data" });
|
||||
}
|
||||
|
||||
const record = await storage.upsertOfficeHours(userId, data);
|
||||
return res.status(200).json(record.data);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to save office hours", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -114,14 +114,10 @@ export const appointmentsStorage: IStorage = {
|
||||
id: number,
|
||||
updateData: UpdateAppointment
|
||||
): Promise<Appointment> {
|
||||
try {
|
||||
return await db.appointment.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Appointment with ID ${id} not found`);
|
||||
}
|
||||
return db.appointment.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
},
|
||||
|
||||
async deleteAppointment(id: number): Promise<void> {
|
||||
|
||||
@@ -19,6 +19,7 @@ import * as exportPaymentsReportsStorage from "./export-payments-reports-storage
|
||||
import { cronJobLogStorage } from "./cron-job-log-storage";
|
||||
import { twilioStorage } from "./twilio-storage";
|
||||
import { aiSettingsStorage } from "./ai-settings-storage";
|
||||
import { officeHoursStorage } from "./office-hours-storage";
|
||||
|
||||
|
||||
export const storage = {
|
||||
@@ -41,6 +42,7 @@ export const storage = {
|
||||
...cronJobLogStorage,
|
||||
...twilioStorage,
|
||||
...aiSettingsStorage,
|
||||
...officeHoursStorage,
|
||||
|
||||
};
|
||||
|
||||
|
||||
15
apps/Backend/src/storage/office-hours-storage.ts
Normal file
15
apps/Backend/src/storage/office-hours-storage.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
export const officeHoursStorage = {
|
||||
async getOfficeHours(userId: number) {
|
||||
return db.officeHours.findUnique({ where: { userId } });
|
||||
},
|
||||
|
||||
async upsertOfficeHours(userId: number, data: object) {
|
||||
return db.officeHours.upsert({
|
||||
where: { userId },
|
||||
update: { data },
|
||||
create: { userId, data },
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user