feat(procedure-combos) - v1

This commit is contained in:
2026-01-12 02:15:46 +05:30
parent fce816f13f
commit c53dfd544d
11 changed files with 886 additions and 3 deletions

View File

@@ -0,0 +1,139 @@
import { Router, Request, Response } from "express";
import { storage } from "../storage";
import { prisma } from "@repo/db/client";
import {
insertAppointmentProcedureSchema,
updateAppointmentProcedureSchema,
} from "@repo/db/types";
const router = Router();
/**
* GET /api/appointment-procedures/:appointmentId
* Get all procedures for an appointment
*/
router.get("/:appointmentId", async (req: Request, res: Response) => {
try {
const appointmentId = Number(req.params.appointmentId);
if (isNaN(appointmentId)) {
return res.status(400).json({ message: "Invalid appointmentId" });
}
const rows = await storage.getByAppointmentId(appointmentId);
return res.json(rows);
} catch (err: any) {
console.error("GET appointment procedures error", err);
return res.status(500).json({ message: err.message ?? "Server error" });
}
});
/**
* POST /api/appointment-procedures
* Add single manual procedure
*/
router.post("/", async (req: Request, res: Response) => {
try {
const parsed = insertAppointmentProcedureSchema.parse(req.body);
const created = await storage.createProcedure(parsed);
return res.json(created);
} catch (err: any) {
console.error("POST appointment procedure error", err);
if (err.name === "ZodError") {
return res.status(400).json({ message: err.errors });
}
return res.status(500).json({ message: err.message ?? "Server error" });
}
});
/**
* POST /api/appointment-procedures/bulk
* Add multiple procedures (combos)
*/
router.post("/bulk", async (req: Request, res: Response) => {
try {
const rows = req.body;
if (!Array.isArray(rows) || rows.length === 0) {
return res.status(400).json({ message: "Invalid payload" });
}
const count = await storage.createProceduresBulk(rows);
return res.json({ success: true, count });
} catch (err: any) {
console.error("POST bulk appointment procedures error", err);
return res.status(500).json({ message: err.message ?? "Server error" });
}
});
/**
* PUT /api/appointment-procedures/:id
* Update a procedure
*/
router.put("/:id", async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ message: "Invalid id" });
}
const parsed = updateAppointmentProcedureSchema.parse(req.body);
const updated = await storage.updateProcedure(id, parsed);
return res.json(updated);
} catch (err: any) {
console.error("PUT appointment procedure error", err);
if (err.name === "ZodError") {
return res.status(400).json({ message: err.errors });
}
return res.status(500).json({ message: err.message ?? "Server error" });
}
});
/**
* DELETE /api/appointment-procedures/:id
* Delete single procedure
*/
router.delete("/:id", async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ message: "Invalid id" });
}
await storage.deleteProcedure(id);
return res.json({ success: true });
} catch (err: any) {
console.error("DELETE appointment procedure error", err);
return res.status(500).json({ message: err.message ?? "Server error" });
}
});
/**
* DELETE /api/appointment-procedures/clear/:appointmentId
* Clear all procedures for appointment
*/
router.delete("/clear/:appointmentId", async (req: Request, res: Response) => {
try {
const appointmentId = Number(req.params.appointmentId);
if (isNaN(appointmentId)) {
return res.status(400).json({ message: "Invalid appointmentId" });
}
await storage.clearByAppointmentId(appointmentId);
return res.json({ success: true });
} catch (err: any) {
console.error("CLEAR appointment procedures error", err);
return res.status(500).json({ message: err.message ?? "Server error" });
}
});
export default router;

View File

@@ -1,6 +1,7 @@
import { Router } from "express"; import { Router } from "express";
import patientsRoutes from "./patients"; import patientsRoutes from "./patients";
import appointmentsRoutes from "./appointments"; import appointmentsRoutes from "./appointments";
import appointmentProceduresRoutes from "./appointments-procedures";
import usersRoutes from "./users"; import usersRoutes from "./users";
import staffsRoutes from "./staffs"; import staffsRoutes from "./staffs";
import claimsRoutes from "./claims"; import claimsRoutes from "./claims";
@@ -21,6 +22,7 @@ const router = Router();
router.use("/patients", patientsRoutes); router.use("/patients", patientsRoutes);
router.use("/appointments", appointmentsRoutes); router.use("/appointments", appointmentsRoutes);
router.use("/appointment-procedures", appointmentProceduresRoutes);
router.use("/users", usersRoutes); router.use("/users", usersRoutes);
router.use("/staffs", staffsRoutes); router.use("/staffs", staffsRoutes);
router.use("/patientDataExtraction", patientDataExtractionRoutes); router.use("/patientDataExtraction", patientDataExtractionRoutes);

