diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 6da7b91..183d019 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -7,7 +7,8 @@ import multer from "multer"; import { forwardToSeleniumClaimAgent } from "../services/seleniumClaimClient"; import path from "path"; import axios from "axios"; -import fs from "fs"; +import { Prisma } from "@repo/db/generated/prisma"; +import { Decimal } from "@prisma/client/runtime/library"; const router = Router(); @@ -284,8 +285,36 @@ router.post("/", async (req: Request, res: Response): Promise => { userId: req.user!.id, }); - const newClaim = await storage.createClaim(parsedClaim); - res.status(201).json(newClaim); + // Step 1: Calculate total billed from service lines + const serviceLinesCreateInput = ( + parsedClaim.serviceLines as Prisma.ServiceLineCreateNestedManyWithoutClaimInput + )?.create; + const lines = Array.isArray(serviceLinesCreateInput) + ? (serviceLinesCreateInput as unknown as { amount: number }[]) + : []; + const totalBilled = lines.reduce( + (sum, line) => sum + (line.amount ?? 0), + 0 + ); + + // Step 2: Create claim (with service lines) + const claim = await storage.createClaim(parsedClaim); + + // Step 3: Create empty payment + await storage.createPayment({ + patientId: claim.patientId, + userId: req.user!.id, + claimId: claim.id, + totalBilled: new Decimal(totalBilled), + totalPaid: new Decimal(0), + totalDue: new Decimal(totalBilled), + status: "PENDING", + notes: null, + paymentMethod: null, + receivedDate: null, + }); + + res.status(201).json(claim); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index b19b8dd..651297e 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -38,13 +38,7 @@ const updateAppointmentSchema = ( type UpdateAppointment = z.infer; //patient types -const PatientSchema = ( - PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject -).omit({ - appointments: true, -}); type Patient = z.infer; -type Patient2 = z.infer; const insertPatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -196,9 +190,17 @@ type PaymentWithExtras = Prisma.PaymentGetPayload<{ include: { transactions: true; servicePayments: true; - claim: true; + claim: { + include: { + serviceLines: true; + }; + }; }; -}>; +}> & { + patientName: string; + paymentDate: Date; + paymentMethod: string; +}; export interface IStorage { // User methods @@ -942,45 +944,79 @@ export const storage: IStorage = { id: number, userId: number ): Promise { - return db.payment.findFirst({ + const payment = await db.payment.findFirst({ where: { id, userId }, include: { - claim: true, + claim: { + include: { + serviceLines: true, + }, + }, transactions: true, servicePayments: true, }, }); + + if (!payment) return null; + + return { + ...payment, + patientName: payment.claim?.patientName ?? "", + paymentDate: payment.createdAt, + paymentMethod: payment.transactions[0]?.method ?? "OTHER", + }; }, async getPaymentsByClaimId( claimId: number, userId: number ): Promise { - return db.payment.findFirst({ + const payment = await db.payment.findFirst({ where: { claimId, userId }, include: { - claim: true, + claim: { + include: { + serviceLines: true, + }, + }, transactions: true, servicePayments: true, }, }); + + if (!payment) return null; + + return { + ...payment, + patientName: payment.claim?.patientName ?? "", + paymentDate: payment.createdAt, + paymentMethod: payment.transactions[0]?.method ?? "OTHER", + }; }, async getPaymentsByPatientId( patientId: number, userId: number ): Promise { - return db.payment.findMany({ - where: { - patientId, - userId, - }, + const payments = await db.payment.findMany({ + where: { patientId, userId }, include: { - claim: true, + claim: { + include: { + serviceLines: true, + }, + }, transactions: true, servicePayments: true, }, }); + + return payments.map((payment) => ({ + ...payment, + patientName: payment.claim?.patientName ?? "", + paymentDate: payment.createdAt, + paymentMethod: payment.transactions[0]?.method ?? "OTHER", + })); }, async getRecentPaymentsByUser( @@ -988,17 +1024,28 @@ export const storage: IStorage = { limit: number, offset: number ): Promise { - return db.payment.findMany({ + const payments = await db.payment.findMany({ where: { userId }, orderBy: { createdAt: "desc" }, skip: offset, take: limit, include: { - claim: true, + claim: { + include: { + serviceLines: true, + }, + }, transactions: true, servicePayments: true, }, }); + + return payments.map((payment) => ({ + ...payment, + patientName: payment.claim?.patientName ?? "", + paymentDate: payment.createdAt, + paymentMethod: payment.transactions[0]?.method ?? "OTHER", + })); }, async getPaymentsByDateRange( @@ -1006,7 +1053,7 @@ export const storage: IStorage = { from: Date, to: Date ): Promise { - return db.payment.findMany({ + const payments = await db.payment.findMany({ where: { userId, createdAt: { @@ -1016,11 +1063,22 @@ export const storage: IStorage = { }, orderBy: { createdAt: "desc" }, include: { - claim: true, + claim: { + include: { + serviceLines: true, + }, + }, transactions: true, servicePayments: true, }, }); + + return payments.map((payment) => ({ + ...payment, + patientName: payment.claim?.patientName ?? "", + paymentDate: payment.createdAt, + paymentMethod: payment.transactions[0]?.method ?? "OTHER", + })); }, async getTotalPaymentCountByUser(userId: number): Promise { diff --git a/apps/Frontend/src/components/claims/claims-recent-table.tsx b/apps/Frontend/src/components/claims/claims-recent-table.tsx index 7c7e6cb..8fc028b 100644 --- a/apps/Frontend/src/components/claims/claims-recent-table.tsx +++ b/apps/Frontend/src/components/claims/claims-recent-table.tsx @@ -35,7 +35,6 @@ import { StaffUncheckedCreateInputObjectSchema, } from "@repo/db/usedSchemas"; import { z } from "zod"; -import { useAuth } from "@/hooks/use-auth"; import LoadingScreen from "@/components/ui/LoadingScreen"; import { Checkbox } from "@/components/ui/checkbox"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; @@ -118,6 +117,10 @@ export default function ClaimsRecentTable({ } }; + useEffect(() => { + setCurrentPage(1); + }, [patientId]); + const getClaimsQueryKey = () => patientId ? ["claims-recent", "patient", patientId, currentPage] @@ -243,8 +246,9 @@ export default function ClaimsRecentTable({ const totalPages = useMemo( () => Math.ceil((claimsData?.totalCount || 0) / claimsPerPage), - [claimsData] + [claimsData?.totalCount, claimsPerPage] ); + const startItem = offset + 1; const endItem = Math.min(offset + claimsPerPage, claimsData?.totalCount || 0); @@ -315,264 +319,286 @@ export default function ClaimsRecentTable({ ); }; - return ( -
-
- - - - {allowCheckbox && Select} - Claim ID - Patient Name - Submission Date - Insurance Provider - Member ID - Total Billed - Status - Actions - - - - {isLoading ? ( - - - - - - ) : isError ? ( - - - Error loading claims. - - - ) : (claimsData?.claims ?? []).length === 0 ? ( - - - No claims found. - - - ) : ( - claimsData?.claims.map((claim) => ( - - {allowCheckbox && ( - - handleSelectClaim(claim)} - /> - - )} - -
- CML-{claim.id!.toString().padStart(4, "0")} -
-
- -
- - - {getInitialsFromName(claim.patientName)} - - + function getPageNumbers(current: number, total: number): (number | "...")[] { + const delta = 2; + const range: (number | "...")[] = []; + const left = Math.max(2, current - delta); + const right = Math.min(total - 1, current + delta); -
-
- {claim.patientName} -
-
- DOB: {formatDateToHumanReadable(claim.dateOfBirth)} -
+ range.push(1); + if (left > 2) range.push("..."); + + for (let i = left; i <= right; i++) { + range.push(i); + } + + if (right < total - 1) range.push("..."); + if (total > 1) range.push(total); + + return range; + } + + return ( +
+
+
+ + + {allowCheckbox && Select} + Claim ID + Patient Name + Submission Date + Insurance Provider + Member ID + Total Billed + Status + Actions + + + + {isLoading ? ( + + + + + + ) : isError ? ( + + + Error loading claims. + + + ) : (claimsData?.claims ?? []).length === 0 ? ( + + + No claims found. + + + ) : ( + claimsData?.claims.map((claim) => ( + + {allowCheckbox && ( + + handleSelectClaim(claim)} + /> + + )} + +
+ CML-{claim.id!.toString().padStart(4, "0")} +
+
+ +
+ + + {getInitialsFromName(claim.patientName)} + + + +
+
+ {claim.patientName} +
+
+ DOB: {formatDateToHumanReadable(claim.dateOfBirth)}
- - -
- {formatDateToHumanReadable(claim.createdAt!)} -
-
- -
- {claim.insuranceProvider ?? "Not specified"} -
-
- -
- {claim.memberId ?? "Not specified"} -
-
- -
- ${getTotalBilled(claim).toFixed(2)} -
-
+
+
+ +
+ {formatDateToHumanReadable(claim.createdAt!)} +
+
+ +
+ {claim.insuranceProvider ?? "Not specified"} +
+
+ +
+ {claim.memberId ?? "Not specified"} +
+
+ +
+ ${getTotalBilled(claim).toFixed(2)} +
+
- -
- {(() => { - const { label, color, icon } = getStatusInfo( - claim.status - ); - return ( - - - {icon} - {label} - + +
+ {(() => { + const { label, color, icon } = getStatusInfo( + claim.status + ); + return ( + + + {icon} + {label} - ); - })()} -
-
+
+ ); + })()} +
+
- -
- {allowDelete && ( - - )} - {allowEdit && ( - - )} - {allowView && ( - - )} -
-
-
- )) - )} -
-
-
+ +
+ {allowDelete && ( + + )} + {allowEdit && ( + + )} + {allowView && ( + + )} +
+
+ + )) + )} + + +
- setIsDeleteClaimOpen(false)} - entityName={currentClaim?.patientName} + setIsDeleteClaimOpen(false)} + entityName={currentClaim?.patientName} + /> + + {isViewClaimOpen && currentClaim && ( + setIsViewClaimOpen(false)} + onOpenChange={(open) => setIsViewClaimOpen(open)} + onEditClaim={(claim) => handleEditClaim(claim)} + claim={currentClaim} /> + )} - {isViewClaimOpen && currentClaim && ( - setIsViewClaimOpen(false)} - onOpenChange={(open) => setIsViewClaimOpen(open)} - onEditClaim={(claim) => handleEditClaim(claim)} - claim={currentClaim} - /> - )} + {isEditClaimOpen && currentClaim && ( + setIsEditClaimOpen(false)} + onOpenChange={(open) => setIsEditClaimOpen(open)} + claim={currentClaim} + onSave={(updatedClaim) => { + updateClaimMutation.mutate(updatedClaim); + }} + /> + )} - {isEditClaimOpen && currentClaim && ( - setIsEditClaimOpen(false)} - onOpenChange={(open) => setIsEditClaimOpen(open)} - claim={currentClaim} - onSave={(updatedClaim) => { - updateClaimMutation.mutate(updatedClaim); - }} - /> - )} + {/* Pagination */} + {totalPages > 1 && ( +
+
+
+ Showing {startItem}–{endItem} of {claimsData?.totalCount || 0}{" "} + results +
+ + + + { + e.preventDefault(); + if (currentPage > 1) setCurrentPage(currentPage - 1); + }} + className={ + currentPage === 1 ? "pointer-events-none opacity-50" : "" + } + /> + - {/* Pagination */} - {totalPages > 1 && ( -
-
-
- Showing {startItem}–{endItem} of {claimsData?.totalCount || 0}{" "} - results -
- - - - { - e.preventDefault(); - if (currentPage > 1) setCurrentPage(currentPage - 1); - }} - className={ - currentPage === 1 - ? "pointer-events-none opacity-50" - : "" - } - /> - - - {Array.from({ length: totalPages }).map((_, i) => ( - + {getPageNumbers(currentPage, totalPages).map((page, idx) => ( + + {page === "..." ? ( + ... + ) : ( { e.preventDefault(); - setCurrentPage(i + 1); + setCurrentPage(page as number); }} - isActive={currentPage === i + 1} + isActive={currentPage === page} > - {i + 1} + {page} - - ))} - - { - e.preventDefault(); - if (currentPage < totalPages) - setCurrentPage(currentPage + 1); - }} - className={ - currentPage === totalPages - ? "pointer-events-none opacity-50" - : "" - } - /> + )} - - -
+ ))} + + + { + e.preventDefault(); + if (currentPage < totalPages) + setCurrentPage(currentPage + 1); + }} + className={ + currentPage === totalPages + ? "pointer-events-none opacity-50" + : "" + } + /> + + +
- )} -
+
+ )} + ); } diff --git a/apps/Frontend/src/components/patients/patient-table.tsx b/apps/Frontend/src/components/patients/patient-table.tsx index f007d78..fce8bf4 100644 --- a/apps/Frontend/src/components/patients/patient-table.tsx +++ b/apps/Frontend/src/components/patients/patient-table.tsx @@ -334,6 +334,25 @@ export function PatientTable({ return colorClasses[id % colorClasses.length]; }; + function getPageNumbers(current: number, total: number): (number | "...")[] { + const delta = 2; + const range: (number | "...")[] = []; + const left = Math.max(2, current - delta); + const right = Math.min(total - 1, current + delta); + + range.push(1); + if (left > 2) range.push("..."); + + for (let i = left; i <= right; i++) { + range.push(i); + } + + if (right < total - 1) range.push("..."); + if (total > 1) range.push(total); + + return range; + } + return (
@@ -712,21 +731,26 @@ export function PatientTable({ } /> + + {getPageNumbers(currentPage, totalPages).map((page, idx) => ( + + {page === "..." ? ( + ... + ) : ( + { + e.preventDefault(); + setCurrentPage(page as number); + }} + isActive={currentPage === page} + > + {page} + + )} + + ))} - {Array.from({ length: totalPages }).map((_, i) => ( - - { - e.preventDefault(); - setCurrentPage(i + 1); - }} - isActive={currentPage === i + 1} - > - {i + 1} - - - ))} ; type PaymentTransaction = z.infer< @@ -45,24 +73,46 @@ type UpdatePayment = z.infer; type PaymentWithExtras = Prisma.PaymentGetPayload<{ include: { - transactions: true; + claim: { include: { serviceLines: true } }; servicePayments: true; - claim: true; + transactions: true; }; -}>; +}> & { + patientName: string; + paymentDate: Date; + paymentMethod: string; +}; interface PaymentApiResponse { - payments: Payment[]; + payments: PaymentWithExtras[]; totalCount: number; } +//creating types out of schema auto generated. +type Claim = z.infer; +export type ClaimStatus = z.infer; +type Staff = z.infer; + +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 { allowEdit?: boolean; allowView?: boolean; allowDelete?: boolean; allowCheckbox?: boolean; - onSelectPayment?: (payment: Payment | null) => void; + onSelectPayment?: (payment: PaymentWithExtras | null) => void; onPageChange?: (page: number) => void; claimId?: number; } @@ -77,219 +127,494 @@ export default function PaymentsRecentTable({ claimId, }: PaymentsRecentTableProps) { const { toast } = useToast(); + + const [isViewPaymentOpen, setIsViewPaymentOpen] = useState(false); + const [isEditPaymentOpen, setIsEditPaymentOpen] = useState(false); + const [isDeletePaymentOpen, setIsDeletePaymentOpen] = useState(false); + const [currentPage, setCurrentPage] = useState(1); const paymentsPerPage = 5; const offset = (currentPage - 1) * paymentsPerPage; - const [selectedPaymentId, setSelectedPaymentId] = useState(null); - const [currentPayment, setCurrentPayment] = useState(null); - const [isViewOpen, setIsViewOpen] = useState(false); - const [isEditOpen, setIsEditOpen] = useState(false); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [currentPayment, setCurrentPayment] = useState< + PaymentWithExtras | undefined + >(undefined); + const [selectedPaymentId, setSelectedPaymentId] = useState( + null + ); - const getQueryKey = () => + const handleSelectPayment = (payment: PaymentWithExtras) => { + const isSelected = selectedPaymentId === payment.id; + const newSelectedId = isSelected ? null : payment.id; + setSelectedPaymentId(Number(newSelectedId)); + if (onSelectPayment) { + onSelectPayment(isSelected ? null : payment); + } + }; + + const getPaymentsQueryKey = () => claimId - ? ["payments", "claim", claimId, currentPage] - : ["payments", "recent", currentPage]; + ? ["payments-recent", "claim", claimId, currentPage] + : ["payments-recent", "global", currentPage]; const { data: paymentsData, isLoading, isError, } = useQuery({ - queryKey: getQueryKey(), + queryKey: getPaymentsQueryKey(), queryFn: async () => { const endpoint = claimId ? `/api/payments/claim/${claimId}?limit=${paymentsPerPage}&offset=${offset}` : `/api/payments/recent?limit=${paymentsPerPage}&offset=${offset}`; const res = await apiRequest("GET", endpoint); - if (!res.ok) throw new Error("Failed to fetch payments"); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.message || "Failed to fetch payments"); + } return res.json(); }, placeholderData: { payments: [], totalCount: 0 }, }); - const deleteMutation = useMutation({ - mutationFn: async (id: number) => { - const res = await apiRequest("DELETE", `/api/payments/${id}`); - if (!res.ok) throw new Error("Failed to delete"); + const updatePaymentMutation = useMutation({ + mutationFn: async (payment: Payment) => { + const response = await apiRequest("PUT", `/api/claims/${payment.id}`, { + data: payment, + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Failed to update Payment"); + } + return response.json(); }, onSuccess: () => { - toast({ title: "Deleted", description: "Payment deleted successfully" }); - setIsDeleteOpen(false); - queryClient.invalidateQueries({ queryKey: getQueryKey() }); + setIsEditPaymentOpen(false); + toast({ + title: "Success", + description: "Payment updated successfully!", + variant: "default", + }); + queryClient.invalidateQueries({ + queryKey: getPaymentsQueryKey(), + }); }, - onError: () => { - toast({ title: "Error", description: "Delete failed", variant: "destructive" }); + onError: (error) => { + toast({ + title: "Error", + description: `Update failed: ${error.message}`, + variant: "destructive", + }); }, }); - const handleSelectPayment = (payment: Payment) => { - const isSelected = selectedPaymentId === payment.id; - const newSelectedId = isSelected ? null : payment.id; - setSelectedPaymentId(newSelectedId); - onSelectPayment?.(isSelected ? null : payment); + const deletePaymentMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("DELETE", `/api/payments/${id}`); + return; + }, + + onSuccess: () => { + setIsDeletePaymentOpen(false); + queryClient.invalidateQueries({ + queryKey: getPaymentsQueryKey(), + }); + toast({ + title: "Deleted", + description: "Payment deleted successfully", + variant: "default", + }); + }, + onError: (error) => { + toast({ + title: "Error", + description: `Failed to delete payment: ${error.message})`, + variant: "destructive", + }); + }, + }); + + const handleEditPayment = (payment: PaymentWithExtras) => { + setCurrentPayment(payment); + setIsEditPaymentOpen(true); }; - const handleDelete = () => { - if (currentPayment?.id) { - deleteMutation.mutate(currentPayment.id); + const handleViewPayment = (payment: PaymentWithExtras) => { + setCurrentPayment(payment); + setIsViewPaymentOpen(true); + }; + + const handleDeletePayment = (payment: PaymentWithExtras) => { + setCurrentPayment(payment); + setIsDeletePaymentOpen(true); + }; + + const handleConfirmDeletePayment = async () => { + if (currentPayment) { + if (typeof currentPayment.id === "number") { + deletePaymentMutation.mutate(currentPayment.id); + } else { + toast({ + title: "Error", + description: "Selected Payment is missing an ID for deletion.", + variant: "destructive", + }); + } + } else { + toast({ + title: "Error", + description: "No Payment selected for deletion.", + variant: "destructive", + }); } }; useEffect(() => { - onPageChange?.(currentPage); - }, [currentPage]); + if (onPageChange) onPageChange(currentPage); + }, [currentPage, onPageChange]); + + useEffect(() => { + setCurrentPage(1); + }, [claimId]); const totalPages = useMemo( () => Math.ceil((paymentsData?.totalCount || 0) / paymentsPerPage), - [paymentsData] + [paymentsData?.totalCount, paymentsPerPage] ); + const startItem = offset + 1; + const endItem = Math.min( + offset + paymentsPerPage, + paymentsData?.totalCount || 0 + ); + + const getInitialsFromName = (fullName: string) => { + const parts = fullName.trim().split(/\s+/); + const filteredParts = parts.filter((part) => part.length > 0); + if (filteredParts.length === 0) { + return ""; + } + const firstInitial = filteredParts[0]!.charAt(0).toUpperCase(); + if (filteredParts.length === 1) { + return firstInitial; + } else { + const lastInitial = + filteredParts[filteredParts.length - 1]!.charAt(0).toUpperCase(); + return firstInitial + lastInitial; + } + }; + + const getAvatarColor = (id: number) => { + const colorClasses = [ + "bg-blue-500", + "bg-teal-500", + "bg-amber-500", + "bg-rose-500", + "bg-indigo-500", + "bg-green-500", + "bg-purple-500", + ]; + return colorClasses[id % colorClasses.length]; + }; + + const getStatusInfo = (status?: ClaimStatus) => { + switch (status) { + case "PENDING": + return { + label: "Pending", + color: "bg-yellow-100 text-yellow-800", + icon: , + }; + case "APPROVED": + return { + label: "Approved", + color: "bg-green-100 text-green-800", + icon: , + }; + case "CANCELLED": + return { + label: "Cancelled", + color: "bg-red-100 text-red-800", + icon: , + }; + default: + return { + label: status + ? status.charAt(0).toUpperCase() + status.slice(1) + : "Unknown", + color: "bg-gray-100 text-gray-800", + icon: , + }; + } + }; + + const getTotalBilled = (claim: ClaimWithServiceLines) => { + return claim.serviceLines.reduce( + (sum, line) => sum + (line.billedAmount || 0), + 0 + ); + }; + + function getPageNumbers(current: number, total: number): (number | "...")[] { + const delta = 2; + const range: (number | "...")[] = []; + const left = Math.max(2, current - delta); + const right = Math.min(total - 1, current + delta); + + range.push(1); + if (left > 2) range.push("..."); + + for (let i = left; i <= right; i++) { + range.push(i); + } + + if (right < total - 1) range.push("..."); + if (total > 1) range.push(total); + + return range; + } + return ( -
+
{allowCheckbox && Select} Payment ID - Payer + Patient Name Amount Date Method - Note Actions {isLoading ? ( - Loading... + + + ) : isError ? ( - Error loading payments + + Error loading payments. + ) : paymentsData?.payments.length === 0 ? ( - No payments found + + No payments found + ) : ( - paymentsData?.payments.map((payment) => ( - - {allowCheckbox && ( + paymentsData?.payments.map((payment) => { + const claim = (payment as PaymentWithExtras) + .claim as ClaimWithServiceLines; + + const totalBilled = getTotalBilled(claim); + const totalPaid = ( + payment as PaymentWithExtras + ).servicePayments.reduce( + (sum, sp) => sum + (sp.paidAmount?.toNumber?.() ?? 0), + 0 + ); + const outstanding = totalBilled - totalPaid; + + return ( + + {allowCheckbox && ( + + handleSelectPayment(payment)} + /> + + )} - handleSelectPayment(payment)} - /> + {typeof payment.id === "number" + ? `PAY-${payment.id.toString().padStart(4, "0")}` + : "N/A"} - )} - {`PAY-${payment.id.toString().padStart(4, "0")}`} - {payment.payerName} - ${payment.amountPaid.toFixed(2)} - {formatDateToHumanReadable(payment.paymentDate)} - {payment.paymentMethod} - {payment.note || "—"} - - {allowDelete && ( - - )} - {allowEdit && ( - - )} - {allowView && ( - - )} - - - )) + {payment.patientName} + {/* 💰 Billed / Paid / Due breakdown */} + +
+ + Total Billed: $ + {totalBilled.toFixed(2)} + + + Total Paid: ${totalPaid.toFixed(2)} + + + Total Due:{" "} + {outstanding > 0 ? ( + + ${outstanding.toFixed(2)} + + ) : ( + Settled + )} + +
+
+ + {formatDateToHumanReadable(payment.paymentDate)} + + {payment.paymentMethod} + +
+ {allowDelete && ( + + )} + {allowEdit && ( + + )} + {allowView && ( + + )} +
+
+
+ ); + }) )}
+ setIsDeletePaymentOpen(false)} + entityName={String(currentPayment?.claimId)} + /> + + {/* /will hanlde both modal later */} + {/* {isViewPaymentOpen && currentPayment && ( + setIsViewPaymentOpen(false)} + onOpenChange={(open) => setIsViewPaymentOpen(open)} + onEditClaim={(payment) => handleEditPayment(payment)} + payment={currentPayment} + /> + )} + + {isEditPaymentOpen && currentPayment && ( + setIsEditPaymentOpen(false)} + onOpenChange={(open) => setIsEditPaymentOpen(open)} + payment={currentPayment} + onSave={(updatedPayment) => { + updatePaymentMutation.mutate(updatedPayment); + }} + /> + )} */} + {/* Pagination */} {totalPages > 1 && ( -
-
- Showing {(offset + 1)}–{Math.min(offset + paymentsPerPage, paymentsData?.totalCount || 0)} of {paymentsData?.totalCount || 0} -
- - - - { - e.preventDefault(); - if (currentPage > 1) setCurrentPage(currentPage - 1); - }} - className={currentPage === 1 ? "pointer-events-none opacity-50" : ""} - /> - - {Array.from({ length: totalPages }).map((_, i) => ( - - +
+
+ Showing {startItem}–{endItem} of {paymentsData?.totalCount || 0}{" "} + results +
+ + + + { e.preventDefault(); - setCurrentPage(i + 1); + if (currentPage > 1) setCurrentPage(currentPage - 1); }} - isActive={currentPage === i + 1} - > - {i + 1} - + className={ + currentPage === 1 ? "pointer-events-none opacity-50" : "" + } + /> - ))} - - { - e.preventDefault(); - if (currentPage < totalPages) setCurrentPage(currentPage + 1); - }} - className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""} - /> - - - + + {getPageNumbers(currentPage, totalPages).map((page, idx) => ( + + {page === "..." ? ( + ... + ) : ( + { + e.preventDefault(); + setCurrentPage(page as number); + }} + isActive={currentPage === page} + > + {page} + + )} + + ))} + + + { + e.preventDefault(); + if (currentPage < totalPages) + setCurrentPage(currentPage + 1); + }} + className={ + currentPage === totalPages + ? "pointer-events-none opacity-50" + : "" + } + /> + + + +
)} - - {/* Modals */} - {isViewOpen && currentPayment && ( - setIsViewOpen(false)} - payment={currentPayment} - /> - )} - - {isEditOpen && currentPayment && ( - setIsEditOpen(false)} - payment={currentPayment} - onSave={() => { - queryClient.invalidateQueries({ queryKey: getQueryKey() }); - }} - /> - )} - - setIsDeleteOpen(false)} - onConfirm={handleDelete} - entityName={`Payment ${currentPayment?.id}`} - />
); } diff --git a/apps/Frontend/src/pages/payments-page.tsx b/apps/Frontend/src/pages/payments-page.tsx index 5707e5a..819c213 100644 --- a/apps/Frontend/src/pages/payments-page.tsx +++ b/apps/Frontend/src/pages/payments-page.tsx @@ -2,29 +2,41 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { TopAppBar } from "@/components/layout/top-app-bar"; import { Sidebar } from "@/components/layout/sidebar"; -import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"; +import { + Card, + CardHeader, + CardTitle, + CardContent, + CardFooter, +} from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/hooks/use-auth"; -import { - CreditCard, - Clock, - CheckCircle, - AlertCircle, - DollarSign, - Receipt, +import { + CreditCard, + Clock, + CheckCircle, + AlertCircle, + DollarSign, + Receipt, Plus, ArrowDown, - ReceiptText + ReceiptText, + Upload, + Image, + X, + Trash2, + Save, } from "lucide-react"; -import { format } from "date-fns"; -import { - Table, - TableHeader, - TableBody, - TableRow, - TableHead, - TableCell +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, } from "@/components/ui/table"; import { Select, @@ -33,26 +45,21 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import PaymentsRecentTable from "@/components/payments/payments-recent-table"; + +import { + AppointmentUncheckedCreateInputObjectSchema, + PatientUncheckedCreateInputObjectSchema, +} from "@repo/db/usedSchemas"; import { z } from "zod"; -const PatientSchema = ( - PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject -).omit({ - appointments: true, -}); -type Patient = z.infer; - -const insertPatientSchema = ( - PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject -).omit({ - id: true, - createdAt: true, - userId: true, -}); -type InsertPatient = z.infer; - - //creating types out of schema auto generated. type Appointment = z.infer; @@ -70,27 +77,64 @@ const updateAppointmentSchema = ( .omit({ id: true, createdAt: true, - userId: true, }) .partial(); type UpdateAppointment = z.infer; +//patient types +type Patient = z.infer; + +const insertPatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).omit({ + id: true, + createdAt: true, +}); +type InsertPatient = z.infer; + +const updatePatientSchema = ( + PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject +) + .omit({ + id: true, + createdAt: true, + userId: true, + }) + .partial(); + +type UpdatePatient = z.infer; + export default function PaymentsPage() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [paymentPeriod, setPaymentPeriod] = useState("all-time"); + const [uploadedImage, setUploadedImage] = useState(null); + const [isExtracting, setIsExtracting] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [showReimbursementWindow, setShowReimbursementWindow] = useState(false); + const [extractedPaymentData, setExtractedPaymentData] = useState([]); + const [editableData, setEditableData] = useState([]); + const [paidItems, setPaidItems] = useState>({}); + const [adjustmentValues, setAdjustmentValues] = useState< + Record + >({}); + const [balanceValues, setBalanceValues] = useState>( + {} + ); const { toast } = useToast(); const { user } = useAuth(); // Fetch patients - const { data: patients = [], isLoading: isLoadingPatients } = useQuery({ + const { data: patients = [], isLoading: isLoadingPatients } = useQuery< + Patient[] + >({ queryKey: ["/api/patients"], enabled: !!user, }); - + // Fetch appointments - const { - data: appointments = [] as Appointment[], - isLoading: isLoadingAppointments + const { + data: appointments = [] as Appointment[], + isLoading: isLoadingAppointments, } = useQuery({ queryKey: ["/api/appointments"], enabled: !!user, @@ -105,39 +149,39 @@ export default function PaymentsPage() { { id: "PMT-1001", patientId: patients[0]?.id || 1, - amount: 75.00, + amount: 75.0, date: new Date(new Date().setDate(new Date().getDate() - 2)), method: "Credit Card", status: "completed", - description: "Co-pay for cleaning" + description: "Co-pay for cleaning", }, { id: "PMT-1002", patientId: patients[0]?.id || 1, - amount: 150.00, + amount: 150.0, date: new Date(new Date().setDate(new Date().getDate() - 7)), method: "Insurance", status: "processing", - description: "Insurance claim for x-rays" + description: "Insurance claim for x-rays", }, { id: "PMT-1003", patientId: patients[0]?.id || 1, - amount: 350.00, + amount: 350.0, date: new Date(new Date().setDate(new Date().getDate() - 14)), method: "Check", status: "completed", - description: "Payment for root canal" + description: "Payment for root canal", }, { id: "PMT-1004", patientId: patients[0]?.id || 1, - amount: 120.00, + amount: 120.0, date: new Date(new Date().setDate(new Date().getDate() - 30)), method: "Credit Card", status: "completed", - description: "Filling procedure" - } + description: "Filling procedure", + }, ]; // Sample outstanding balances @@ -145,37 +189,404 @@ export default function PaymentsPage() { { id: "INV-5001", patientId: patients[0]?.id || 1, - amount: 210.50, + amount: 210.5, dueDate: new Date(new Date().setDate(new Date().getDate() + 7)), description: "Crown procedure", created: new Date(new Date().setDate(new Date().getDate() - 10)), - status: "pending" + status: "pending", }, { id: "INV-5002", patientId: patients[0]?.id || 1, - amount: 85.00, + amount: 85.0, dueDate: new Date(new Date().setDate(new Date().getDate() - 5)), description: "Diagnostic & preventive", created: new Date(new Date().setDate(new Date().getDate() - 20)), - status: "overdue" - } + status: "overdue", + }, ]; // Calculate summary data - const totalOutstanding = sampleOutstanding.reduce((sum, item) => sum + item.amount, 0); + const totalOutstanding = sampleOutstanding.reduce( + (sum, item) => sum + item.amount, + 0 + ); const totalCollected = samplePayments - .filter(payment => payment.status === "completed") + .filter((payment) => payment.status === "completed") .reduce((sum, payment) => sum + payment.amount, 0); const pendingAmount = samplePayments - .filter(payment => payment.status === "processing") + .filter((payment) => payment.status === "processing") .reduce((sum, payment) => sum + payment.amount, 0); const handleRecordPayment = (patientId: number, invoiceId?: string) => { - const patient = patients.find(p => p.id === patientId); + const patient = patients.find((p) => p.id === patientId); toast({ title: "Payment form opened", - description: `Recording payment for ${patient?.firstName} ${patient?.lastName}${invoiceId ? ` (Invoice: ${invoiceId})` : ''}`, + description: `Recording payment for ${patient?.firstName} ${patient?.lastName}${invoiceId ? ` (Invoice: ${invoiceId})` : ""}`, + }); + }; + + // Image upload handlers for OCR + const handleImageDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const files = e.dataTransfer.files; + if (files && files[0]) { + const file = files[0]; + if (file.type.startsWith("image/")) { + setUploadedImage(file); + } else { + toast({ + title: "Invalid file type", + description: "Please upload an image file (JPG, PNG, etc.)", + variant: "destructive", + }); + } + } + }; + + const handleImageSelect = (e: React.ChangeEvent) => { + const files = e.target.files; + if (files && files[0]) { + const file = files[0]; + setUploadedImage(file); + } + }; + + const handleOCRExtraction = async (file: File) => { + setIsExtracting(true); + + try { + // Create FormData for image upload + const formData = new FormData(); + formData.append("image", file); + + // Simulate OCR extraction process + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Sample extracted payment data - 20 rows + const mockExtractedData = [ + { + memberName: "John Smith", + memberId: "123456789", + procedureCode: "D1110", + dateOfService: "12/15/2024", + billedAmount: 150.0, + allowedAmount: 120.0, + paidAmount: 96.0, + }, + { + memberName: "John Smith", + memberId: "123456789", + procedureCode: "D0120", + dateOfService: "12/15/2024", + billedAmount: 85.0, + allowedAmount: 70.0, + paidAmount: 56.0, + }, + { + memberName: "Mary Johnson", + memberId: "987654321", + procedureCode: "D2330", + dateOfService: "12/14/2024", + billedAmount: 280.0, + allowedAmount: 250.0, + paidAmount: 200.0, + }, + { + memberName: "Robert Brown", + memberId: "456789123", + procedureCode: "D3320", + dateOfService: "12/13/2024", + billedAmount: 1050.0, + allowedAmount: 900.0, + paidAmount: 720.0, + }, + { + memberName: "Sarah Davis", + memberId: "789123456", + procedureCode: "D2140", + dateOfService: "12/12/2024", + billedAmount: 320.0, + allowedAmount: 280.0, + paidAmount: 224.0, + }, + { + memberName: "Michael Wilson", + memberId: "321654987", + procedureCode: "D1120", + dateOfService: "12/11/2024", + billedAmount: 120.0, + allowedAmount: 100.0, + paidAmount: 80.0, + }, + { + memberName: "Jennifer Garcia", + memberId: "654987321", + procedureCode: "D0150", + dateOfService: "12/10/2024", + billedAmount: 195.0, + allowedAmount: 165.0, + paidAmount: 132.0, + }, + { + memberName: "David Miller", + memberId: "147258369", + procedureCode: "D2331", + dateOfService: "12/09/2024", + billedAmount: 220.0, + allowedAmount: 190.0, + paidAmount: 152.0, + }, + { + memberName: "Lisa Anderson", + memberId: "258369147", + procedureCode: "D4910", + dateOfService: "12/08/2024", + billedAmount: 185.0, + allowedAmount: 160.0, + paidAmount: 128.0, + }, + { + memberName: "James Taylor", + memberId: "369147258", + procedureCode: "D2392", + dateOfService: "12/07/2024", + billedAmount: 165.0, + allowedAmount: 140.0, + paidAmount: 112.0, + }, + { + memberName: "Patricia Thomas", + memberId: "741852963", + procedureCode: "D0140", + dateOfService: "12/06/2024", + billedAmount: 90.0, + allowedAmount: 75.0, + paidAmount: 60.0, + }, + { + memberName: "Christopher Lee", + memberId: "852963741", + procedureCode: "D2750", + dateOfService: "12/05/2024", + billedAmount: 1250.0, + allowedAmount: 1100.0, + paidAmount: 880.0, + }, + { + memberName: "Linda White", + memberId: "963741852", + procedureCode: "D1351", + dateOfService: "12/04/2024", + billedAmount: 75.0, + allowedAmount: 65.0, + paidAmount: 52.0, + }, + { + memberName: "Mark Harris", + memberId: "159753486", + procedureCode: "D7140", + dateOfService: "12/03/2024", + billedAmount: 185.0, + allowedAmount: 155.0, + paidAmount: 124.0, + }, + { + memberName: "Nancy Martin", + memberId: "486159753", + procedureCode: "D2332", + dateOfService: "12/02/2024", + billedAmount: 280.0, + allowedAmount: 240.0, + paidAmount: 192.0, + }, + { + memberName: "Kevin Thompson", + memberId: "753486159", + procedureCode: "D0210", + dateOfService: "12/01/2024", + billedAmount: 125.0, + allowedAmount: 105.0, + paidAmount: 84.0, + }, + { + memberName: "Helen Garcia", + memberId: "357951486", + procedureCode: "D4341", + dateOfService: "11/30/2024", + billedAmount: 210.0, + allowedAmount: 180.0, + paidAmount: 144.0, + }, + { + memberName: "Daniel Rodriguez", + memberId: "486357951", + procedureCode: "D2394", + dateOfService: "11/29/2024", + billedAmount: 295.0, + allowedAmount: 250.0, + paidAmount: 200.0, + }, + { + memberName: "Carol Lewis", + memberId: "951486357", + procedureCode: "D1206", + dateOfService: "11/28/2024", + billedAmount: 45.0, + allowedAmount: 40.0, + paidAmount: 32.0, + }, + { + memberName: "Paul Clark", + memberId: "246813579", + procedureCode: "D5110", + dateOfService: "11/27/2024", + billedAmount: 1200.0, + allowedAmount: 1050.0, + paidAmount: 840.0, + }, + { + memberName: "Susan Young", + memberId: "135792468", + procedureCode: "D4342", + dateOfService: "11/26/2024", + billedAmount: 175.0, + allowedAmount: 150.0, + paidAmount: 120.0, + }, + { + memberName: "Richard Allen", + memberId: "468135792", + procedureCode: "D2160", + dateOfService: "11/25/2024", + billedAmount: 385.0, + allowedAmount: 320.0, + paidAmount: 256.0, + }, + { + memberName: "Karen Scott", + memberId: "792468135", + procedureCode: "D0220", + dateOfService: "11/24/2024", + billedAmount: 95.0, + allowedAmount: 80.0, + paidAmount: 64.0, + }, + { + memberName: "William Green", + memberId: "135246879", + procedureCode: "D3220", + dateOfService: "11/23/2024", + billedAmount: 820.0, + allowedAmount: 700.0, + paidAmount: 560.0, + }, + { + memberName: "Betty King", + memberId: "579024681", + procedureCode: "D1208", + dateOfService: "11/22/2024", + billedAmount: 55.0, + allowedAmount: 45.0, + paidAmount: 36.0, + }, + { + memberName: "Edward Baker", + memberId: "246897531", + procedureCode: "D2950", + dateOfService: "11/21/2024", + billedAmount: 415.0, + allowedAmount: 350.0, + paidAmount: 280.0, + }, + { + memberName: "Dorothy Hall", + memberId: "681357924", + procedureCode: "D0230", + dateOfService: "11/20/2024", + billedAmount: 135.0, + allowedAmount: 115.0, + paidAmount: 92.0, + }, + { + memberName: "Joseph Adams", + memberId: "924681357", + procedureCode: "D2740", + dateOfService: "11/19/2024", + billedAmount: 1150.0, + allowedAmount: 980.0, + paidAmount: 784.0, + }, + { + memberName: "Sandra Nelson", + memberId: "357924681", + procedureCode: "D4211", + dateOfService: "11/18/2024", + billedAmount: 245.0, + allowedAmount: 210.0, + paidAmount: 168.0, + }, + { + memberName: "Kenneth Carter", + memberId: "681924357", + procedureCode: "D0274", + dateOfService: "11/17/2024", + billedAmount: 165.0, + allowedAmount: 140.0, + paidAmount: 112.0, + }, + ]; + + setExtractedPaymentData(mockExtractedData); + setEditableData([...mockExtractedData]); + setShowReimbursementWindow(true); + + toast({ + title: "OCR Extraction Complete", + description: "Payment information extracted from image successfully", + }); + } catch (error) { + toast({ + title: "OCR Extraction Failed", + description: "Could not extract information from the image", + variant: "destructive", + }); + } finally { + setIsExtracting(false); + } + }; + + const removeUploadedImage = () => { + setUploadedImage(null); + }; + + const updateEditableData = ( + index: number, + field: string, + value: string | number + ) => { + setEditableData((prev) => { + const updated = [...prev]; + updated[index] = { ...updated[index], [field]: value }; + return updated; + }); + }; + + const deleteRow = (index: number) => { + setEditableData((prev) => prev.filter((_, i) => i !== index)); + toast({ + title: "Row Deleted", + description: `Payment record has been removed`, + }); + }; + + const saveRow = (index: number) => { + toast({ + title: "Row Saved", + description: `Changes to payment record ${index + 1} have been saved`, }); }; @@ -190,20 +601,19 @@ export default function PaymentsPage() {
-
-
-
-

Payments

-

- Manage patient payments and outstanding balances -

-
+ {/* Header */} +
+
+

Payments

+

+ Manage patient payments and outstanding balances +

- +
- setPaymentPeriod(value)} > @@ -216,11 +626,6 @@ export default function PaymentsPage() { This Year - -
@@ -228,56 +633,193 @@ export default function PaymentsPage() {
- Outstanding Balance + + Outstanding Balance +
-
${totalOutstanding.toFixed(2)}
+
+ ${totalOutstanding.toFixed(2)} +

From {sampleOutstanding.length} outstanding invoices

- + - Payments Collected + + Payments Collected +
-
${totalCollected.toFixed(2)}
+
+ ${totalCollected.toFixed(2)} +

- From {samplePayments.filter(p => p.status === "completed").length} completed payments + From{" "} + { + samplePayments.filter((p) => p.status === "completed") + .length + }{" "} + completed payments

- + - Pending Payments + + Pending Payments +
-
${pendingAmount.toFixed(2)}
+
+ ${pendingAmount.toFixed(2)} +

- From {samplePayments.filter(p => p.status === "processing").length} pending transactions + From{" "} + { + samplePayments.filter((p) => p.status === "processing") + .length + }{" "} + pending transactions

+ {/* OCR Image Upload Section */} +
+
+

+ Payment Document OCR +

+
+ + + +
+
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onClick={() => + document.getElementById("image-upload-input")?.click() + } + > + {uploadedImage ? ( +
+
+ +
+

+ {uploadedImage.name} +

+

+ {(uploadedImage.size / 1024 / 1024).toFixed(2)} MB +

+
+ +
+ {isExtracting && ( +
+ Extracting payment information... +
+ )} +
+ ) : ( +
+ +
+

+ Upload Payment Document +

+

+ Drag and drop an image or click to browse +

+ +
+

+ Supported formats: JPG, PNG, GIF • Max size: 10MB +

+
+ )} + + +
+ +
+ +
+
+
+
+
+ {/* Outstanding Balances Section */}
-

Outstanding Balances

+

+ Outstanding Balances +

- + {sampleOutstanding.length > 0 ? ( @@ -285,54 +827,83 @@ export default function PaymentsPage() { Patient - Invoice - Description + Claimed procedure codes Amount - Due Date - Status + Paid + Adjustment + Balance Action {sampleOutstanding.map((invoice) => { - const patient = patients.find(p => p.id === invoice.patientId) || - { firstName: "Sample", lastName: "Patient" }; - + const patient = patients.find( + (p) => p.id === invoice.patientId + ) || { firstName: "Sample", lastName: "Patient" }; + return ( {patient.firstName} {patient.lastName} - {invoice.id} {invoice.description} ${invoice.amount.toFixed(2)} - {format(invoice.dueDate, 'MMM dd, yyyy')} - - {invoice.status === 'overdue' ? ( - <> - - Overdue - - ) : ( - <> - - Pending - - )} - + { + setPaidItems((prev) => ({ + ...prev, + [invoice.id]: !!checked, + })); + }} + className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600" + /> + + + { + const value = parseFloat(e.target.value) || 0; + setAdjustmentValues((prev) => ({ + ...prev, + [invoice.id]: value, + })); + }} + className="w-20 h-8" + /> + + + { + const value = parseFloat(e.target.value) || 0; + setBalanceValues((prev) => ({ + ...prev, + [invoice.id]: value, + })); + }} + className="w-20 h-8" + /> - @@ -343,14 +914,16 @@ export default function PaymentsPage() { ) : (
-

No outstanding balances

+

+ No outstanding balances +

All patient accounts are current

)}
- + {sampleOutstanding.length > 0 && (
@@ -364,96 +937,191 @@ export default function PaymentsPage() { )}
- - {/* Recent Payments Section */} -
-
-

Recent Payments

-
- - - - {samplePayments.length > 0 ? ( - - - - Patient - Payment ID - Description - Date - Amount - Method - Status - Action - - - - {samplePayments.map((payment) => { - const patient = patients.find(p => p.id === payment.patientId) || - { firstName: "Sample", lastName: "Patient" }; - - return ( - - - {patient.firstName} {patient.lastName} - - {payment.id} - {payment.description} - {format(payment.date, 'MMM dd, yyyy')} - ${payment.amount.toFixed(2)} - {payment.method} - - - {payment.status === 'completed' ? ( - <> - - Completed - - ) : ( - <> - - Processing - - )} - - - - - - - ); - })} - -
- ) : ( -
- -

No payment history

-

- Payments will appear here once processed -

-
- )} -
-
-
+ +
+ + {/* Reimbursement Data Popup */} + + + + + Insurance Reimbursement/Payment Details + + + +
+ + + + Member Name + Member ID + Procedure Code + Date of Service + Billed Amount + Allowed Amount + Paid Amount + Delete/Save + + + + {editableData.map((payment, index) => ( + + + + updateEditableData( + index, + "memberName", + e.target.value + ) + } + className="border-0 p-1 bg-transparent" + /> + + + + updateEditableData(index, "memberId", e.target.value) + } + className="border-0 p-1 bg-transparent" + /> + + + + updateEditableData( + index, + "procedureCode", + e.target.value + ) + } + className="border-0 p-1 bg-transparent" + /> + + + + updateEditableData( + index, + "dateOfService", + e.target.value + ) + } + className="border-0 p-1 bg-transparent" + /> + + + + updateEditableData( + index, + "billedAmount", + parseFloat(e.target.value) || 0 + ) + } + className="border-0 p-1 bg-transparent" + /> + + + + updateEditableData( + index, + "allowedAmount", + parseFloat(e.target.value) || 0 + ) + } + className="border-0 p-1 bg-transparent" + /> + + + + updateEditableData( + index, + "paidAmount", + parseFloat(e.target.value) || 0 + ) + } + className="border-0 p-1 bg-transparent text-green-600 font-medium" + /> + + +
+ + +
+
+
+ ))} +
+
+ + {extractedPaymentData.length === 0 && ( +
+ No payment data extracted +
+ )} +
+ + + + + +
+
); -} \ No newline at end of file +}