feat(procedureCodes-dialog) - v2 done
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user