payment checkpoint 3

This commit is contained in:
2025-08-08 00:34:56 +05:30
parent c107c798cf
commit 89897ef2d6
11 changed files with 699 additions and 274 deletions

View File

@@ -202,6 +202,27 @@ router.get("/filter", async (req: Request, res: Response): Promise<any> => {
} }
}); });
// GET /api/payments/:id
router.get("/:id", async (req: Request, res: Response): Promise<any> => {
try {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: "Unauthorized" });
const id = parseIntOrError(req.params.id, "Payment ID");
const payment = await storage.getPaymentById(userId, id);
if (!payment)
return res.status(404).json({ message: "Payment not found" });
res.status(200).json(payment);
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Failed to retrieve payment";
res.status(500).json({ message });
}
});
// POST /api/payments/:claimId // POST /api/payments/:claimId
router.post("/:claimId", async (req: Request, res: Response): Promise<any> => { router.post("/:claimId", async (req: Request, res: Response): Promise<any> => {
try { try {

View File

@@ -9,12 +9,14 @@ import {
PdfFileUncheckedCreateInputObjectSchema, PdfFileUncheckedCreateInputObjectSchema,
PdfGroupUncheckedCreateInputObjectSchema, PdfGroupUncheckedCreateInputObjectSchema,
PdfCategorySchema, PdfCategorySchema,
PaymentUncheckedCreateInputObjectSchema,
PaymentTransactionCreateInputObjectSchema,
ServiceLinePaymentCreateInputObjectSchema,
} from "@repo/db/usedSchemas"; } from "@repo/db/usedSchemas";
import { z } from "zod"; import { z } from "zod";
import { Prisma } from "@repo/db/generated/prisma"; import {
InsertPayment,
Payment,
PaymentWithExtras,
UpdatePayment,
} from "@repo/db/types";
//creating types out of schema auto generated. //creating types out of schema auto generated.
type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>; type Appointment = z.infer<typeof AppointmentUncheckedCreateInputObjectSchema>;
@@ -158,50 +160,6 @@ export interface ClaimPdfMetadata {
uploadedAt: Date; uploadedAt: Date;
} }
// Base Payment type
type Payment = z.infer<typeof PaymentUncheckedCreateInputObjectSchema>;
type PaymentTransaction = z.infer<
typeof PaymentTransactionCreateInputObjectSchema
>;
type ServiceLinePayment = z.infer<
typeof ServiceLinePaymentCreateInputObjectSchema
>;
const insertPaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
updatedAt: true,
});
type InsertPayment = z.infer<typeof insertPaymentSchema>;
const updatePaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdatePayment = z.infer<typeof updatePaymentSchema>;
type PaymentWithExtras = Prisma.PaymentGetPayload<{
include: {
transactions: true;
servicePayments: true;
claim: {
include: {
serviceLines: true;
};
};
};
}> & {
patientName: string;
paymentDate: Date;
paymentMethod: string;
};
export interface IStorage { export interface IStorage {
// User methods // User methods
getUser(id: number): Promise<User | undefined>; getUser(id: number): Promise<User | undefined>;
@@ -952,8 +910,16 @@ export const storage: IStorage = {
serviceLines: true, serviceLines: true,
}, },
}, },
transactions: true, transactions: {
servicePayments: true, include: {
serviceLinePayments: {
include: {
serviceLine: true,
},
},
},
},
updatedBy: true,
}, },
}); });
@@ -979,8 +945,16 @@ export const storage: IStorage = {
serviceLines: true, serviceLines: true,
}, },
}, },
transactions: true, transactions: {
servicePayments: true, include: {
serviceLinePayments: {
include: {
serviceLine: true,
},
},
},
},
updatedBy: true,
}, },
}); });
@@ -1006,8 +980,16 @@ export const storage: IStorage = {
serviceLines: true, serviceLines: true,
}, },
}, },
transactions: true, transactions: {
servicePayments: true, include: {
serviceLinePayments: {
include: {
serviceLine: true,
},
},
},
},
updatedBy: true,
}, },
}); });
@@ -1035,8 +1017,16 @@ export const storage: IStorage = {
serviceLines: true, serviceLines: true,
}, },
}, },
transactions: true, transactions: {
servicePayments: true, include: {
serviceLinePayments: {
include: {
serviceLine: true,
},
},
},
},
updatedBy: true,
}, },
}); });
@@ -1068,8 +1058,16 @@ export const storage: IStorage = {
serviceLines: true, serviceLines: true,
}, },
}, },
transactions: true, transactions: {
servicePayments: true, include: {
serviceLinePayments: {
include: {
serviceLine: true,
},
},
},
},
updatedBy: true,
}, },
}); });