View File

@@ -0,0 +1,70 @@
import {
AppointmentProcedure,
InsertAppointmentProcedure,
UpdateAppointmentProcedure,
} from "@repo/db/types";
import { prisma as db } from "@repo/db/client";
export interface IAppointmentProceduresStorage {
getByAppointmentId(appointmentId: number): Promise<AppointmentProcedure[]>;
createProcedure(
data: InsertAppointmentProcedure
): Promise<AppointmentProcedure>;
createProceduresBulk(data: InsertAppointmentProcedure[]): Promise<number>;
updateProcedure(
id: number,
data: UpdateAppointmentProcedure
): Promise<AppointmentProcedure>;
deleteProcedure(id: number): Promise<void>;
clearByAppointmentId(appointmentId: number): Promise<void>;
}
export const appointmentProceduresStorage: IAppointmentProceduresStorage = {
async getByAppointmentId(
appointmentId: number
): Promise<AppointmentProcedure[]> {
return db.appointmentProcedure.findMany({
where: { appointmentId },
orderBy: { createdAt: "asc" },
});
},
async createProcedure(
data: InsertAppointmentProcedure
): Promise<AppointmentProcedure> {
return db.appointmentProcedure.create({
data: data as AppointmentProcedure,
});
},
async createProceduresBulk(
data: InsertAppointmentProcedure[]
): Promise<number> {
const result = await db.appointmentProcedure.createMany({
data: data as any[],
});
return result.count;
},
async updateProcedure(
id: number,
data: UpdateAppointmentProcedure
): Promise<AppointmentProcedure> {
return db.appointmentProcedure.update({
where: { id },
data: data as any,
});
},
async deleteProcedure(id: number): Promise<void> {
await db.appointmentProcedure.delete({
where: { id },
});
},
async clearByAppointmentId(appointmentId: number): Promise<void> {
await db.appointmentProcedure.deleteMany({
where: { appointmentId },
});
},
};

View File

