feat(ocr) - schema updated, allowed payment model to allow both - claim and ocr data
This commit is contained in:
@@ -230,7 +230,18 @@ router.put(
|
|||||||
return res.status(404).json({ message: "Payment not found" });
|
return res.status(404).json({ message: "Payment not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceLineTransactions = paymentRecord.claim.serviceLines
|
// Collect service lines from either claim or direct payment(OCR based data)
|
||||||
|
const serviceLines = paymentRecord.claim
|
||||||
|
? paymentRecord.claim.serviceLines
|
||||||
|
: paymentRecord.serviceLines;
|
||||||
|
|
||||||
|
if (!serviceLines || serviceLines.length === 0) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: "No service lines available for this payment" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceLineTransactions = serviceLines
|
||||||
.filter((line) => line.totalDue.gt(0))
|
.filter((line) => line.totalDue.gt(0))
|
||||||
.map((line) => ({
|
.map((line) => ({
|
||||||
serviceLineId: line.id,
|
serviceLineId: line.id,
|
||||||
@@ -273,8 +284,19 @@ router.put(
|
|||||||
if (!paymentRecord) {
|
if (!paymentRecord) {
|
||||||
return res.status(404).json({ message: "Payment not found" });
|
return res.status(404).json({ message: "Payment not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serviceLines = paymentRecord.claim
|
||||||
|
? paymentRecord.claim.serviceLines
|
||||||
|
: paymentRecord.serviceLines;
|
||||||
|
|
||||||
|
if (!serviceLines || serviceLines.length === 0) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: "No service lines available for this payment" });
|
||||||
|
}
|
||||||
|
|
||||||
// Build reversal transactions (negating what’s already paid/adjusted)
|
// Build reversal transactions (negating what’s already paid/adjusted)
|
||||||
const serviceLineTransactions = paymentRecord.claim.serviceLines
|
const serviceLineTransactions = serviceLines
|
||||||
.filter((line) => line.totalPaid.gt(0) || line.totalAdjusted.gt(0))
|
.filter((line) => line.totalPaid.gt(0) || line.totalAdjusted.gt(0))
|
||||||
.map((line) => ({
|
.map((line) => ({
|
||||||
serviceLineId: line.id,
|
serviceLineId: line.id,
|
||||||
|
|||||||
@@ -16,10 +16,17 @@ export async function validateTransactions(
|
|||||||
throw new Error("Payment not found");
|
throw new Error("Payment not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Choose service lines from claim if present, otherwise direct payment service lines(OCR Based datas)
|
||||||
|
const serviceLines = paymentRecord.claim
|
||||||
|
? paymentRecord.claim.serviceLines
|
||||||
|
: paymentRecord.serviceLines;
|
||||||
|
|
||||||
|
if (!serviceLines || serviceLines.length === 0) {
|
||||||
|
throw new Error("No service lines available for this payment");
|
||||||
|
}
|
||||||
|
|
||||||
for (const txn of serviceLineTransactions) {
|
for (const txn of serviceLineTransactions) {
|
||||||
const line = paymentRecord.claim.serviceLines.find(
|
const line = serviceLines.find((sl) => sl.id === txn.serviceLineId);
|
||||||
(sl) => sl.id === txn.serviceLineId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!line) {
|
if (!line) {
|
||||||
throw new Error(`Invalid service line: ${txn.serviceLineId}`);
|
throw new Error(`Invalid service line: ${txn.serviceLineId}`);
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ export default function PaymentEditModal({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serviceLines =
|
||||||
|
payment.claim?.serviceLines ?? payment.serviceLines ?? [];
|
||||||
|
|
||||||
const handleEditServiceLine = (lineId: number) => {
|
const handleEditServiceLine = (lineId: number) => {
|
||||||
if (expandedLineId === lineId) {
|
if (expandedLineId === lineId) {
|
||||||
// Closing current line
|
// Closing current line
|
||||||
@@ -80,7 +83,7 @@ export default function PaymentEditModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find line data
|
// Find line data
|
||||||
const line = payment.claim.serviceLines.find((sl) => sl.id === lineId);
|
const line = serviceLines.find((sl) => sl.id === lineId);
|
||||||
if (!line) return;
|
if (!line) return;
|
||||||
|
|
||||||
// updating form to show its data, while expanding.
|
// updating form to show its data, while expanding.
|
||||||
@@ -136,9 +139,7 @@ export default function PaymentEditModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const line = payment.claim.serviceLines.find(
|
const line = serviceLines.find((sl) => sl.id === formState.serviceLineId);
|
||||||
(sl) => sl.id === formState.serviceLineId
|
|
||||||
);
|
|
||||||
if (!line) {
|
if (!line) {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -189,9 +190,7 @@ export default function PaymentEditModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePayFullDue = async (
|
const handlePayFullDue = async (line: (typeof serviceLines)[0]) => {
|
||||||
line: (typeof payment.claim.serviceLines)[0]
|
|
||||||
) => {
|
|
||||||
if (!line || !payment) {
|
if (!line || !payment) {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -268,15 +267,28 @@ export default function PaymentEditModal({
|
|||||||
{/* Claim + Patient Info */}
|
{/* Claim + Patient Info */}
|
||||||
<div className="space-y-2 border-b border-gray-200 pb-4">
|
<div className="space-y-2 border-b border-gray-200 pb-4">
|
||||||
<h3 className="text-2xl font-bold text-gray-900">
|
<h3 className="text-2xl font-bold text-gray-900">
|
||||||
{payment.claim.patientName}
|
{payment.claim?.patientName ??
|
||||||
|
(`${payment.patient?.firstName ?? ""} ${payment.patient?.lastName ?? ""}`.trim() ||
|
||||||
|
"Unknown Patient")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||||
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
{payment.claimId ? (
|
||||||
Claim #{payment.claimId.toString().padStart(4, "0")}
|
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
||||||
</span>
|
Claim #{payment.claimId.toString().padStart(4, "0")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
||||||
|
OCR Imported Payment
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
||||||
Service Date:{" "}
|
Service Date:{" "}
|
||||||
{formatDateToHumanReadable(payment.claim.serviceDate)}
|
{payment.claim?.serviceDate
|
||||||
|
? formatDateToHumanReadable(payment.claim.serviceDate)
|
||||||
|
: serviceLines.length > 0
|
||||||
|
? formatDateToHumanReadable(serviceLines[0]?.procedureDate)
|
||||||
|
: formatDateToHumanReadable(payment.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -366,8 +378,8 @@ export default function PaymentEditModal({
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
|
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
|
||||||
<div className="mt-3 space-y-4">
|
<div className="mt-3 space-y-4">
|
||||||
{payment.claim.serviceLines.length > 0 ? (
|
{serviceLines.length > 0 ? (
|
||||||
payment.claim.serviceLines.map((line) => {
|
serviceLines.map((line) => {
|
||||||
const isExpanded = expandedLineId === line.id;
|
const isExpanded = expandedLineId === line.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
flexRender,
|
flexRender,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
import { convertOCRDate } from "@/utils/dateUtils";
|
||||||
|
|
||||||
// ---------------- Types ----------------
|
// ---------------- Types ----------------
|
||||||
|
|
||||||
@@ -45,11 +46,17 @@ export default function PaymentOCRBlock() {
|
|||||||
return Array.isArray(data) ? data : data.rows;
|
return Array.isArray(data) ? data : data.rows;
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const withIds: Row[] = data.map((r, i) => ({ ...r, __id: i }));
|
// Remove unwanted keys before using the data
|
||||||
|
const cleaned = data.map((row) => {
|
||||||
|
const { ["Extraction Success"]: _, ["Source File"]: __, ...rest } = row;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
|
const withIds: Row[] = cleaned.map((r, i) => ({ ...r, __id: i }));
|
||||||
setRows(withIds);
|
setRows(withIds);
|
||||||
|
|
||||||
const allKeys = Array.from(
|
const allKeys = Array.from(
|
||||||
data.reduce<Set<string>>((acc, row) => {
|
cleaned.reduce<Set<string>>((acc, row) => {
|
||||||
Object.keys(row).forEach((k) => acc.add(k));
|
Object.keys(row).forEach((k) => acc.add(k));
|
||||||
return acc;
|
return acc;
|
||||||
}, new Set())
|
}, new Set())
|
||||||
@@ -147,17 +154,57 @@ export default function PaymentOCRBlock() {
|
|||||||
extractPaymentOCR.mutate(uploadedImages);
|
extractPaymentOCR.mutate(uploadedImages);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
console.log("Saving edited rows:", rows);
|
try {
|
||||||
toast({
|
const payload = rows.map((row) => {
|
||||||
title: "Saved",
|
const billed = Number(row["Billed Amount"] ?? 0);
|
||||||
description: "Edited OCR results are ready to be sent to the database.",
|
const allowed = Number(row["Allowed Amount"] ?? 0);
|
||||||
});
|
const paid = Number(row["Paid Amount"] ?? 0);
|
||||||
// Here can POST `rows` to your backend API for DB save
|
|
||||||
|
return {
|
||||||
|
patientId: parseInt(row["Patient ID"] as string, 10),
|
||||||
|
totalBilled: billed,
|
||||||
|
totalPaid: paid,
|
||||||
|
totalAdjusted: billed - allowed, // ❗ write-off
|
||||||
|
totalDue: allowed - paid, // ❗ patient responsibility
|
||||||
|
notes: `OCR import - CDT ${row["CDT Code"]}, Tooth ${row["Tooth"]}, Date ${row["Date SVC"]}`,
|
||||||
|
serviceLine: {
|
||||||
|
procedureCode: row["CDT Code"],
|
||||||
|
procedureDate: convertOCRDate(row["Date SVC"]), // you’ll parse "070825" → proper Date
|
||||||
|
toothNumber: row["Tooth"],
|
||||||
|
totalBilled: billed,
|
||||||
|
totalPaid: paid,
|
||||||
|
totalAdjusted: billed - allowed,
|
||||||
|
totalDue: allowed - paid,
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
paidAmount: paid,
|
||||||
|
adjustedAmount: billed - allowed, // same as totalAdjusted
|
||||||
|
method: "OTHER", // fallback, since OCR doesn’t give this
|
||||||
|
receivedDate: new Date(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/payments/ocr",
|
||||||
|
JSON.stringify({ payments: payload })
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("Failed to save OCR payments");
|
||||||
|
|
||||||
|
toast({ title: "Saved", description: "OCR rows saved successfully" });
|
||||||
|
} catch (err: any) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: err.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//rows helper
|
//rows helper
|
||||||
|
|
||||||
const handleAddRow = () => {
|
const handleAddRow = () => {
|
||||||
const newRow: Row = { __id: rows.length };
|
const newRow: Row = { __id: rows.length };
|
||||||
columns.forEach((c) => {
|
columns.forEach((c) => {
|
||||||
@@ -367,128 +414,3 @@ export default function PaymentOCRBlock() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------- Editable OCRTable ----------------------------
|
|
||||||
|
|
||||||
// function OCRTable({
|
|
||||||
// rows,
|
|
||||||
// setRows,
|
|
||||||
// }: {
|
|
||||||
// rows: Row[];
|
|
||||||
// setRows: React.Dispatch<React.SetStateAction<Row[]>>;
|
|
||||||
// }) {
|
|
||||||
// const [columns, setColumns] = React.useState<string[]>([]);
|
|
||||||
|
|
||||||
// // Initialize columns once when rows come in
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (rows.length && columns.length === 0) {
|
|
||||||
// const dynamicCols = Array.from(
|
|
||||||
// rows.reduce<Set<string>>((acc, r) => {
|
|
||||||
// Object.keys(r || {}).forEach((k) => acc.add(k));
|
|
||||||
// return acc;
|
|
||||||
// }, new Set())
|
|
||||||
// );
|
|
||||||
// setColumns(dynamicCols);
|
|
||||||
// }
|
|
||||||
// }, [rows]);
|
|
||||||
|
|
||||||
// if (!rows?.length) return null;
|
|
||||||
|
|
||||||
// // ---------------- column handling ----------------
|
|
||||||
// const handleColumnNameChange = (colIdx: number, value: string) => {
|
|
||||||
// // Just update local columns state for typing responsiveness
|
|
||||||
// setColumns((prev) => prev.map((c, i) => (i === colIdx ? value : c)));
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const commitColumnRename = (colIdx: number) => {
|
|
||||||
// const oldName = Object.keys(rows[0] ?? {})[colIdx];
|
|
||||||
// const newName = columns[colIdx];
|
|
||||||
// if (!oldName || !newName || oldName === newName) return;
|
|
||||||
|
|
||||||
// const updated = rows.map((r) => {
|
|
||||||
// const { [oldName]: oldVal, ...rest } = r;
|
|
||||||
// return { ...rest, [newName]: oldVal };
|
|
||||||
// });
|
|
||||||
// setRows(updated);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // ---------------- row/col editing ----------------
|
|
||||||
// const addColumn = () => {
|
|
||||||
// const newColName = `New Column ${columns.length + 1}`;
|
|
||||||
// setColumns((prev) => [...prev, newColName]);
|
|
||||||
// const updated = rows.map((r) => ({ ...r, [newColName]: "" }));
|
|
||||||
// setRows(updated);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const addRow = () => {
|
|
||||||
// const newRow: Row = {};
|
|
||||||
// columns.forEach((c) => (newRow[c] = ""));
|
|
||||||
// setRows([...rows, newRow]);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleCellChange = (rowIdx: number, col: string, value: string) => {
|
|
||||||
// const updated = rows.map((r, i) =>
|
|
||||||
// i === rowIdx ? { ...r, [col]: value } : r
|
|
||||||
// );
|
|
||||||
// setRows(updated);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // ---------------- render ----------------
|
|
||||||
// return (
|
|
||||||
// <div className="mt-6 overflow-auto border rounded-lg">
|
|
||||||
// <table className="min-w-full text-sm">
|
|
||||||
// <thead className="bg-gray-50">
|
|
||||||
// <tr>
|
|
||||||
// {columns.map((c, idx) => (
|
|
||||||
// <th
|
|
||||||
// key={idx}
|
|
||||||
// className="px-3 py-2 text-left font-semibold text-gray-700 whitespace-nowrap"
|
|
||||||
// >
|
|
||||||
// <input
|
|
||||||
// type="text"
|
|
||||||
// className="border rounded px-2 py-1 text-sm font-semibold w-40"
|
|
||||||
// value={c}
|
|
||||||
// onChange={(e) => handleColumnNameChange(idx, e.target.value)}
|
|
||||||
// onBlur={() => commitColumnRename(idx)}
|
|
||||||
// onKeyDown={(e) => {
|
|
||||||
// if (e.key === "Enter") {
|
|
||||||
// e.currentTarget.blur(); // commit rename
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// </th>
|
|
||||||
// ))}
|
|
||||||
// <th className="px-3 py-2">
|
|
||||||
// <Button size="sm" variant="outline" onClick={addColumn}>
|
|
||||||
// + Add Column
|
|
||||||
// </Button>
|
|
||||||
// </th>
|
|
||||||
// </tr>
|
|
||||||
// </thead>
|
|
||||||
// <tbody>
|
|
||||||
// {rows.map((r, i) => (
|
|
||||||
// <tr key={i} className="odd:bg-white even:bg-gray-50">
|
|
||||||
// {columns.map((c) => (
|
|
||||||
// <td key={c} className="px-3 py-2 whitespace-nowrap">
|
|
||||||
// <input
|
|
||||||
// type="text"
|
|
||||||
// className="w-full border rounded px-2 py-1 text-sm"
|
|
||||||
// value={r[c] != null ? String(r[c]) : ""}
|
|
||||||
// onChange={(e) => handleCellChange(i, c, e.target.value)}
|
|
||||||
// />
|
|
||||||
// </td>
|
|
||||||
// ))}
|
|
||||||
// </tr>
|
|
||||||
// ))}
|
|
||||||
// <tr>
|
|
||||||
// <td colSpan={columns.length + 1} className="px-3 py-2 text-center">
|
|
||||||
// <Button size="sm" variant="outline" onClick={addRow}>
|
|
||||||
// + Add Row
|
|
||||||
// </Button>
|
|
||||||
// </td>
|
|
||||||
// </tr>
|
|
||||||
// </tbody>
|
|
||||||
// </table>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -101,3 +101,41 @@ export const formatDateToHumanReadable = (
|
|||||||
year: "numeric", // e.g., "2023", "2025"
|
year: "numeric", // e.g., "2023", "2025"
|
||||||
}).format(date);
|
}).format(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert any OCR numeric-ish value into a number.
|
||||||
|
* Handles string | number | null | undefined gracefully.
|
||||||
|
*/
|
||||||
|
export function toNum(val: string | number | null | undefined): number {
|
||||||
|
if (val == null || val === "") return 0;
|
||||||
|
if (typeof val === "number") return val;
|
||||||
|
const parsed = Number(val);
|
||||||
|
return isNaN(parsed) ? 0 : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert any OCR string-like value into a safe string.
|
||||||
|
*/
|
||||||
|
export function toStr(val: string | number | null | undefined): string {
|
||||||
|
if (val == null) return "";
|
||||||
|
return String(val).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert OCR date strings like "070825" (MMDDYY) into a JS Date object.
|
||||||
|
* Example: "070825" → 2025-08-07.
|
||||||
|
*/
|
||||||
|
export function convertOCRDate(input: string | number | null | undefined): Date {
|
||||||
|
const raw = toStr(input);
|
||||||
|
|
||||||
|
if (!/^\d{6}$/.test(raw)) {
|
||||||
|
throw new Error(`Invalid OCR date format: ${raw}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = parseInt(raw.slice(0, 2), 10) - 1;
|
||||||
|
const day = parseInt(raw.slice(2, 4), 10);
|
||||||
|
const year2 = parseInt(raw.slice(4, 6), 10);
|
||||||
|
const year = year2 < 50 ? 2000 + year2 : 1900 + year2;
|
||||||
|
|
||||||
|
return new Date(year, month, day);
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ model Staff {
|
|||||||
role String // e.g., "Dentist", "Hygienist", "Assistant"
|
role String // e.g., "Dentist", "Hygienist", "Assistant"
|
||||||
phone String?
|
phone String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
appointments Appointment[]
|
appointments Appointment[]
|
||||||
claims Claim[] @relation("ClaimStaff")
|
claims Claim[] @relation("ClaimStaff")
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,8 @@ enum ClaimStatus {
|
|||||||
|
|
||||||
model ServiceLine {
|
model ServiceLine {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
claimId Int
|
claimId Int?
|
||||||
|
paymentId Int?
|
||||||
procedureCode String
|
procedureCode String
|
||||||
procedureDate DateTime @db.Date
|
procedureDate DateTime @db.Date
|
||||||
oralCavityArea String?
|
oralCavityArea String?
|
||||||
@@ -145,7 +146,9 @@ model ServiceLine {
|
|||||||
totalDue Decimal @default(0.00) @db.Decimal(10, 2)
|
totalDue Decimal @default(0.00) @db.Decimal(10, 2)
|
||||||
status ServiceLineStatus @default(UNPAID)
|
status ServiceLineStatus @default(UNPAID)
|
||||||
|
|
||||||
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
|
claim Claim? @relation(fields: [claimId], references: [id], onDelete: Cascade)
|
||||||
|
payment Payment? @relation(fields: [paymentId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
serviceLineTransactions ServiceLineTransaction[]
|
serviceLineTransactions ServiceLineTransaction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +207,7 @@ enum PdfCategory {
|
|||||||
|
|
||||||
model Payment {
|
model Payment {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
claimId Int @unique
|
claimId Int? @unique
|
||||||
patientId Int
|
patientId Int
|
||||||
userId Int
|
userId Int
|
||||||
updatedById Int?
|
updatedById Int?
|
||||||
@@ -217,12 +220,12 @@ model Payment {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
|
claim Claim? @relation(fields: [claimId], references: [id], onDelete: Cascade)
|
||||||
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||||
updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id])
|
updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id])
|
||||||
serviceLineTransactions ServiceLineTransaction[]
|
serviceLineTransactions ServiceLineTransaction[]
|
||||||
|
serviceLines ServiceLine[]
|
||||||
|
|
||||||
@@index([id])
|
|
||||||
@@index([claimId])
|
@@index([claimId])
|
||||||
@@index([patientId])
|
@@index([patientId])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,12 +67,14 @@ export type PaymentWithExtras = Prisma.PaymentGetPayload<{
|
|||||||
serviceLines: true;
|
serviceLines: true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
serviceLines: true; // ✅ OCR-only service lines directly under Payment
|
||||||
serviceLineTransactions: {
|
serviceLineTransactions: {
|
||||||
include: {
|
include: {
|
||||||
serviceLine: true;
|
serviceLine: true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
updatedBy: true;
|
updatedBy: true;
|
||||||
|
patient: true;
|
||||||
};
|
};
|
||||||
}> & {
|
}> & {
|
||||||
patientName: string;
|
patientName: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user