View File

@@ -1,140 +1,452 @@
import { useState } from "react"; import {
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; Dialog,
import { Input } from "@/components/ui/input"; DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { formatDateToHumanReadable } from "@/utils/dateUtils";
import { Textarea } from "@/components/ui/textarea"; import React, { useState } from "react";
import { apiRequest, queryClient } from "@/lib/queryClient"; import { paymentStatusOptions, PaymentWithExtras } from "@repo/db/types";
import { useToast } from "@/hooks/use-toast"; import { PaymentStatus, PaymentMethod } from "@repo/db/types";
import { z } from "zod";
import { format } from "date-fns";
import { PaymentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
type Payment = z.infer<typeof PaymentUncheckedCreateInputObjectSchema>; import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@radix-ui/react-select";
import { Input } from "@/components/ui/input";
import Decimal from "decimal.js";
import { toast } from "@/hooks/use-toast";
interface PaymentEditModalProps { type PaymentEditModalProps = {
isOpen: boolean; isOpen: boolean;
onOpenChange: (open: boolean) => void;
onClose: () => void; onClose: () => void;
payment: Payment; onEditServiceLine: (updatedPayment: PaymentWithExtras) => void;
onSave: () => void; payment: PaymentWithExtras | null;
} };
export default function PaymentEditModal({ export default function PaymentEditModal({
isOpen, isOpen,
onOpenChange,
onClose, onClose,
payment, payment,
onSave, onEditServiceLine,
}: PaymentEditModalProps) { }: PaymentEditModalProps) {
const { toast } = useToast(); if (!payment) return null;
const [form, setForm] = useState({
payerName: payment.payerName,
amountPaid: payment.amountPaid.toString(),
paymentDate: format(new Date(payment.paymentDate), "yyyy-MM-dd"),
paymentMethod: payment.paymentMethod,
note: payment.note || "",
});
const [loading, setLoading] = useState(false); const [expandedLineId, setExpandedLineId] = useState<number | null>(null);
const [updatedPaidAmounts, setUpdatedPaidAmounts] = useState<
Record<number, number>
>({});
const [updatedAdjustedAmounts, setUpdatedAdjustedAmounts] = useState<
Record<number, number>
>({});
const [updatedNotes, setUpdatedNotes] = useState<Record<number, string>>({});
const [updatedPaymentStatus, setUpdatedPaymentStatus] =
useState<PaymentStatus>(payment?.status ?? "PENDING");
const [updatedTransactions, setUpdatedTransactions] = useState(
() => payment?.transactions ?? []
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { type DraftPaymentData = {
const { name, value } = e.target; paidAmount: number;
setForm({ ...form, [name]: value }); adjustedAmount?: number;
notes?: string;
paymentStatus?: PaymentStatus;
payerName?: string;
method: PaymentMethod;
receivedDate: string;
};
const [serviceLineDrafts, setServiceLineDrafts] = useState<Record<number, any>>({});
const totalPaid = payment.transactions.reduce(
(sum, tx) =>
sum +
tx.serviceLinePayments.reduce((s, sp) => s + Number(sp.paidAmount), 0),
0
);
const totalBilled = payment.claim.serviceLines.reduce(
(sum, line) => sum + line.billedAmount,
0
);
const totalDue = totalBilled - totalPaid;
const handleEditServiceLine = (lineId: number) => {
setExpandedLineId(lineId === expandedLineId ? null : lineId);
};
const handleFieldChange = (lineId: number, field: string, value: any) => {
setServiceLineDrafts((prev) => ({
...prev,
[lineId]: {
...prev[lineId],
[field]: value,
},
}));
};
// const handleSavePayment = (lineId: number) => {
// const newPaidAmount = updatedPaidAmounts[lineId];
// const newAdjustedAmount = updatedAdjustedAmounts[lineId] ?? 0;
// const newNotes = updatedNotes[lineId] ?? "";
// if (newPaidAmount == null || isNaN(newPaidAmount)) return;
// const updatedTxs = updatedTransactions.map((tx) => ({
// ...tx,
// serviceLinePayments: tx.serviceLinePayments.map((sp) =>
// sp.serviceLineId === lineId
// ? {
// ...sp,
// paidAmount: new Decimal(newPaidAmount),
// adjustedAmount: new Decimal(newAdjustedAmount),
// notes: newNotes,
// }
// : sp
// ),
// }));
// const updatedPayment: PaymentWithExtras = {
// ...payment,
// transactions: updatedTxs,
// status: updatedPaymentStatus,
// };
// setUpdatedTransactions(updatedTxs);
// onEditServiceLine(updatedPayment);
// setExpandedLineId(null);
// };
const handleSavePayment = async (lineId: number) => {
const data = serviceLineDrafts[lineId];
if (!data || !data.paidAmount || !data.method || !data.receivedDate) {
console.log("please fill al")
return;
}
const transactionPayload = {
paymentId: payment.id,
amount: data.paidAmount + (data.adjustedAmount ?? 0),
method: data.method,
payerName: data.payerName,
notes: data.notes,
receivedDate: new Date(data.receivedDate),
serviceLinePayments: [
{
serviceLineId: lineId,
paidAmount: data.paidAmount,
adjustedAmount: data.adjustedAmount ?? 0,
notes: data.notes,
},
],
}; };
const handleSubmit = async () => {
setLoading(true);
try { try {
const res = await apiRequest("PUT", `/api/payments/${payment.id}`, { await onEditServiceLine(transactionPayload);
...form, setExpandedLineId(null);
amountPaid: parseFloat(form.amountPaid),
paymentDate: new Date(form.paymentDate),
});
if (!res.ok) throw new Error("Failed to update payment");
toast({ title: "Success", description: "Payment updated successfully" });
queryClient.invalidateQueries();
onClose(); onClose();
onSave(); } catch (err) {
} catch (error) { console.log(err)
toast({
title: "Error",
description: "Failed to update payment",
variant: "destructive",
});
} finally {
setLoading(false);
} }
}; };
const renderInput = (label: string, type: string, lineId: number, field: string, step?: string) => (
<div className="space-y-1">
<label className="text-sm font-medium">{label}</label>
<Input
type={type}
step={step}
value={serviceLineDrafts[lineId]?.[field] ?? ""}
onChange={(e) =>
handleFieldChange(lineId, field, type === "number" ? parseFloat(e.target.value) : e.target.value)
}
/>
</div>
);
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit Payment</DialogTitle> <DialogTitle>Edit Payment</DialogTitle>
<DialogDescription>
View and manage payments applied to service lines.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid gap-2"> {/* Claim + Patient Info */}
<Label htmlFor="payerName">Payer Name</Label> <div className="space-y-1">
<Input <h3 className="text-xl font-semibold">
id="payerName" {payment.claim.patientName}
name="payerName" </h3>
value={form.payerName} <p className="text-gray-500">
onChange={handleChange} Claim ID: {payment.claimId.toString().padStart(4, "0")}
/> </p>
<p className="text-gray-500">
Service Date:{" "}
{formatDateToHumanReadable(payment.claim.serviceDate)}
</p>
</div> </div>
<div className="grid gap-2"> {/* Payment Summary */}
<Label htmlFor="amountPaid">Amount Paid</Label> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<div>
<h4 className="font-medium text-gray-900">Payment Info</h4>
<div className="mt-2 space-y-1">
<p>
<span className="text-gray-500">Total Billed:</span> $
{totalBilled.toFixed(2)}
</p>
<p>
<span className="text-gray-500">Total Paid:</span> $
{totalPaid.toFixed(2)}
</p>
<p>
<span className="text-gray-500">Total Due:</span> $
{totalDue.toFixed(2)}
</p>
<div className="pt-2">
<label className="text-sm text-gray-600">Status</label>
<Select
value={updatedPaymentStatus}
onValueChange={(value: PaymentStatus) =>
setUpdatedPaymentStatus(value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{paymentStatusOptions.map((status) => (
<SelectItem key={status} value={status}>
{status}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div>
<h4 className="font-medium text-gray-900">Metadata</h4>
<div className="mt-2 space-y-1">
<p>
<span className="text-gray-500">Received Date:</span>{" "}
{payment.receivedDate
? formatDateToHumanReadable(payment.receivedDate)
: "N/A"}
</p>
<p>
<span className="text-gray-500">Method:</span>{" "}
{payment.paymentMethod ?? "N/A"}
</p>
<p>
<span className="text-gray-500">Notes:</span>{" "}
{payment.notes || "N/A"}
</p>
</div>
</div>
</div>
{/* Service Lines Payments */}
<div>
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
<div className="mt-2 space-y-3">
{payment.claim.serviceLines.length > 0 ? (
<>
{payment.claim.serviceLines.map((line) => {
const linePayments = payment.transactions.flatMap((tx) =>
tx.serviceLinePayments.filter(
(sp) => sp.serviceLineId === line.id
)
);
const paidAmount = linePayments.reduce(
(sum, sp) => sum + Number(sp.paidAmount),
0
);
const adjusted = linePayments.reduce(
(sum, sp) => sum + Number(sp.adjustedAmount),
0
);
const due = line.billedAmount - paidAmount;
return (
<div
key={line.id}
className="border p-3 rounded-md bg-gray-50"
>
<p>
<span className="text-gray-500">Procedure Code:</span>{" "}
{line.procedureCode}
</p>
<p>
<span className="text-gray-500">Billed:</span> $
{line.billedAmount.toFixed(2)}
</p>
<p>
<span className="text-gray-500">Paid:</span> $
{paidAmount.toFixed(2)}
</p>
<p>
<span className="text-gray-500">Adjusted:</span> $
{adjusted.toFixed(2)}
</p>
<p>
<span className="text-gray-500">Due:</span> $
{due.toFixed(2)}
</p>
<div className="pt-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEditServiceLine(line.id)}
>
{expandedLineId === line.id
? "Cancel"
: "Edit Payments"}
</Button>
</div>
{expandedLineId === line.id && (
<div className="mt-3 space-y-2">
<div className="space-y-1">
<label
htmlFor={`paid-${line.id}`}
className="text-sm font-medium"
>
Paid Amount
</label>
<Input <Input
id="amountPaid"
name="amountPaid"
type="number" type="number"
step="0.01" step="0.01"
value={form.amountPaid} placeholder="Paid Amount"
onChange={handleChange} defaultValue={paidAmount}
onChange={(e) =>
setUpdatedPaidAmounts({
...updatedPaidAmounts,
[line.id]: parseFloat(e.target.value),
})
}
/> />
</div> </div>
<div className="space-y-1">
<div className="grid gap-2"> <label
<Label htmlFor="paymentDate">Payment Date</Label> htmlFor={`adjusted-${line.id}`}
className="text-sm font-medium"
>
Adjusted Amount
</label>
<Input <Input
id="paymentDate" id={`adjusted-${line.id}`}
name="paymentDate" type="number"
type="date" step="0.01"
value={form.paymentDate} placeholder="Adjusted Amount"
onChange={handleChange} defaultValue={adjusted}
onChange={(e) =>
setUpdatedAdjustedAmounts({
...updatedAdjustedAmounts,
[line.id]: parseFloat(e.target.value),
})
}
/> />
</div> </div>
<div className="grid gap-2"> <div className="space-y-1">
<Label htmlFor="paymentMethod">Payment Method</Label> <label
htmlFor={`notes-${line.id}`}
className="text-sm font-medium"
>
Notes
</label>
<Input <Input
id="paymentMethod" id={`notes-${line.id}`}
name="paymentMethod" type="text"
value={form.paymentMethod} placeholder="Notes"
onChange={handleChange} onChange={(e) =>
setUpdatedNotes({
...updatedNotes,
[line.id]: e.target.value,
})
}
/> />
</div> </div>
<Button
<div className="grid gap-2"> size="sm"
<Label htmlFor="note">Note</Label> onClick={() => handleSavePayment(line.id)}
<Textarea >
id="note" Save
name="note"
value={form.note}
onChange={handleChange}
/>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={onClose} disabled={loading}>Cancel</Button>
<Button onClick={handleSubmit} disabled={loading}>
{loading ? "Saving..." : "Save Changes"}
</Button> </Button>
</DialogFooter> </div>
)}
</div>
);
})}
</>
) : (
<p className="text-gray-500">No service lines available.</p>
)}
</div>
</div>
{/* Transactions Overview */}
<div>
<h4 className="font-medium text-gray-900 pt-6">All Transactions</h4>
<div className="mt-2 space-y-2">
{payment.transactions.length > 0 ? (
payment.transactions.map((tx) => (
<div
key={tx.id}
className="border p-3 rounded-md bg-white shadow-sm"
>
<p>
<span className="text-gray-500">Date:</span>{" "}
{formatDateToHumanReadable(tx.receivedDate)}
</p>
<p>
<span className="text-gray-500">Amount:</span> $
{Number(tx.amount).toFixed(2)}
</p>
<p>
<span className="text-gray-500">Method:</span> {tx.method}
</p>
{tx.serviceLinePayments.map((sp) => (
<p key={sp.id} className="text-sm text-gray-600 ml-2">
Applied ${Number(sp.paidAmount).toFixed(2)} to service
line ID {sp.serviceLineId}
</p>
))}
</div>
))
) : (
<p className="text-gray-500">No transactions recorded.</p>
)}
</div>
</div>
{/* Actions */}
<div className="flex justify-end space-x-2 pt-6">
<Button variant="outline" onClick={onClose}>
Close
</Button>
</div>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@@ -29,84 +29,23 @@ import {
PaginationPrevious, PaginationPrevious,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import {
PaymentUncheckedCreateInputObjectSchema,
PaymentTransactionCreateInputObjectSchema,
ServiceLinePaymentCreateInputObjectSchema,
ClaimUncheckedCreateInputObjectSchema,
ClaimStatusSchema,
StaffUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas";
import { z } from "zod";
import { DeleteConfirmationDialog } from "../ui/deleteDialog"; import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import PaymentViewModal from "./payment-view-modal"; import PaymentViewModal from "./payment-view-modal";
import PaymentEditModal from "./payment-edit-modal"; import PaymentEditModal from "./payment-edit-modal";
import { Prisma } from "@repo/db/generated/prisma";
import LoadingScreen from "../ui/LoadingScreen"; import LoadingScreen from "../ui/LoadingScreen";
import {
type Payment = z.infer<typeof PaymentUncheckedCreateInputObjectSchema>; ClaimStatus,
type PaymentTransaction = z.infer< ClaimWithServiceLines,
typeof PaymentTransactionCreateInputObjectSchema Payment,
>; PaymentWithExtras,
type ServiceLinePayment = z.infer< } from "@repo/db/types";
typeof ServiceLinePaymentCreateInputObjectSchema import EditPaymentModal from "./payment-edit-modal";
>;
const insertPaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
updatedAt: true,
});
type InsertPayment = z.infer<typeof insertPaymentSchema>;
const updatePaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
type UpdatePayment = z.infer<typeof updatePaymentSchema>;
type PaymentWithExtras = Prisma.PaymentGetPayload<{
include: {
claim: { include: { serviceLines: true } };
servicePayments: true;
transactions: true;
};
}> & {
patientName: string;
paymentDate: Date;
paymentMethod: string;
};
interface PaymentApiResponse { interface PaymentApiResponse {
payments: PaymentWithExtras[]; payments: PaymentWithExtras[];
totalCount: number; totalCount: number;
} }
//creating types out of schema auto generated.
type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
type ClaimWithServiceLines = Claim & {
serviceLines: {
id: number;
claimId: number;
procedureCode: string;
procedureDate: Date;
oralCavityArea: string | null;
toothNumber: string | null;
toothSurface: string | null;
billedAmount: number;
}[];
staff: Staff | null;
};
interface PaymentsRecentTableProps { interface PaymentsRecentTableProps {
allowEdit?: boolean; allowEdit?: boolean;
allowView?: boolean; allowView?: boolean;
@@ -179,7 +118,7 @@ export default function PaymentsRecentTable({
}); });
const updatePaymentMutation = useMutation({ const updatePaymentMutation = useMutation({
mutationFn: async (payment: Payment) => { mutationFn: async (payment: PaymentWithExtras) => {
const response = await apiRequest("PUT", `/api/claims/${payment.id}`, { const response = await apiRequest("PUT", `/api/claims/${payment.id}`, {
data: payment, data: payment,
}); });
@@ -424,12 +363,14 @@ export default function PaymentsRecentTable({
.claim as ClaimWithServiceLines; .claim as ClaimWithServiceLines;
const totalBilled = getTotalBilled(claim); const totalBilled = getTotalBilled(claim);
const totalPaid = (
payment as PaymentWithExtras const totalPaid = (payment as PaymentWithExtras).transactions
).servicePayments.reduce( .flatMap((tx) => tx.serviceLinePayments)
.reduce(
(sum, sp) => sum + (sp.paidAmount?.toNumber?.() ?? 0), (sum, sp) => sum + (sp.paidAmount?.toNumber?.() ?? 0),
0 0
); );
const outstanding = totalBilled - totalPaid; const outstanding = totalBilled - totalPaid;
return ( return (
@@ -539,19 +480,19 @@ export default function PaymentsRecentTable({
onEditClaim={(payment) => handleEditPayment(payment)} onEditClaim={(payment) => handleEditPayment(payment)}
payment={currentPayment} payment={currentPayment}
/> />
)} )} */}
{isEditPaymentOpen && currentPayment && ( {isEditPaymentOpen && currentPayment && (
<ClaimPaymentModal <EditPaymentModal
isOpen={isEditPaymentOpen} isOpen={isEditPaymentOpen}
onClose={() => setIsEditPaymentOpen(false)}
onOpenChange={(open) => setIsEditPaymentOpen(open)} onOpenChange={(open) => setIsEditPaymentOpen(open)}
onClose={() => setIsEditPaymentOpen(false)}
payment={currentPayment} payment={currentPayment}
onSave={(updatedPayment) => { onEditServiceLine={(updatedPayment) => {
updatePaymentMutation.mutate(updatedPayment); updatePaymentMutation.mutate(updatedPayment);
}} }}
/> />
)} */} )}
{/* Pagination */} {/* Pagination */}
{totalPages > 1 && ( {totalPages > 1 && (

7
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@prisma/client": "^6.13.0", "@prisma/client": "^6.13.0",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"decimal.js": "^10.6.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
@@ -6312,6 +6313,12 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/decimal.js-light": { "node_modules/decimal.js-light": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",

View File

@@ -35,6 +35,7 @@
"dependencies": { "dependencies": {
"@prisma/client": "^6.13.0", "@prisma/client": "^6.13.0",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"decimal.js": "^10.6.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",

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",
"./generated/prisma" : "./generated/prisma/index.d.ts" "./generated/prisma" : "./generated/prisma/index.d.ts",
"./types" : "./types/index.ts"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.10.0", "@prisma/client": "^6.10.0",

View File

@@ -55,6 +55,7 @@ model Patient {
appointments Appointment[] appointments Appointment[]
claims Claim[] claims Claim[]
groups PdfGroup[] groups PdfGroup[]
payment Payment[]
@@index([insuranceId]) @@index([insuranceId])
@@index([createdAt]) @@index([createdAt])
@@ -135,10 +136,12 @@ model ServiceLine {
toothNumber String? toothNumber String?
toothSurface String? toothSurface String?
billedAmount Float billedAmount Float
totalPaid Decimal @default(0.00) @db.Decimal(10, 2)
totalAdjusted 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)
servicePayments ServiceLinePayment[] serviceLineTransactions ServiceLineTransaction[]
} }
enum ServiceLineStatus { enum ServiceLineStatus {
@@ -196,57 +199,45 @@ enum PdfCategory {
model Payment { model Payment {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
claimId Int @unique
patientId Int patientId Int
userId Int userId Int
claimId Int @unique
updatedById Int? updatedById Int?
totalBilled Decimal @db.Decimal(10, 2) totalBilled Decimal @db.Decimal(10, 2)
totalPaid Decimal @default(0.00) @db.Decimal(10, 2) totalPaid Decimal @default(0.00) @db.Decimal(10, 2)
totalDue Decimal @db.Decimal(10, 2) totalDue Decimal @db.Decimal(10, 2)
status PaymentStatus @default(PENDING) status PaymentStatus @default(PENDING)
receivedDate DateTime?
paymentMethod PaymentMethod?
notes String? notes String?
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)
updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id]) updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id])
transactions PaymentTransaction[] transactions ServiceLineTransaction[]
@@index([id]) @@index([id])
@@index([claimId]) @@index([claimId])
@@index([patientId]) @@index([patientId])
} }
model PaymentTransaction { model ServiceLineTransaction {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
paymentId Int paymentId Int
amount Decimal @db.Decimal(10, 2) serviceLineId Int
method PaymentMethod
transactionId String? transactionId String?
paidAmount Decimal @db.Decimal(10, 2)
adjustedAmount Decimal @default(0.00) @db.Decimal(10, 2)
method PaymentMethod
receivedDate DateTime receivedDate DateTime
payerName String? payerName String?
notes String? notes String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
payment Payment @relation(fields: [paymentId], references: [id], onDelete: Cascade) payment Payment @relation(fields: [paymentId], references: [id], onDelete: Cascade)
serviceLinePayments ServiceLinePayment[]
@@index([paymentId])
}
model ServiceLinePayment {
id Int @id @default(autoincrement())
transactionId Int
serviceLineId Int
paidAmount Decimal @db.Decimal(10, 2)
adjustedAmount Decimal @default(0.00) @db.Decimal(10, 2)
notes String?
transaction PaymentTransaction @relation(fields: [transactionId], references: [id], onDelete: Cascade)
serviceLine ServiceLine @relation(fields: [serviceLineId], references: [id], onDelete: Cascade) serviceLine ServiceLine @relation(fields: [serviceLineId], references: [id], onDelete: Cascade)
@@index([transactionId]) @@index([paymentId])
@@index([serviceLineId]) @@index([serviceLineId])
} }

View File

@@ -0,0 +1,2 @@
export * from "./payment-types";

View File

@@ -0,0 +1,149 @@
import {
PaymentUncheckedCreateInputObjectSchema,
PaymentTransactionCreateInputObjectSchema,
ServiceLinePaymentCreateInputObjectSchema,
ClaimUncheckedCreateInputObjectSchema,
ClaimStatusSchema,
StaffUncheckedCreateInputObjectSchema,
PaymentMethodSchema,
PaymentStatusSchema
} from "@repo/db/usedSchemas";
import { Prisma } from "@repo/db/generated/prisma";
import { z } from "zod";
import { Decimal } from "decimal.js";
// ========== BASIC TYPES ==========
// Payment basic type from Zod
export type Payment = z.infer<typeof PaymentUncheckedCreateInputObjectSchema>;
// Zod input type for creating a transaction
export type PaymentTransactionInput = z.infer<
typeof PaymentTransactionCreateInputObjectSchema
>;
// Prisma output type for single transaction (fetched with includes)
export type PaymentTransactionRecord = Prisma.PaymentTransactionGetPayload<{
include: {
serviceLinePayments: {
include: { serviceLine: true };
};
};
}>;
// Type for ServiceLinePayment input (used for updates/creates)
export type ServiceLinePayment = z.infer<
typeof ServiceLinePaymentCreateInputObjectSchema
>;
// Enum for payment
export type PaymentStatus = z.infer<typeof PaymentStatusSchema>;
export type PaymentMethod = z.infer<typeof PaymentMethodSchema>
// ✅ Runtime arrays (used in code logic / map / select options)
export const paymentStatusOptions = PaymentStatusSchema.options;
export const paymentMethodOptions = PaymentMethodSchema.options;
// ========== INPUT TYPES ==========
// Used to update individual service line payment
export type PartialServicePaymentInput = {
id: number;
paidAmount: Decimal;
adjustedAmount: Decimal;
notes: string | null;
};
// For creating a new payment
export const insertPaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export type InsertPayment = z.infer<typeof insertPaymentSchema>;
// For updating an existing payment (partial updates)
export const updatePaymentSchema = (
PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
)
.omit({
id: true,
createdAt: true,
})
.partial();
export type UpdatePayment = z.infer<typeof updatePaymentSchema>;
// Input for updating a payment with new transactions + updated service payments
export type UpdatePaymentInput = {
newTransactions: PaymentTransactionInput[];
servicePayments: PartialServicePaymentInput[];
totalPaid: Decimal;
totalDue: Decimal;
status: PaymentStatus;
};
// ========== EXTENDED TYPES ==========
// Payment with full nested data
export type PaymentWithExtras = Prisma.PaymentGetPayload<{
include: {
claim: {
include: {
serviceLines: true;
};
};
transactions: {
include: {
serviceLinePayments: {
include: {
serviceLine: true;
};
};
};
};
updatedBy: true;
};
}> & {
patientName: string;
paymentDate: Date;
paymentMethod: string;
};
// Claim model with embedded service lines
export type Claim = z.infer<typeof ClaimUncheckedCreateInputObjectSchema>;
export type ClaimStatus = z.infer<typeof ClaimStatusSchema>;
export type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
export type ClaimWithServiceLines = Claim & {
serviceLines: {
id: number;
claimId: number;
procedureCode: string;
procedureDate: Date;
oralCavityArea: string | null;
toothNumber: string | null;
toothSurface: string | null;
billedAmount: number;
status: string;
}[];
staff: Staff | null;
};
export type NewTransactionPayload = {
paymentId: number;
amount: number;
method: PaymentMethod;
payerName?: string;
notes?: string;
receivedDate: Date;
serviceLinePayments: {
serviceLineId: number;
paidAmount: number;
adjustedAmount: number;
notes?: string;
}[];
};

View File

@@ -12,3 +12,5 @@ export * from '../shared/schemas/enums/ClaimStatus.schema'
export * from '../shared/schemas/objects/PaymentUncheckedCreateInput.schema' export * from '../shared/schemas/objects/PaymentUncheckedCreateInput.schema'
export * from '../shared/schemas/objects/PaymentTransactionCreateInput.schema' export * from '../shared/schemas/objects/PaymentTransactionCreateInput.schema'
export * from '../shared/schemas/objects/ServiceLinePaymentCreateInput.schema' export * from '../shared/schemas/objects/ServiceLinePaymentCreateInput.schema'
export * from '../shared/schemas/enums/PaymentMethod.schema'
export * from '../shared/schemas/enums/PaymentStatus.schema'