@@ -2,7 +2,8 @@
import { usersStorage } from './users-storage'; import { usersStorage } from './users-storage';
import { patientsStorage } from './patients-storage'; import { patientsStorage } from './patients-storage';
import { appointmentsStorage } from './appointements-storage'; import { appointmentsStorage } from './appointments-storage';
import { appointmentProceduresStorage } from './appointment-procedures-storage';
import { staffStorage } from './staff-storage'; import { staffStorage } from './staff-storage';
import { claimsStorage } from './claims-storage'; import { claimsStorage } from './claims-storage';
import { insuranceCredsStorage } from './insurance-creds-storage'; import { insuranceCredsStorage } from './insurance-creds-storage';
@@ -19,6 +20,7 @@ export const storage = {
...usersStorage, ...usersStorage,
...patientsStorage, ...patientsStorage,
...appointmentsStorage, ...appointmentsStorage,
...appointmentProceduresStorage,
...staffStorage, ...staffStorage,
...claimsStorage, ...claimsStorage,
...insuranceCredsStorage, ...insuranceCredsStorage,

View File

@@ -0,0 +1,531 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Trash2, Plus, Save, X } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { PROCEDURE_COMBOS, COMBO_CATEGORIES } from "@/utils/procedureCombos";
import {
CODE_MAP,
getPriceForCodeWithAgeFromMap,
} from "@/utils/procedureCombosMapping";
import { Patient } from "@repo/db/types";
interface AppointmentProcedure {
id: number;
appointmentId: number;
patientId: number;
procedureCode: string;
procedureLabel?: string | null;
fee?: number | null;
isDirect: boolean;
toothNumber?: string | null;
toothSurface?: string | null;
oralCavityArea?: string | null;
source: "COMBO" | "MANUAL";
comboKey?: string | null;
}
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
appointmentId: number;
patientId: number;
patient: Patient;
}
export function AppointmentProceduresDialog({
open,
onOpenChange,
appointmentId,
patientId,
patient,
}: Props) {
const { toast } = useToast();
// -----------------------------
// state for manual add
// -----------------------------
const [manualCode, setManualCode] = useState("");
const [manualLabel, setManualLabel] = useState("");
const [manualFee, setManualFee] = useState("");
const [manualTooth, setManualTooth] = useState("");
const [manualSurface, setManualSurface] = useState("");
// -----------------------------
// state for inline edit
// -----------------------------
const [editingId, setEditingId] = useState<number | null>(null);
const [editRow, setEditRow] = useState<Partial<AppointmentProcedure>>({});
// -----------------------------
// fetch procedures
// -----------------------------
const { data: procedures = [], isLoading } = useQuery<AppointmentProcedure[]>(
{
queryKey: ["appointment-procedures", appointmentId],
queryFn: async () => {
const res = await apiRequest(
"GET",
`/api/appointment-procedures/${appointmentId}`
);
if (!res.ok) throw new Error("Failed to load procedures");
return res.json();
},
enabled: open && !!appointmentId,
}
);
// -----------------------------
// mutations
// -----------------------------
const addManualMutation = useMutation({
mutationFn: async () => {
const payload = {
appointmentId,
patientId,
procedureCode: manualCode,
procedureLabel: manualLabel || null,
fee: manualFee ? Number(manualFee) : null,
toothNumber: manualTooth || null,
toothSurface: manualSurface || null,
source: "MANUAL",
isDirect: false,
};
const res = await apiRequest(
"POST",
"/api/appointment-procedures",
payload
);
if (!res.ok) throw new Error("Failed to add procedure");
return res.json();
},
onSuccess: () => {
toast({ title: "Procedure added" });
setManualCode("");
setManualLabel("");
setManualFee("");
setManualTooth("");
setManualSurface("");
queryClient.invalidateQueries({
queryKey: ["appointment-procedures", appointmentId],
});
},
onError: (err: any) => {
toast({
title: "Error",
description: err.message ?? "Failed to add procedure",
variant: "destructive",
});
},
});
const bulkAddMutation = useMutation({
mutationFn: async (rows: any[]) => {
const res = await apiRequest(
"POST",
"/api/appointment-procedures/bulk",
rows
);
if (!res.ok) throw new Error("Failed to add combo procedures");
return res.json();
},
onSuccess: () => {
toast({ title: "Combo added" });
queryClient.invalidateQueries({
queryKey: ["appointment-procedures", appointmentId],
});
},
});
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest(
"DELETE",
`/api/appointment-procedures/${id}`
);
if (!res.ok) throw new Error("Failed to delete");
},
onSuccess: () => {
toast({ title: "Deleted" });
queryClient.invalidateQueries({
queryKey: ["appointment-procedures", appointmentId],
});
},
});
const updateMutation = useMutation({
mutationFn: async () => {
if (!editingId) return;
const res = await apiRequest(
"PUT",
`/api/appointment-procedures/${editingId}`,
editRow
);
if (!res.ok) throw new Error("Failed to update");
return res.json();
},
onSuccess: () => {
toast({ title: "Updated" });
setEditingId(null);
setEditRow({});
queryClient.invalidateQueries({
queryKey: ["appointment-procedures", appointmentId],
});
},
});
const markClaimModeMutation = useMutation({
mutationFn: async (mode: "DIRECT" | "MANUAL") => {
const payload = {
mode,
appointmentId,
};
const res = await apiRequest(
"POST",
"/api/appointment-procedures/mark-claim-mode",
payload
);
if (!res.ok) throw new Error("Failed to mark claim mode");
},
onSuccess: (_, mode) => {
toast({
title:
mode === "DIRECT" ? "Direct claim selected" : "Manual claim selected",
});
queryClient.invalidateQueries({
queryKey: ["appointment-procedures", appointmentId],
});
},
});
// -----------------------------
// handlers
// -----------------------------
const handleAddCombo = (comboKey: string) => {
const combo = PROCEDURE_COMBOS[comboKey];
if (!combo || !patient?.dateOfBirth) return;
const serviceDate = new Date();
const dob = patient.dateOfBirth;
const age = (() => {
const birth = new Date(dob);
const ref = new Date(serviceDate);
let a = ref.getFullYear() - birth.getFullYear();
const hadBirthday =
ref.getMonth() > birth.getMonth() ||
(ref.getMonth() === birth.getMonth() &&
ref.getDate() >= birth.getDate());
if (!hadBirthday) a -= 1;
return a;
})();
const rows = combo.codes.map((code: string, idx: number) => {
const priceDecimal = getPriceForCodeWithAgeFromMap(CODE_MAP, code, age);
return {
appointmentId,
patientId,
procedureCode: code,
procedureLabel: combo.label,
fee: priceDecimal.toNumber(),
source: "COMBO",
comboKey: comboKey,
toothNumber: combo.toothNumbers?.[idx] ?? null,
isDirect: false,
};
});
bulkAddMutation.mutate(rows);
};
const startEdit = (row: AppointmentProcedure) => {
setEditingId(row.id);
setEditRow({
procedureCode: row.procedureCode,
procedureLabel: row.procedureLabel,
fee: row.fee,
toothNumber: row.toothNumber,
toothSurface: row.toothSurface,
});
};
const cancelEdit = () => {
setEditingId(null);
setEditRow({});
};
// -----------------------------
// UI
// -----------------------------
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
Appointment Procedures
</DialogTitle>
</DialogHeader>
{/* ================= COMBOS ================= */}
<div className="space-y-4">
<div className="text-sm font-semibold text-muted-foreground">
Quick Add Combos
</div>
{Object.entries(COMBO_CATEGORIES).map(([categoryName, comboKeys]) => (
<div key={categoryName} className="space-y-2">
<div className="text-sm font-medium">{categoryName}</div>
<div className="flex flex-wrap gap-2">
{comboKeys.map((comboKey) => {
const combo = PROCEDURE_COMBOS[comboKey];
if (!combo) return null;
return (
<Button
key={comboKey}
variant="secondary"
size="sm"
onClick={() => handleAddCombo(comboKey)}
>
{combo.label}
</Button>
);
})}
</div>
</div>
))}
</div>
{/* ================= MANUAL ADD ================= */}
<div className="mt-8 border rounded-lg p-4 bg-muted/20 space-y-3">
<div className="font-medium text-sm">Add Manual Procedure</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
<div>
<Label>Code</Label>
<Input
value={manualCode}
onChange={(e) => setManualCode(e.target.value)}
placeholder="D0120"
/>
</div>
<div>
<Label>Label</Label>
<Input
value={manualLabel}
onChange={(e) => setManualLabel(e.target.value)}
placeholder="Exam"
/>
</div>
<div>
<Label>Fee</Label>
<Input
value={manualFee}
onChange={(e) => setManualFee(e.target.value)}
placeholder="100"
type="number"
/>
</div>
<div>
<Label>Tooth</Label>
<Input
value={manualTooth}
onChange={(e) => setManualTooth(e.target.value)}
placeholder="14"
/>
</div>
<div>
<Label>Surface</Label>
<Input
value={manualSurface}
onChange={(e) => setManualSurface(e.target.value)}
placeholder="MO"
/>
</div>
</div>
<div className="flex justify-end">
<Button
size="sm"
onClick={() => addManualMutation.mutate()}
disabled={!manualCode || addManualMutation.isPending}
>
<Plus className="h-4 w-4 mr-1" />
Add Procedure
</Button>
</div>
</div>
{/* ================= LIST ================= */}
<div className="mt-8 space-y-2">
<div className="text-sm font-semibold">Selected Procedures</div>
<div className="border rounded-lg divide-y bg-white">
{isLoading && (
<div className="p-4 text-sm text-muted-foreground">
Loading...
</div>
)}
{!isLoading && procedures.length === 0 && (
<div className="p-4 text-sm text-muted-foreground">
No procedures added
</div>
)}
{procedures.map((p) => (
<div
key={p.id}
className="flex items-center gap-2 p-3 text-sm hover:bg-muted/40 transition"
>
{editingId === p.id ? (
<>
<Input
className="w-[90px]"
value={editRow.procedureCode ?? ""}
onChange={(e) =>
setEditRow({
...editRow,
procedureCode: e.target.value,
})
}
/>
<Input
className="flex-1"
value={editRow.procedureLabel ?? ""}
onChange={(e) =>
setEditRow({
...editRow,
procedureLabel: e.target.value,
})
}
/>
<Input
className="w-[90px]"
value={editRow.fee ?? ""}
onChange={(e) =>
setEditRow({ ...editRow, fee: Number(e.target.value) })
}
/>
<Input
className="w-[80px]"
value={editRow.toothNumber ?? ""}
onChange={(e) =>
setEditRow({
...editRow,
toothNumber: e.target.value,
})
}
/>
<Input
className="w-[80px]"
value={editRow.toothSurface ?? ""}
onChange={(e) =>
setEditRow({
...editRow,
toothSurface: e.target.value,
})
}
/>
<Button
size="icon"
variant="ghost"
onClick={() => updateMutation.mutate()}
>
<Save className="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" onClick={cancelEdit}>
<X className="h-4 w-4" />
</Button>
</>
) : (
<>
<div className="w-[90px] font-medium">
{p.procedureCode}
</div>
<div className="flex-1 text-muted-foreground">
{p.procedureLabel}
</div>
<div className="w-[90px]">{p.fee}</div>
<div className="w-[80px]">{p.toothNumber}</div>
<div className="w-[80px]">{p.toothSurface}</div>
<span
className={`text-xs px-2 py-1 rounded ${
p.isDirect
? "bg-green-100 text-green-700"
: "bg-blue-100 text-blue-700"
}`}
>
{p.isDirect ? "Direct" : "Manual"}
</span>
<Button
size="icon"
variant="ghost"
onClick={() => startEdit(p)}
>
Edit
</Button>
<Button
size="icon"
variant="ghost"
onClick={() => deleteMutation.mutate(p.id)}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</>
)}
</div>
))}
</div>
</div>
{/* ================= FOOTER ================= */}
<div className="flex justify-between items-center gap-2 mt-8 pt-4 border-t">
<div className="flex gap-2">
<Button
className="bg-green-600 hover:bg-green-700"
onClick={() => markClaimModeMutation.mutate("DIRECT")}
disabled={!procedures.length}
>
Direct Claim
</Button>
<Button
variant="outline"
className="border-blue-500 text-blue-600 hover:bg-blue-50"
onClick={() => markClaimModeMutation.mutate("MANUAL")}
disabled={!procedures.length}
>
Manual Claim
</Button>
</div>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -53,6 +53,7 @@ import {
} from "@/redux/slices/seleniumEligibilityBatchCheckTaskSlice"; } from "@/redux/slices/seleniumEligibilityBatchCheckTaskSlice";
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner"; import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
import { PatientStatusBadge } from "@/components/appointments/patient-status-badge"; import { PatientStatusBadge } from "@/components/appointments/patient-status-badge";
import { AppointmentProceduresDialog } from "@/components/appointment-procedures/appointment-procedures-dialog";
// Define types for scheduling // Define types for scheduling
interface TimeSlot { interface TimeSlot {
@@ -93,6 +94,17 @@ export default function AppointmentsPage() {
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [proceduresDialogOpen, setProceduresDialogOpen] = useState(false);
const [proceduresAppointmentId, setProceduresAppointmentId] = useState<
number | null
>(null);
const [proceduresPatientId, setProceduresPatientId] = useState<number | null>(
null
);
const [proceduresPatient, setProceduresPatient] = useState<Patient | null>(
null
);
const [calendarOpen, setCalendarOpen] = useState(false); const [calendarOpen, setCalendarOpen] = useState(false);
const [editingAppointment, setEditingAppointment] = useState< const [editingAppointment, setEditingAppointment] = useState<
Appointment | undefined Appointment | undefined
@@ -866,6 +878,34 @@ export default function AppointmentsPage() {
// intentionally do not clear task status here so banner persists until user dismisses it // intentionally do not clear task status here so banner persists until user dismisses it
} }
}; };
const handleOpenProcedures = (appointmentId: number) => {
const apt = appointments.find((a) => a.id === appointmentId);
if (!apt) {
toast({
title: "Error",
description: "Appointment not found",
variant: "destructive",
});
return;
}
const patient = patientsFromDay.find((p) => p.id === apt.patientId);
if (!patient) {
toast({
title: "Error",
description: "Patient not found for this appointment",
variant: "destructive",
});
return;
}
setProceduresAppointmentId(Number(apt.id));
setProceduresPatientId(apt.patientId);
setProceduresPatient(patient);
setProceduresDialogOpen(true);
};
return ( return (
<div> <div>
<SeleniumTaskBanner <SeleniumTaskBanner
@@ -999,6 +1039,16 @@ export default function AppointmentsPage() {
</span> </span>
</Item> </Item>
{/* Procedures */}
<Item
onClick={({ props }) => handleOpenProcedures(props.appointmentId)}
>
<span className="flex items-center gap-2">
<ClipboardList className="h-4 w-4" />
Procedures
</span>
</Item>
{/* Clinic Notes */} {/* Clinic Notes */}
<Item onClick={({ props }) => handleClinicNotes(props.appointmentId)}> <Item onClick={({ props }) => handleClinicNotes(props.appointmentId)}>
<span className="flex items-center gap-2 text-yellow-600"> <span className="flex items-center gap-2 text-yellow-600">
@@ -1126,6 +1176,24 @@ export default function AppointmentsPage() {
onDelete={handleDeleteAppointment} onDelete={handleDeleteAppointment}
/> />
{/* Appointment Procedure Dialog */}
{proceduresAppointmentId && proceduresPatientId && proceduresPatient && (
<AppointmentProceduresDialog
open={proceduresDialogOpen}
onOpenChange={(open) => {
setProceduresDialogOpen(open);
if (!open) {
setProceduresAppointmentId(null);
setProceduresPatientId(null);
setProceduresPatient(null);
}
}}
appointmentId={proceduresAppointmentId}
patientId={proceduresPatientId}
patient={proceduresPatient}
/>
)}
<DeleteConfirmationDialog <DeleteConfirmationDialog
isOpen={confirmDeleteState.open} isOpen={confirmDeleteState.open}
onConfirm={handleConfirmDelete} onConfirm={handleConfirmDelete}

View File

@@ -308,3 +308,6 @@ export function applyComboToForm<T extends ClaimFormLike>(
return next; return next;
} }
export { CODE_MAP, getPriceForCodeWithAgeFromMap };

View File

@@ -58,6 +58,7 @@ model Patient {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
appointments Appointment[] appointments Appointment[]
procedures AppointmentProcedure[]
claims Claim[] claims Claim[]
groups PdfGroup[] groups PdfGroup[]
payment Payment[] payment Payment[]
@@ -92,6 +93,7 @@ model Appointment {
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
staff Staff? @relation(fields: [staffId], references: [id]) staff Staff? @relation(fields: [staffId], references: [id])
procedures AppointmentProcedure[]
claims Claim[] claims Claim[]
@@index([patientId]) @@index([patientId])
@@ -111,6 +113,40 @@ model Staff {
claims Claim[] @relation("ClaimStaff") claims Claim[] @relation("ClaimStaff")
} }
enum ProcedureSource {
COMBO
MANUAL
}
model AppointmentProcedure {
id Int @id @default(autoincrement())
appointmentId Int
patientId Int
procedureCode String
procedureLabel String?
fee Decimal? @db.Decimal(10,2)
category String?
isDirect Boolean @default(false)
toothNumber String?
toothSurface String?
oralCavityArea String?
source ProcedureSource @default(MANUAL)
comboKey String?
createdAt DateTime @default(now())
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
@@index([appointmentId])
@@index([patientId])
}
model Claim { model Claim {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
patientId Int patientId Int

View File

@@ -1,4 +1,4 @@
import { AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import { AppointmentUncheckedCreateInputObjectSchema, AppointmentProcedureUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
import {z} from "zod"; import {z} from "zod";
export type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; export type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
@@ -20,3 +20,34 @@ export const updateAppointmentSchema = (
}) })
.partial(); .partial();
export type UpdateAppointment = z.infer<typeof updateAppointmentSchema>; export type UpdateAppointment = z.infer<typeof updateAppointmentSchema>;
// Appointment Procedure Types.
export type AppointmentProcedure = z.infer<
typeof AppointmentProcedureUncheckedCreateInputObjectSchema
>;
export const insertAppointmentProcedureSchema = (
AppointmentProcedureUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
});
export type InsertAppointmentProcedure = z.infer<
typeof insertAppointmentProcedureSchema
>;
export const updateAppointmentProcedureSchema = (
AppointmentProcedureUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
export type UpdateAppointmentProcedure = z.infer<
typeof updateAppointmentProcedureSchema
>;

View File

@@ -1,5 +1,6 @@
// using this, as the browser load only the required files , not whole db/shared/schemas/ files. // using this, as the browser load only the required files , not whole db/shared/schemas/ files.
export * from '../shared/schemas/objects/AppointmentUncheckedCreateInput.schema'; export * from '../shared/schemas/objects/AppointmentUncheckedCreateInput.schema';
export * from '../shared/schemas/objects/AppointmentProcedureUncheckedCreateInput.schema';
export * from '../shared/schemas/objects/PatientUncheckedCreateInput.schema'; export * from '../shared/schemas/objects/PatientUncheckedCreateInput.schema';
export * from '../shared/schemas/enums/PatientStatus.schema'; export * from '../shared/schemas/enums/PatientStatus.schema';
export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema'; export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema';