payment checkpoint 3
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 handleSubmit = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
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">
|
||||||
<Input
|
<div>
|
||||||
id="amountPaid"
|
<h4 className="font-medium text-gray-900">Payment Info</h4>
|
||||||
name="amountPaid"
|
<div className="mt-2 space-y-1">
|
||||||
type="number"
|
<p>
|
||||||
step="0.01"
|
<span className="text-gray-500">Total Billed:</span> $
|
||||||
value={form.amountPaid}
|
{totalBilled.toFixed(2)}
|
||||||
onChange={handleChange}
|
</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>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
{/* Service Lines Payments */}
|
||||||
<Label htmlFor="paymentDate">Payment Date</Label>
|
<div>
|
||||||
<Input
|
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
|
||||||
id="paymentDate"
|
<div className="mt-2 space-y-3">
|
||||||
name="paymentDate"
|
{payment.claim.serviceLines.length > 0 ? (
|
||||||
type="date"
|
<>
|
||||||
value={form.paymentDate}
|
{payment.claim.serviceLines.map((line) => {
|
||||||
onChange={handleChange}
|
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
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Paid Amount"
|
||||||
|
defaultValue={paidAmount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setUpdatedPaidAmounts({
|
||||||
|
...updatedPaidAmounts,
|
||||||
|
[line.id]: parseFloat(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor={`adjusted-${line.id}`}
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
Adjusted Amount
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id={`adjusted-${line.id}`}
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Adjusted Amount"
|
||||||
|
defaultValue={adjusted}
|
||||||
|
onChange={(e) =>
|
||||||
|
setUpdatedAdjustedAmounts({
|
||||||
|
...updatedAdjustedAmounts,
|
||||||
|
[line.id]: parseFloat(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor={`notes-${line.id}`}
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
Notes
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id={`notes-${line.id}`}
|
||||||
|
type="text"
|
||||||
|
placeholder="Notes"
|
||||||
|
onChange={(e) =>
|
||||||
|
setUpdatedNotes({
|
||||||
|
...updatedNotes,
|
||||||
|
[line.id]: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSavePayment(line.id)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">No service lines available.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
{/* Transactions Overview */}
|
||||||
<Label htmlFor="paymentMethod">Payment Method</Label>
|
<div>
|
||||||
<Input
|
<h4 className="font-medium text-gray-900 pt-6">All Transactions</h4>
|
||||||
id="paymentMethod"
|
<div className="mt-2 space-y-2">
|
||||||
name="paymentMethod"
|
{payment.transactions.length > 0 ? (
|
||||||
value={form.paymentMethod}
|
payment.transactions.map((tx) => (
|
||||||
onChange={handleChange}
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
{/* Actions */}
|
||||||
<Label htmlFor="note">Note</Label>
|
<div className="flex justify-end space-x-2 pt-6">
|
||||||
<Textarea
|
<Button variant="outline" onClick={onClose}>
|
||||||
id="note"
|
Close
|
||||||
name="note"
|
</Button>
|
||||||
value={form.note}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="ghost" onClick={onClose} disabled={loading}>Cancel</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={loading}>
|
|
||||||
{loading ? "Saving..." : "Save Changes"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)
|
||||||
(sum, sp) => sum + (sp.paidAmount?.toNumber?.() ?? 0),
|
.reduce(
|
||||||
0
|
(sum, sp) => sum + (sp.paidAmount?.toNumber?.() ?? 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
7
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id])
|
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||||
transactions PaymentTransaction[]
|
updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id])
|
||||||
|
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)
|
||||||
receivedDate DateTime
|
adjustedAmount Decimal @default(0.00) @db.Decimal(10, 2)
|
||||||
payerName String?
|
method PaymentMethod
|
||||||
notes String?
|
receivedDate DateTime
|
||||||
createdAt DateTime @default(now())
|
payerName String?
|
||||||
payment Payment @relation(fields: [paymentId], references: [id], onDelete: Cascade)
|
notes String?
|
||||||
serviceLinePayments ServiceLinePayment[]
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
payment Payment @relation(fields: [paymentId], references: [id], onDelete: Cascade)
|
||||||
|
serviceLine ServiceLine @relation(fields: [serviceLineId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([paymentId])
|
@@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)
|
|
||||||
|
|
||||||
@@index([transactionId])
|
|
||||||
@@index([serviceLineId])
|
@@index([serviceLineId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
packages/db/types/index.ts
Normal file
2
packages/db/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
export * from "./payment-types";
|
||||||
149
packages/db/types/payment-types.ts
Normal file
149
packages/db/types/payment-types.ts
Normal 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;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
@@ -11,4 +11,6 @@ export * from '../shared/schemas/enums/PdfCategory.schema'
|
|||||||
export * from '../shared/schemas/enums/ClaimStatus.schema'
|
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'
|
||||||
Reference in New Issue
Block a user