feat(procedureCodes-dialog) - v2 done

This commit is contained in:
2026-01-15 02:50:46 +05:30
parent c53dfd544d
commit a0b3189430
10 changed files with 301 additions and 122 deletions

View File

@@ -28,6 +28,32 @@ router.get("/:appointmentId", async (req: Request, res: Response) => {
} }
}); });
router.get(
"/prefill-from-appointment/:appointmentId",
async (req: Request, res: Response) => {
try {
const appointmentId = Number(req.params.appointmentId);
if (!appointmentId || isNaN(appointmentId)) {
return res.status(400).json({ error: "Invalid appointmentId" });
}
const data = await storage.getPrefillDataByAppointmentId(appointmentId);
if (!data) {
return res.status(404).json({ error: "Appointment not found" });
}
return res.json(data);
} catch (err: any) {
console.error("prefill-from-appointment error", err);
return res
.status(500)
.json({ error: err.message ?? "Failed to prefill claim data" });
}
}
);
/** /**
* POST /api/appointment-procedures * POST /api/appointment-procedures
* Add single manual procedure * Add single manual procedure

View File

@@ -336,6 +336,15 @@ router.post("/", async (req: Request, res: Response): Promise<any> => {
} }
// --- TRANSFORM serviceLines // --- TRANSFORM serviceLines
if (
!Array.isArray(req.body.serviceLines) ||
req.body.serviceLines.length === 0
) {
return res.status(400).json({
message: "At least one service line is required to create a claim",
});
}
if (Array.isArray(req.body.serviceLines)) { if (Array.isArray(req.body.serviceLines)) {
req.body.serviceLines = req.body.serviceLines.map( req.body.serviceLines = req.body.serviceLines.map(
(line: InputServiceLine) => ({ (line: InputServiceLine) => ({

View File

@@ -1,12 +1,20 @@
import { import {
Appointment,
AppointmentProcedure, AppointmentProcedure,
InsertAppointmentProcedure, InsertAppointmentProcedure,
Patient,
UpdateAppointmentProcedure, UpdateAppointmentProcedure,
} from "@repo/db/types"; } from "@repo/db/types";
import { prisma as db } from "@repo/db/client"; import { prisma as db } from "@repo/db/client";
export interface IAppointmentProceduresStorage { export interface IAppointmentProceduresStorage {
getByAppointmentId(appointmentId: number): Promise<AppointmentProcedure[]>; getByAppointmentId(appointmentId: number): Promise<AppointmentProcedure[]>;
getPrefillDataByAppointmentId(appointmentId: number): Promise<{
appointment: Appointment;
patient: Patient;
procedures: AppointmentProcedure[];
} | null>;
createProcedure( createProcedure(
data: InsertAppointmentProcedure data: InsertAppointmentProcedure
): Promise<AppointmentProcedure>; ): Promise<AppointmentProcedure>;
@@ -29,6 +37,28 @@ export const appointmentProceduresStorage: IAppointmentProceduresStorage = {
}); });
}, },
async getPrefillDataByAppointmentId(appointmentId: number) {
const appointment = await db.appointment.findUnique({
where: { id: appointmentId },
include: {
patient: true,
procedures: {
orderBy: { createdAt: "asc" },
},
},
});
if (!appointment) {
return null;
}
return {
appointment,
patient: appointment.patient,
procedures: appointment.procedures,
};
},
async createProcedure( async createProcedure(
data: InsertAppointmentProcedure data: InsertAppointmentProcedure
): Promise<AppointmentProcedure> { ): Promise<AppointmentProcedure> {

View File

@@ -17,22 +17,9 @@ import {
CODE_MAP, CODE_MAP,
getPriceForCodeWithAgeFromMap, getPriceForCodeWithAgeFromMap,
} from "@/utils/procedureCombosMapping"; } from "@/utils/procedureCombosMapping";
import { Patient } from "@repo/db/types"; import { Patient, AppointmentProcedure } from "@repo/db/types";
import { useLocation } from "wouter";
interface AppointmentProcedure { import { DeleteConfirmationDialog } from "../ui/deleteDialog";
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 { interface Props {
open: boolean; open: boolean;
@@ -65,6 +52,10 @@ export function AppointmentProceduresDialog({
// ----------------------------- // -----------------------------
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
const [editRow, setEditRow] = useState<Partial<AppointmentProcedure>>({}); const [editRow, setEditRow] = useState<Partial<AppointmentProcedure>>({});
const [clearAllOpen, setClearAllOpen] = useState(false);
// for redirection to claim submission
const [, setLocation] = useLocation();
// ----------------------------- // -----------------------------
// fetch procedures // fetch procedures
@@ -98,7 +89,6 @@ export function AppointmentProceduresDialog({
toothNumber: manualTooth || null, toothNumber: manualTooth || null,
toothSurface: manualSurface || null, toothSurface: manualSurface || null,
source: "MANUAL", source: "MANUAL",
isDirect: false,
}; };
const res = await apiRequest( const res = await apiRequest(
@@ -163,6 +153,30 @@ export function AppointmentProceduresDialog({
}, },
}); });
const clearAllMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest(
"DELETE",
`/api/appointment-procedures/clear/${appointmentId}`
);
if (!res.ok) throw new Error("Failed to clear procedures");
},
onSuccess: () => {
toast({ title: "All procedures cleared" });
queryClient.invalidateQueries({
queryKey: ["appointment-procedures", appointmentId],
});
setClearAllOpen(false);
},
onError: (err: any) => {
toast({
title: "Error",
description: err.message ?? "Failed to clear procedures",
variant: "destructive",
});
},
});
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!editingId) return; if (!editingId) return;
@@ -184,30 +198,6 @@ export function AppointmentProceduresDialog({
}, },
}); });
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 // handlers
// ----------------------------- // -----------------------------
@@ -242,7 +232,6 @@ export function AppointmentProceduresDialog({
source: "COMBO", source: "COMBO",
comboKey: comboKey, comboKey: comboKey,
toothNumber: combo.toothNumbers?.[idx] ?? null, toothNumber: combo.toothNumbers?.[idx] ?? null,
isDirect: false,
}; };
}); });
@@ -250,6 +239,8 @@ export function AppointmentProceduresDialog({
}; };
const startEdit = (row: AppointmentProcedure) => { const startEdit = (row: AppointmentProcedure) => {
if (!row.id) return;
setEditingId(row.id); setEditingId(row.id);
setEditRow({ setEditRow({
procedureCode: row.procedureCode, procedureCode: row.procedureCode,
@@ -265,12 +256,34 @@ export function AppointmentProceduresDialog({
setEditRow({}); setEditRow({});
}; };
const handleDirectClaim = () => {
setLocation(`/claims?appointmentId=${appointmentId}&mode=direct`);
onOpenChange(false);
};
const handleManualClaim = () => {
setLocation(`/claims?appointmentId=${appointmentId}&mode=manual`);
onOpenChange(false);
};
// ----------------------------- // -----------------------------
// UI // UI
// ----------------------------- // -----------------------------
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto"> <DialogContent
className="max-w-6xl max-h-[90vh] overflow-y-auto pointer-events-none"
onPointerDownOutside={(e) => {
if (clearAllOpen) {
e.preventDefault(); // block only when delete dialog is open
}
}}
onInteractOutside={(e) => {
if (clearAllOpen) {
e.preventDefault(); // block only when delete dialog is open
}
}}
>
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl font-semibold"> <DialogTitle className="text-xl font-semibold">
Appointment Procedures Appointment Procedures
@@ -278,7 +291,7 @@ export function AppointmentProceduresDialog({
</DialogHeader> </DialogHeader>
{/* ================= COMBOS ================= */} {/* ================= COMBOS ================= */}
<div className="space-y-4"> <div className="space-y-4 pointer-events-auto">
<div className="text-sm font-semibold text-muted-foreground"> <div className="text-sm font-semibold text-muted-foreground">
Quick Add Combos Quick Add Combos
</div> </div>
@@ -374,7 +387,18 @@ export function AppointmentProceduresDialog({
{/* ================= LIST ================= */} {/* ================= LIST ================= */}
<div className="mt-8 space-y-2"> <div className="mt-8 space-y-2">
<div className="text-sm font-semibold">Selected Procedures</div> <div className="flex items-center justify-between">
<div className="text-sm font-semibold">Selected Procedures</div>
<Button
variant="destructive"
size="sm"
disabled={!procedures.length}
onClick={() => setClearAllOpen(true)}
>
Clear All
</Button>
</div>
<div className="border rounded-lg divide-y bg-white"> <div className="border rounded-lg divide-y bg-white">
{isLoading && ( {isLoading && (
@@ -418,11 +442,16 @@ export function AppointmentProceduresDialog({
/> />
<Input <Input
className="w-[90px]" className="w-[90px]"
value={editRow.fee ?? ""} value={
editRow.fee !== undefined && editRow.fee !== null
? String(editRow.fee)
: ""
}
onChange={(e) => onChange={(e) =>
setEditRow({ ...editRow, fee: Number(e.target.value) }) setEditRow({ ...editRow, fee: Number(e.target.value) })
} }
/> />
<Input <Input
className="w-[80px]" className="w-[80px]"
value={editRow.toothNumber ?? ""} value={editRow.toothNumber ?? ""}
@@ -464,20 +493,15 @@ export function AppointmentProceduresDialog({
<div className="flex-1 text-muted-foreground"> <div className="flex-1 text-muted-foreground">
{p.procedureLabel} {p.procedureLabel}
</div> </div>
<div className="w-[90px]">{p.fee}</div> <div className="w-[90px]">
{p.fee !== null && p.fee !== undefined
? String(p.fee)
: ""}
</div>
<div className="w-[80px]">{p.toothNumber}</div> <div className="w-[80px]">{p.toothNumber}</div>
<div className="w-[80px]">{p.toothSurface}</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 <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
@@ -489,7 +513,7 @@ export function AppointmentProceduresDialog({
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
onClick={() => deleteMutation.mutate(p.id)} onClick={() => deleteMutation.mutate(p.id!)}
> >
<Trash2 className="h-4 w-4 text-red-500" /> <Trash2 className="h-4 w-4 text-red-500" />
</Button> </Button>
@@ -505,8 +529,8 @@ export function AppointmentProceduresDialog({
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
className="bg-green-600 hover:bg-green-700" className="bg-green-600 hover:bg-green-700"
onClick={() => markClaimModeMutation.mutate("DIRECT")}
disabled={!procedures.length} disabled={!procedures.length}
onClick={handleDirectClaim}
> >
Direct Claim Direct Claim
</Button> </Button>
@@ -514,8 +538,8 @@ export function AppointmentProceduresDialog({
<Button <Button
variant="outline" variant="outline"
className="border-blue-500 text-blue-600 hover:bg-blue-50" className="border-blue-500 text-blue-600 hover:bg-blue-50"
onClick={() => markClaimModeMutation.mutate("MANUAL")}
disabled={!procedures.length} disabled={!procedures.length}
onClick={handleManualClaim}
> >
Manual Claim Manual Claim
</Button> </Button>
@@ -526,6 +550,16 @@ export function AppointmentProceduresDialog({
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>
<DeleteConfirmationDialog
isOpen={clearAllOpen}
entityName="all procedures for this appointment"
onCancel={() => setClearAllOpen(false)}
onConfirm={() => {
setClearAllOpen(false);
clearAllMutation.mutate();
}}
/>
</Dialog> </Dialog>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useCallback, memo } from "react"; import { useState, useEffect, useRef, useCallback, memo, useMemo } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@@ -58,6 +58,7 @@ import { RemarksField } from "./claims-ui";
interface ClaimFormProps { interface ClaimFormProps {
patientId: number; patientId: number;
appointmentId?: number; appointmentId?: number;
autoSubmit?: boolean;
onSubmit: (data: ClaimFormData) => Promise<Claim>; onSubmit: (data: ClaimFormData) => Promise<Claim>;
onHandleAppointmentSubmit: ( onHandleAppointmentSubmit: (
appointmentData: InsertAppointment | UpdateAppointment appointmentData: InsertAppointment | UpdateAppointment
@@ -68,23 +69,10 @@ interface ClaimFormProps {
onClose: () => void; onClose: () => void;
} }
const PERMANENT_TOOTH_NAMES = Array.from(
{ length: 32 },
(_, i) => `T_${i + 1}`
);
const PRIMARY_TOOTH_NAMES = Array.from("ABCDEFGHIJKLMNOPQRST").map(
(ch) => `T_${ch}`
);
function isValidToothKey(key: string) {
return (
PERMANENT_TOOTH_NAMES.includes(key) || PRIMARY_TOOTH_NAMES.includes(key)
);
}
export function ClaimForm({ export function ClaimForm({
patientId, patientId,
appointmentId, appointmentId,
autoSubmit,
onHandleAppointmentSubmit, onHandleAppointmentSubmit,
onHandleUpdatePatient, onHandleUpdatePatient,
onHandleForMHSeleniumClaim, onHandleForMHSeleniumClaim,
@@ -95,6 +83,9 @@ export function ClaimForm({
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
const [prefillDone, setPrefillDone] = useState(false);
const autoSubmittedRef = useRef(false);
const [patient, setPatient] = useState<Patient | null>(null); const [patient, setPatient] = useState<Patient | null>(null);
// Query patient based on given patient id // Query patient based on given patient id
@@ -225,6 +216,53 @@ export function ClaimForm({
}; };
}, [appointmentId]); }, [appointmentId]);
//
// 2. effect - prefill proceduresCodes (if exists for appointment) into serviceLines
useEffect(() => {
if (!appointmentId) return;
let cancelled = false;
(async () => {
try {
const res = await apiRequest(
"GET",
`/api/appointment-procedures/prefill-from-appointment/${appointmentId}`
);
if (!res.ok) return;
const data = await res.json();
if (cancelled) return;
const mappedLines = (data.procedures || []).map((p: any) => ({
procedureCode: p.procedureCode,
procedureDate: serviceDate,
oralCavityArea: p.oralCavityArea || "",
toothNumber: p.toothNumber || "",
toothSurface: p.toothSurface || "",
totalBilled: new Decimal(p.fee || 0),
totalAdjusted: new Decimal(0),
totalPaid: new Decimal(0),
}));
setForm((prev) => ({
...prev,
serviceLines: mappedLines,
}));
setPrefillDone(true);
} catch (err) {
console.error("Failed to prefill procedures:", err);
}
})();
return () => {
cancelled = true;
};
}, [appointmentId, serviceDate]);
// Update service date when calendar date changes // Update service date when calendar date changes
const onServiceDateChange = (date: Date | undefined) => { const onServiceDateChange = (date: Date | undefined) => {
if (date) { if (date) {
@@ -439,21 +477,6 @@ export function ClaimForm({
}); });
}; };
const updateMissingTooth = useCallback(
(name: string, value: "" | "X" | "O") => {
if (!isValidToothKey(name)) return;
setForm((prev) => {
const current = prev.missingTeeth[name] ?? "";
if (current === value) return prev;
const nextMap = { ...prev.missingTeeth };
if (!value) delete nextMap[name];
else nextMap[name] = value;
return { ...prev, missingTeeth: nextMap };
});
},
[]
);
const clearAllToothSelections = () => const clearAllToothSelections = () =>
setForm((prev) => ({ ...prev, missingTeeth: {} as MissingMapStrict })); setForm((prev) => ({ ...prev, missingTeeth: {} as MissingMapStrict }));
@@ -772,6 +795,37 @@ export function ClaimForm({
await handleMHSubmit(nextForm); await handleMHSubmit(nextForm);
}; };
const isFormReady = useMemo(() => {
return (
!!patient &&
!!form.memberId?.trim() &&
!!form.dateOfBirth?.trim() &&
!!form.patientName?.trim() &&
Array.isArray(form.serviceLines) &&
form.serviceLines.some(
(l) => l.procedureCode && l.procedureCode.trim() !== ""
)
);
}, [
patient,
form.memberId,
form.dateOfBirth,
form.patientName,
form.serviceLines,
]);
// when autoSubmit mode is given, it will then submit the claims.
useEffect(() => {
if (!autoSubmit) return;
if (!prefillDone) return;
if (!isFormReady) return;
if (autoSubmittedRef.current) return;
autoSubmittedRef.current = true;
handleMHSubmit();
}, [autoSubmit, prefillDone, isFormReady]);
// overlay click handler (close when clicking backdrop) // overlay click handler (close when clicking backdrop)
const onOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { const onOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
// only close if clicked the backdrop itself (not inner modal) // only close if clicked the backdrop itself (not inner modal)
@@ -780,6 +834,14 @@ export function ClaimForm({
} }
}; };
useEffect(() => {
return () => {
// reset when ClaimForm unmounts (modal closes)
autoSubmittedRef.current = false;
setPrefillDone(false);
};
}, []);
return ( return (
<div <div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto" className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto"

View File

@@ -12,22 +12,36 @@ export const DeleteConfirmationDialog = ({
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-[9999] pointer-events-auto">
<div className="bg-white p-6 rounded-md shadow-md w-[90%] max-w-md"> <div
className="bg-white p-6 rounded-md shadow-md w-[90%] max-w-md pointer-events-auto"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-semibold mb-4">Confirm Deletion</h2> <h2 className="text-xl font-semibold mb-4">Confirm Deletion</h2>
<p> <p>
Are you sure you want to delete <strong>{entityName}</strong>? Are you sure you want to delete <strong>{entityName}</strong>?
</p> </p>
<div className="mt-6 flex justify-end space-x-4"> <div className="mt-6 flex justify-end space-x-4">
<button <button
type="button"
className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300" className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
onClick={onCancel} onClick={(e) => {
e.stopPropagation();
onCancel();
}}
> >
Cancel Cancel
</button> </button>
<button <button
type="button"
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700" className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700"
onClick={onConfirm} onClick={(e) => {
e.stopPropagation();
onConfirm();
}}
> >
Delete Delete
</button> </button>

View File

@@ -146,9 +146,13 @@ export default function ClaimsPage() {
// case1: - this params are set by pdf extraction/patient page or either by patient-add-form. then used in claim page here. // case1: - this params are set by pdf extraction/patient page or either by patient-add-form. then used in claim page here.
const [location] = useLocation(); const [location] = useLocation();
const { newPatient } = useMemo(() => {
const { newPatient, mode } = useMemo(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
return { newPatient: params.get("newPatient") }; return {
newPatient: params.get("newPatient"),
mode: params.get("mode"), // direct | manual | null};
};
}, [location]); }, [location]);
const handleNewClaim = (patientId: number, appointmentId?: number) => { const handleNewClaim = (patientId: number, appointmentId?: number) => {
@@ -532,6 +536,7 @@ export default function ClaimsPage() {
<ClaimForm <ClaimForm
patientId={selectedPatientId} patientId={selectedPatientId}
appointmentId={selectedAppointmentId ?? undefined} appointmentId={selectedAppointmentId ?? undefined}
autoSubmit={mode === "direct"}
onClose={closeClaim} onClose={closeClaim}
onSubmit={handleClaimSubmit} onSubmit={handleClaimSubmit}
onHandleAppointmentSubmit={handleAppointmentSubmit} onHandleAppointmentSubmit={handleAppointmentSubmit}

View File

@@ -253,22 +253,22 @@ export const PROCEDURE_COMBOS: Record<
// Orthodontics // Orthodontics
orthPreExamDirect: { orthPreExamDirect: {
id: "orthPreExamDirect", id: "orthPreExamDirect",
label: "Pre-Orth Exam", label: "Direct Pre-Orth Exam",
codes: ["D9310"], codes: ["D9310"],
}, },
orthRecordDirect: { orthRecordDirect: {
id: "orthRecordDirect", id: "orthRecordDirect",
label: "Orth Record", label: "Direct Orth Record",
codes: ["D8660"], codes: ["D8660"],
}, },
orthPerioVisitDirect: { orthPerioVisitDirect: {
id: "orthPerioVisitDirect", id: "orthPerioVisitDirect",
label: "Perio Orth Visit ", label: "Direct Perio Orth Visit ",
codes: ["D8670"], codes: ["D8670"],
}, },
orthRetentionDirect: { orthRetentionDirect: {
id: "orthRetentionDirect", id: "orthRetentionDirect",
label: "Orth Retention", label: "Direct Orth Retention",
codes: ["D8680"], codes: ["D8680"],
}, },
orthPA: { orthPA: {

View File

@@ -14,7 +14,8 @@
"./client": "./src/index.ts", "./client": "./src/index.ts",
"./shared/schemas": "./shared/schemas/index.ts", "./shared/schemas": "./shared/schemas/index.ts",
"./usedSchemas": "./usedSchemas/index.ts", "./usedSchemas": "./usedSchemas/index.ts",
"./types": "./types/index.ts" "./types": "./types/index.ts",
"./generated/prisma": "./generated/prisma/index.js"
}, },
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.0.1", "@prisma/adapter-pg": "^7.0.1",

View File

@@ -37,10 +37,10 @@ model User {
} }
model Patient { model Patient {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
firstName String firstName String
lastName String lastName String
dateOfBirth DateTime @db.Date dateOfBirth DateTime @db.Date
gender String gender String
phone String phone String
email String? email String?
@@ -53,12 +53,12 @@ model Patient {
policyHolder String? policyHolder String?
allergies String? allergies String?
medicalConditions String? medicalConditions String?
status PatientStatus @default(UNKNOWN) status PatientStatus @default(UNKNOWN)
userId Int userId Int
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[] procedures AppointmentProcedure[]
claims Claim[] claims Claim[]
groups PdfGroup[] groups PdfGroup[]
payment Payment[] payment Payment[]
@@ -90,11 +90,11 @@ model Appointment {
eligibilityStatus PatientStatus @default(UNKNOWN) eligibilityStatus PatientStatus @default(UNKNOWN)
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[] procedures AppointmentProcedure[]
claims Claim[] claims Claim[]
@@index([patientId]) @@index([patientId])
@@index([date]) @@index([date])
@@ -119,25 +119,24 @@ enum ProcedureSource {
} }
model AppointmentProcedure { model AppointmentProcedure {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
appointmentId Int appointmentId Int
patientId Int patientId Int
procedureCode String procedureCode String
procedureLabel String? procedureLabel String?
fee Decimal? @db.Decimal(10,2) fee Decimal? @db.Decimal(10, 2)
category String? category String?
isDirect Boolean @default(false)
toothNumber String? toothNumber String?
toothSurface String? toothSurface String?
oralCavityArea String? oralCavityArea String?
source ProcedureSource @default(MANUAL) source ProcedureSource @default(MANUAL)
comboKey String? comboKey String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade) appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
@@ -146,7 +145,6 @@ model AppointmentProcedure {
@@index([patientId]) @@index([patientId])
} }
model Claim { model Claim {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
patientId Int patientId Int