From 89897ef2d6553ff8179612223b4c5d14ddd89f49 Mon Sep 17 00:00:00 2001 From: Potenz Date: Fri, 8 Aug 2025 00:34:56 +0530 Subject: [PATCH] payment checkpoint 3 --- apps/Backend/src/routes/payments.ts | 21 + apps/Backend/src/storage/index.ts | 114 ++-- .../payments/payment-edit-modal.tsx | 514 ++++++++++++++---- .../payments/payments-recent-table.tsx | 101 +--- package-lock.json | 7 + package.json | 1 + packages/db/package.json | 3 +- packages/db/prisma/schema.prisma | 57 +- packages/db/types/index.ts | 2 + packages/db/types/payment-types.ts | 149 +++++ packages/db/usedSchemas/index.ts | 4 +- 11 files changed, 699 insertions(+), 274 deletions(-) create mode 100644 packages/db/types/index.ts create mode 100644 packages/db/types/payment-types.ts diff --git a/apps/Backend/src/routes/payments.ts b/apps/Backend/src/routes/payments.ts index 139c189..456badd 100644 --- a/apps/Backend/src/routes/payments.ts +++ b/apps/Backend/src/routes/payments.ts @@ -202,6 +202,27 @@ router.get("/filter", async (req: Request, res: Response): Promise => { } }); +// GET /api/payments/:id +router.get("/:id", async (req: Request, res: Response): Promise => { + 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 router.post("/:claimId", async (req: Request, res: Response): Promise => { try { diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 651297e..c41663b 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -9,12 +9,14 @@ import { PdfFileUncheckedCreateInputObjectSchema, PdfGroupUncheckedCreateInputObjectSchema, PdfCategorySchema, - PaymentUncheckedCreateInputObjectSchema, - PaymentTransactionCreateInputObjectSchema, - ServiceLinePaymentCreateInputObjectSchema, } from "@repo/db/usedSchemas"; 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. type Appointment = z.infer; @@ -158,50 +160,6 @@ export interface ClaimPdfMetadata { uploadedAt: Date; } -// Base Payment type -type Payment = z.infer; -type PaymentTransaction = z.infer< - typeof PaymentTransactionCreateInputObjectSchema ->; -type ServiceLinePayment = z.infer< - typeof ServiceLinePaymentCreateInputObjectSchema ->; - -const insertPaymentSchema = ( - PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject -).omit({ - id: true, - createdAt: true, - updatedAt: true, -}); -type InsertPayment = z.infer; - -const updatePaymentSchema = ( - PaymentUncheckedCreateInputObjectSchema as unknown as z.ZodObject -) - .omit({ - id: true, - createdAt: true, - }) - .partial(); -type UpdatePayment = z.infer; - -type PaymentWithExtras = Prisma.PaymentGetPayload<{ - include: { - transactions: true; - servicePayments: true; - claim: { - include: { - serviceLines: true; - }; - }; - }; -}> & { - patientName: string; - paymentDate: Date; - paymentMethod: string; -}; - export interface IStorage { // User methods getUser(id: number): Promise; @@ -952,8 +910,16 @@ export const storage: IStorage = { serviceLines: true, }, }, - transactions: true, - servicePayments: true, + transactions: { + include: { + serviceLinePayments: { + include: { + serviceLine: true, + }, + }, + }, + }, + updatedBy: true, }, }); @@ -979,8 +945,16 @@ export const storage: IStorage = { serviceLines: true, }, }, - transactions: true, - servicePayments: true, + transactions: { + include: { + serviceLinePayments: { + include: { + serviceLine: true, + }, + }, + }, + }, + updatedBy: true, }, }); @@ -1006,8 +980,16 @@ export const storage: IStorage = { serviceLines: true, }, }, - transactions: true, - servicePayments: true, + transactions: { + include: { + serviceLinePayments: { + include: { + serviceLine: true, + }, + }, + }, + }, + updatedBy: true, }, }); @@ -1035,8 +1017,16 @@ export const storage: IStorage = { serviceLines: true, }, }, - transactions: true, - servicePayments: true, + transactions: { + include: { + serviceLinePayments: { + include: { + serviceLine: true, + }, + }, + }, + }, + updatedBy: true, }, }); @@ -1068,8 +1058,16 @@ export const storage: IStorage = { serviceLines: true, }, }, - transactions: true, - servicePayments: true, + transactions: { + include: { + serviceLinePayments: { + include: { + serviceLine: true, + }, + }, + }, + }, + updatedBy: true, }, }); diff --git a/apps/Frontend/src/components/payments/payment-edit-modal.tsx b/apps/Frontend/src/components/payments/payment-edit-modal.tsx index 8d727be..77a4455 100644 --- a/apps/Frontend/src/components/payments/payment-edit-modal.tsx +++ b/apps/Frontend/src/components/payments/payment-edit-modal.tsx @@ -1,140 +1,452 @@ -import { useState } from "react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { apiRequest, queryClient } from "@/lib/queryClient"; -import { useToast } from "@/hooks/use-toast"; -import { z } from "zod"; -import { format } from "date-fns"; -import { PaymentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import { formatDateToHumanReadable } from "@/utils/dateUtils"; +import React, { useState } from "react"; +import { paymentStatusOptions, PaymentWithExtras } from "@repo/db/types"; +import { PaymentStatus, PaymentMethod } from "@repo/db/types"; -type Payment = z.infer; +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; + onOpenChange: (open: boolean) => void; onClose: () => void; - payment: Payment; - onSave: () => void; -} + onEditServiceLine: (updatedPayment: PaymentWithExtras) => void; + payment: PaymentWithExtras | null; +}; export default function PaymentEditModal({ isOpen, + onOpenChange, onClose, payment, - onSave, + onEditServiceLine, }: PaymentEditModalProps) { - const { toast } = useToast(); - 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 || "", - }); + if (!payment) return null; - const [loading, setLoading] = useState(false); + const [expandedLineId, setExpandedLineId] = useState(null); + const [updatedPaidAmounts, setUpdatedPaidAmounts] = useState< + Record + >({}); + const [updatedAdjustedAmounts, setUpdatedAdjustedAmounts] = useState< + Record + >({}); + const [updatedNotes, setUpdatedNotes] = useState>({}); + const [updatedPaymentStatus, setUpdatedPaymentStatus] = + useState(payment?.status ?? "PENDING"); + const [updatedTransactions, setUpdatedTransactions] = useState( + () => payment?.transactions ?? [] + ); - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setForm({ ...form, [name]: value }); + type DraftPaymentData = { + paidAmount: number; + adjustedAmount?: number; + notes?: string; + paymentStatus?: PaymentStatus; + payerName?: string; + method: PaymentMethod; + receivedDate: string; }; - const handleSubmit = async () => { - setLoading(true); + + + const [serviceLineDrafts, setServiceLineDrafts] = useState>({}); + + + 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 { - const res = await apiRequest("PUT", `/api/payments/${payment.id}`, { - ...form, - 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(); + await onEditServiceLine(transactionPayload); + setExpandedLineId(null); onClose(); - onSave(); - } catch (error) { - toast({ - title: "Error", - description: "Failed to update payment", - variant: "destructive", - }); - } finally { - setLoading(false); + } catch (err) { + console.log(err) } }; + + const renderInput = (label: string, type: string, lineId: number, field: string, step?: string) => ( +
+ + + handleFieldChange(lineId, field, type === "number" ? parseFloat(e.target.value) : e.target.value) + } + /> +
+ ); + + return ( - - + + Edit Payment + + View and manage payments applied to service lines. +
-
- - + {/* Claim + Patient Info */} +
+

+ {payment.claim.patientName} +

+

+ Claim ID: {payment.claimId.toString().padStart(4, "0")} +

+

+ Service Date:{" "} + {formatDateToHumanReadable(payment.claim.serviceDate)} +

-
- - + {/* Payment Summary */} +
+
+

Payment Info

+
+

+ Total Billed: $ + {totalBilled.toFixed(2)} +

+

+ Total Paid: $ + {totalPaid.toFixed(2)} +

+

+ Total Due: $ + {totalDue.toFixed(2)} +

+
+ + +
+
+
+ +
+

Metadata

+
+

+ Received Date:{" "} + {payment.receivedDate + ? formatDateToHumanReadable(payment.receivedDate) + : "N/A"} +

+

+ Method:{" "} + {payment.paymentMethod ?? "N/A"} +

+

+ Notes:{" "} + {payment.notes || "N/A"} +

+
+
-
- - + {/* Service Lines Payments */} +
+

Service Lines

+
+ {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 ( +
+

+ Procedure Code:{" "} + {line.procedureCode} +

+

+ Billed: $ + {line.billedAmount.toFixed(2)} +

+

+ Paid: $ + {paidAmount.toFixed(2)} +

+

+ Adjusted: $ + {adjusted.toFixed(2)} +

+

+ Due: $ + {due.toFixed(2)} +

+ +
+ +
+ + {expandedLineId === line.id && ( +
+
+ + + setUpdatedPaidAmounts({ + ...updatedPaidAmounts, + [line.id]: parseFloat(e.target.value), + }) + } + /> +
+
+ + + setUpdatedAdjustedAmounts({ + ...updatedAdjustedAmounts, + [line.id]: parseFloat(e.target.value), + }) + } + /> +
+ +
+ + + setUpdatedNotes({ + ...updatedNotes, + [line.id]: e.target.value, + }) + } + /> +
+ +
+ )} +
+ ); + })} + + ) : ( +

No service lines available.

+ )} +
-
- - + {/* Transactions Overview */} +
+

All Transactions

+
+ {payment.transactions.length > 0 ? ( + payment.transactions.map((tx) => ( +
+

+ Date:{" "} + {formatDateToHumanReadable(tx.receivedDate)} +

+

+ Amount: $ + {Number(tx.amount).toFixed(2)} +

+

+ Method: {tx.method} +

+ {tx.serviceLinePayments.map((sp) => ( +

+ • Applied ${Number(sp.paidAmount).toFixed(2)} to service + line ID {sp.serviceLineId} +

+ ))} +
+ )) + ) : ( +

No transactions recorded.

+ )} +
-
- -