From 8d4aff12a1af74c43aaec3f48bd7ae30fc3d7da6 Mon Sep 17 00:00:00 2001 From: Potenz Date: Thu, 14 Aug 2025 23:12:00 +0530 Subject: [PATCH] ui and minor changes for payment page --- apps/Backend/src/app.ts | 34 +- apps/Backend/src/routes/claims.ts | 7 +- apps/Backend/src/routes/payments.ts | 39 +- .../components/claims/claims-recent-table.tsx | 2 +- .../payments/payment-edit-modal.tsx | 632 ++++++++++----- .../payments/payments-recent-table.tsx | 195 +++-- apps/Frontend/src/pages/payments-page.tsx | 750 +----------------- packages/db/types/payment-types.ts | 5 +- 8 files changed, 639 insertions(+), 1025 deletions(-) diff --git a/apps/Backend/src/app.ts b/apps/Backend/src/app.ts index ad7a72d..5d382ac 100644 --- a/apps/Backend/src/app.ts +++ b/apps/Backend/src/app.ts @@ -1,32 +1,32 @@ -import express from 'express'; +import express from "express"; import cors from "cors"; -import routes from './routes'; -import { errorHandler } from './middlewares/error.middleware'; -import { apiLogger } from './middlewares/logger.middleware'; -import authRoutes from './routes/auth' -import { authenticateJWT } from './middlewares/auth.middleware'; -import dotenv from 'dotenv'; +import routes from "./routes"; +import { errorHandler } from "./middlewares/error.middleware"; +import { apiLogger } from "./middlewares/logger.middleware"; +import authRoutes from "./routes/auth"; +import { authenticateJWT } from "./middlewares/auth.middleware"; +import dotenv from "dotenv"; dotenv.config(); const FRONTEND_URL = process.env.FRONTEND_URL; - const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // For form data app.use(apiLogger); -app.use(cors({ - origin: FRONTEND_URL, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], -credentials: true, -})); +app.use( + cors({ + origin: FRONTEND_URL, + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, + }) +); - -app.use('/api/auth', authRoutes); -app.use('/api', authenticateJWT, routes); +app.use("/api/auth", authRoutes); +app.use("/api", authenticateJWT, routes); app.use(errorHandler); diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 84cc242..d354b63 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -7,7 +7,7 @@ import { forwardToSeleniumClaimAgent } from "../services/seleniumClaimClient"; import path from "path"; import axios from "axios"; import { Prisma } from "@repo/db/generated/prisma"; -import { Decimal } from "@prisma/client/runtime/library"; +import { Decimal } from "decimal.js"; import { ExtendedClaimSchema, InputServiceLine, @@ -255,8 +255,9 @@ router.post("/", async (req: Request, res: Response): Promise => { (line: InputServiceLine) => ({ ...line, totalBilled: Number(line.totalBilled), - totalAdjusted: Number(line.totalAdjusted), - totalPaid: Number(line.totalPaid), + totalAdjusted: 0, + totalPaid: 0, + totalDue: Number(line.totalBilled), }) ); req.body.serviceLines = { create: req.body.serviceLines }; diff --git a/apps/Backend/src/routes/payments.ts b/apps/Backend/src/routes/payments.ts index 74bd43a..25885cd 100644 --- a/apps/Backend/src/routes/payments.ts +++ b/apps/Backend/src/routes/payments.ts @@ -11,6 +11,7 @@ import { } from "@repo/db/types"; import Decimal from "decimal.js"; import { prisma } from "@repo/db/client"; +import { PaymentStatusSchema } from "@repo/db/types"; const paymentFilterSchema = z.object({ from: z.string().datetime(), @@ -74,8 +75,8 @@ router.get( const parsedClaimId = parseIntOrError(req.params.claimId, "Claim ID"); const payments = await storage.getPaymentsByClaimId( - userId, - parsedClaimId + parsedClaimId, + userId ); if (!payments) return res.status(404).json({ message: "No payments found for claim" }); @@ -102,8 +103,8 @@ router.get( ); const payments = await storage.getPaymentsByPatientId( - userId, - parsedPatientId + parsedPatientId, + userId ); if (!payments) @@ -152,7 +153,7 @@ router.get("/:id", async (req: Request, res: Response): Promise => { const id = parseIntOrError(req.params.id, "Payment ID"); - const payment = await storage.getPaymentById(userId, id); + const payment = await storage.getPaymentById(id, userId); if (!payment) return res.status(404).json({ message: "Payment not found" }); res.status(200).json(payment); @@ -309,6 +310,34 @@ router.put("/:id", async (req: Request, res: Response): Promise => { } }); +// PATCH /api/payments/:id/status +router.patch( + "/:id/status", + async (req: Request, res: Response): Promise => { + try { + const userId = req.user?.id; + if (!userId) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const paymentId = parseIntOrError(req.params.id, "Payment ID"); + + const status = PaymentStatusSchema.parse(req.body.data.status); + + const updatedPayment = await prisma.payment.update({ + where: { id: paymentId }, + data: { status, updatedById: userId }, + }); + + res.json(updatedPayment); + } catch (err: unknown) { + const message = + err instanceof Error ? err.message : "Failed to update payment status"; + res.status(500).json({ message }); + } + } +); + // DELETE /api/payments/:id router.delete("/:id", async (req: Request, res: Response): Promise => { try { diff --git a/apps/Frontend/src/components/claims/claims-recent-table.tsx b/apps/Frontend/src/components/claims/claims-recent-table.tsx index 3c8271f..309d8dc 100644 --- a/apps/Frontend/src/components/claims/claims-recent-table.tsx +++ b/apps/Frontend/src/components/claims/claims-recent-table.tsx @@ -364,7 +364,7 @@ export default function ClaimsRecentTable({ )}
- CML-{claim.id!.toString().padStart(4, "0")} + CLM-{claim.id!.toString().padStart(4, "0")}
diff --git a/apps/Frontend/src/components/payments/payment-edit-modal.tsx b/apps/Frontend/src/components/payments/payment-edit-modal.tsx index efdf5aa..b38996f 100644 --- a/apps/Frontend/src/components/payments/payment-edit-modal.tsx +++ b/apps/Frontend/src/components/payments/payment-edit-modal.tsx @@ -29,12 +29,16 @@ import { } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { toast } from "@/hooks/use-toast"; +import { X } from "lucide-react"; type PaymentEditModalProps = { isOpen: boolean; onOpenChange: (open: boolean) => void; onClose: () => void; onEditServiceLine: (payload: NewTransactionPayload) => void; + isUpdatingServiceLine?: boolean; + onUpdateStatus: (paymentId: number, status: PaymentStatus) => void; + isUpdatingStatus?: boolean; payment: PaymentWithExtras | null; }; @@ -44,9 +48,12 @@ export default function PaymentEditModal({ onClose, payment, onEditServiceLine, + isUpdatingServiceLine, + onUpdateStatus, + isUpdatingStatus, }: PaymentEditModalProps) { if (!payment) return null; - + const [expandedLineId, setExpandedLineId] = useState(null); const [paymentStatus, setPaymentStatus] = React.useState( payment.status @@ -99,7 +106,56 @@ export default function PaymentEditModal({ const handleSavePayment = async () => { if (!formState.serviceLineId) { - toast({ title: "Error", description: "No service line selected." }); + toast({ + title: "Error", + description: "No service line selected.", + variant: "destructive", + }); + return; + } + const paidAmount = Number(formState.paidAmount) || 0; + const adjustedAmount = Number(formState.adjustedAmount) || 0; + + if (paidAmount < 0 || adjustedAmount < 0) { + toast({ + title: "Invalid Amount", + description: "Amounts cannot be negative.", + variant: "destructive", + }); + return; + } + + if (paidAmount === 0 && adjustedAmount === 0) { + toast({ + title: "Invalid Amount", + description: + "Either paid or adjusted amount must be greater than zero.", + variant: "destructive", + }); + return; + } + + const line = payment.claim.serviceLines.find( + (sl) => sl.id === formState.serviceLineId + ); + if (!line) { + toast({ + title: "Error", + description: "Selected service line not found.", + variant: "destructive", + }); + return; + } + + const dueAmount = Number(line.totalDue); + if (paidAmount > dueAmount) { + toast({ + title: "Invalid Payment", + description: `Paid amount ($${paidAmount.toFixed( + 2 + )}) cannot exceed due amount ($${dueAmount.toFixed(2)}).`, + variant: "destructive", + }); return; } @@ -114,70 +170,148 @@ export default function PaymentEditModal({ adjustedAmount: Number(formState.adjustedAmount) || 0, method: formState.method, receivedDate: parseLocalDate(formState.receivedDate), - payerName: formState.payerName || undefined, - notes: formState.notes || undefined, + payerName: formState.payerName?.trim() || undefined, + notes: formState.notes?.trim() || undefined, }, ], }; try { await onEditServiceLine(payload); + toast({ + title: "Success", + description: "Payment Transaction added successfully.", + }); setExpandedLineId(null); - onClose(); } catch (err) { console.error(err); toast({ title: "Error", description: "Failed to save payment." }); } }; + const handlePayFullDue = async ( + line: (typeof payment.claim.serviceLines)[0] + ) => { + if (!line || !payment) { + toast({ + title: "Error", + description: "Service line or payment data missing.", + variant: "destructive", + }); + return; + } + + const dueAmount = Number(line.totalDue); + if (isNaN(dueAmount) || dueAmount <= 0) { + toast({ + title: "No Due", + description: "This service line has no outstanding balance.", + variant: "destructive", + }); + return; + } + + const payload: NewTransactionPayload = { + paymentId: payment.id, + status: paymentStatus, + serviceLineTransactions: [ + { + serviceLineId: line.id, + paidAmount: dueAmount, + adjustedAmount: 0, + method: paymentMethodOptions[1] as PaymentMethod, // Maybe make dynamic later + receivedDate: new Date(), + }, + ], + }; + + try { + await onEditServiceLine(payload); + toast({ + title: "Success", + description: `Full due amount ($${dueAmount.toFixed( + 2 + )}) paid for ${line.procedureCode}`, + }); + } catch (err) { + console.error(err); + toast({ + title: "Error", + description: "Failed to update payment.", + variant: "destructive", + }); + } + }; + return ( - - Edit Payment - - View and manage payments applied to service lines. - - +
+ + Edit Payment + + View and manage payments applied to service lines. + + -
+ {/* Close button in top-right */} + +
+ +
{/* Claim + Patient Info */} -
-

+
+

{payment.claim.patientName}

-

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

-

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

-

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

+
+ + Claim #{payment.claimId.toString().padStart(4, "0")} + + + Service Date:{" "} + {formatDateToHumanReadable(payment.claim.serviceDate)} + +
- {/* Payment Summary */} -
+ {/* Payment Summary + Metadata */} +
+ {/* Payment Info */}
-

Payment Info

-
+

Payment Info

+

- Total Billed: $ - {Number(payment.totalBilled || 0).toFixed(2)} + Total Billed:{" "} + + ${Number(payment.totalBilled || 0).toFixed(2)} +

- Total Paid: $ - {Number(payment.totalPaid || 0).toFixed(2)} + Total Paid:{" "} + + ${Number(payment.totalPaid || 0).toFixed(2)} +

- Total Due: $ - {Number(payment.totalDue || 0).toFixed(2)} + Total Due:{" "} + + ${Number(payment.totalDue || 0).toFixed(2)} +

-
- + + {/* Status Selector */} +
+
+ +
+ {/* Metadata */}
-

Metadata

-
+

Metadata

+

Created At:{" "} {payment.createdAt @@ -209,7 +354,7 @@ export default function PaymentEditModal({ : "N/A"}

- Last Upadated At:{" "} + Last Updated At:{" "} {payment.updatedAt ? formatDateToHumanReadable(payment.updatedAt) : "N/A"} @@ -221,176 +366,186 @@ export default function PaymentEditModal({ {/* Service Lines Payments */}

Service Lines

-
+
{payment.claim.serviceLines.length > 0 ? ( - <> - {payment.claim.serviceLines.map((line) => { - return ( -
+ payment.claim.serviceLines.map((line) => { + const isExpanded = expandedLineId === line.id; + + return ( +
+ {/* Top Info */} +

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

- Billed: $ - {Number(line.totalBilled || 0).toFixed(2)} + Billed:{" "} + + ${Number(line.totalBilled || 0).toFixed(2)} +

- Paid: $ - {Number(line.totalPaid || 0).toFixed(2)} + Paid:{" "} + + ${Number(line.totalPaid || 0).toFixed(2)} +

- Adjusted: $ - {Number(line.totalAdjusted || 0).toFixed(2)} + Adjusted:{" "} + + ${Number(line.totalAdjusted || 0).toFixed(2)} +

- Due: $ - {Number(line.totalDue || 0).toFixed(2)} + Due:{" "} + + ${Number(line.totalDue || 0).toFixed(2)} +

+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Expanded Partial Payment Form */} + {isExpanded && ( +
+
+ + + updateField( + "paidAmount", + parseFloat(e.target.value) + ) + } + /> +
+ +
+ + + updateField( + "adjustedAmount", + parseFloat(e.target.value) + ) + } + /> +
+ +
+ + +
+ +
+ + + updateField("receivedDate", e.target.value) + } + /> +
+ +
+ + + updateField("payerName", e.target.value) + } + /> +
+ +
+ + + updateField("notes", e.target.value) + } + /> +
-
- - {expandedLineId === line.id && ( -
-
- - - updateField( - "paidAmount", - parseFloat(e.target.value) - ) - } - /> -
-
- - - updateField( - "adjustedAmount", - parseFloat(e.target.value) - ) - } - /> -
- -
- - -
- -
- - - updateField("receivedDate", e.target.value) - } - /> -
- -
- - - updateField("payerName", e.target.value) - } - /> -
- -
- - - updateField("notes", e.target.value) - } - /> -
- -
- )} -
- ); - })} - + )} +
+ ); + }) ) : (

No service lines available.

)} @@ -400,39 +555,82 @@ export default function PaymentEditModal({ {/* Transactions Overview */}

All Transactions

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

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

-

- Paid Amount: $ - {Number(tx.paidAmount).toFixed(2)} -

-

- Adjusted Amount: $ - {Number(tx.adjustedAmount).toFixed(2)} -

-

- Method: {tx.method} -

- {tx.payerName && ( +
+ {/* Transaction ID */} + {tx.id && ( +

+ Transaction ID:{" "} + {tx.id} +

+ )} + + {/* Procedure Code */} + {tx.serviceLine?.procedureCode && ( +

+ Procedure Code:{" "} + + {tx.serviceLine.procedureCode} + +

+ )} + + {/* Paid Amount */}

- Payer Name:{" "} - {tx.payerName} + Paid Amount:{" "} + + ${Number(tx.paidAmount).toFixed(2)} +

- )} - {tx.notes && ( + + {/* Adjusted Amount */} + {Number(tx.adjustedAmount) > 0 && ( +

+ + Adjusted Amount: + {" "} + + ${Number(tx.adjustedAmount).toFixed(2)} + +

+ )} + + {/* Date */}

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

- )} + + {/* Method */} +

+ Method:{" "} + {tx.method} +

+ + {/* Payer Name */} + {tx.payerName && tx.payerName.trim() !== "" && ( +

+ Payer Name:{" "} + {tx.payerName} +

+ )} + + {/* Notes */} + {tx.notes && tx.notes.trim() !== "" && ( +

+ Notes:{" "} + {tx.notes} +

+ )} +
)) ) : ( @@ -443,7 +641,7 @@ export default function PaymentEditModal({ {/* Actions */}
-
diff --git a/apps/Frontend/src/components/payments/payments-recent-table.tsx b/apps/Frontend/src/components/payments/payments-recent-table.tsx index a8a3825..319f415 100644 --- a/apps/Frontend/src/components/payments/payments-recent-table.tsx +++ b/apps/Frontend/src/components/payments/payments-recent-table.tsx @@ -16,6 +16,9 @@ import { Clock, CheckCircle, AlertCircle, + TrendingUp, + ThumbsDown, + DollarSign, } from "lucide-react"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; @@ -30,15 +33,15 @@ import { } from "@/components/ui/pagination"; import { Checkbox } from "@/components/ui/checkbox"; import { DeleteConfirmationDialog } from "../ui/deleteDialog"; -import PaymentViewModal from "./payment-view-modal"; import LoadingScreen from "../ui/LoadingScreen"; import { ClaimStatus, - ClaimWithServiceLines, NewTransactionPayload, + PaymentStatus, PaymentWithExtras, } from "@repo/db/types"; import EditPaymentModal from "./payment-edit-modal"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; interface PaymentApiResponse { payments: PaymentWithExtras[]; @@ -47,7 +50,6 @@ interface PaymentApiResponse { interface PaymentsRecentTableProps { allowEdit?: boolean; - allowView?: boolean; allowDelete?: boolean; allowCheckbox?: boolean; onSelectPayment?: (payment: PaymentWithExtras | null) => void; @@ -57,7 +59,6 @@ interface PaymentsRecentTableProps { export default function PaymentsRecentTable({ allowEdit, - allowView, allowDelete, allowCheckbox, onSelectPayment, @@ -66,7 +67,6 @@ export default function PaymentsRecentTable({ }: PaymentsRecentTableProps) { const { toast } = useToast(); - const [isViewPaymentOpen, setIsViewPaymentOpen] = useState(false); const [isEditPaymentOpen, setIsEditPaymentOpen] = useState(false); const [isDeletePaymentOpen, setIsDeletePaymentOpen] = useState(false); @@ -131,17 +131,25 @@ export default function PaymentsRecentTable({ } return response.json(); }, - onSuccess: () => { - setIsEditPaymentOpen(false); + onSuccess: async (updated, { paymentId }) => { toast({ title: "Success", description: "Payment updated successfully!", - variant: "default", }); + queryClient.invalidateQueries({ queryKey: getPaymentsQueryKey(), }); + + // Fetch updated payment and set into local state + const refreshedPayment = await apiRequest( + "GET", + `/api/payments/${paymentId}` + ).then((res) => res.json()); + + setCurrentPayment(refreshedPayment); // <-- keep modal in sync }, + onError: (error) => { toast({ title: "Error", @@ -151,6 +159,57 @@ export default function PaymentsRecentTable({ }, }); + const updatePaymentStatusMutation = useMutation({ + mutationFn: async ({ + paymentId, + status, + }: { + paymentId: number; + status: PaymentStatus; + }) => { + const response = await apiRequest( + "PATCH", + `/api/payments/${paymentId}/status`, + { + data: { status }, + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Failed to update payment status"); + } + + return response.json(); + }, + + onSuccess: async (updated, { paymentId }) => { + toast({ + title: "Success", + description: "Payment Status updated successfully!", + }); + + queryClient.invalidateQueries({ + queryKey: getPaymentsQueryKey(), + }); + + // Fetch updated payment and set into local state + const refreshedPayment = await apiRequest( + "GET", + `/api/payments/${paymentId}` + ).then((res) => res.json()); + + setCurrentPayment(refreshedPayment); // <-- keep modal in sync + }, + onError: (error) => { + toast({ + title: "Error", + description: `Status update failed: ${error.message}`, + variant: "destructive", + }); + }, + }); + const deletePaymentMutation = useMutation({ mutationFn: async (id: number) => { const res = await apiRequest("DELETE", `/api/payments/${id}`); @@ -182,11 +241,6 @@ export default function PaymentsRecentTable({ setIsEditPaymentOpen(true); }; - const handleViewPayment = (payment: PaymentWithExtras) => { - setCurrentPayment(payment); - setIsViewPaymentOpen(true); - }; - const handleDeletePayment = (payment: PaymentWithExtras) => { setCurrentPayment(payment); setIsDeletePaymentOpen(true); @@ -231,7 +285,7 @@ export default function PaymentsRecentTable({ paymentsData?.totalCount || 0 ); - const getInitialsFromName = (fullName: string) => { + const getInitials = (fullName: string) => { const parts = fullName.trim().split(/\s+/); const filteredParts = parts.filter((part) => part.length > 0); if (filteredParts.length === 0) { @@ -260,7 +314,7 @@ export default function PaymentsRecentTable({ return colorClasses[id % colorClasses.length]; }; - const getStatusInfo = (status?: ClaimStatus) => { + const getStatusInfo = (status?: PaymentStatus) => { switch (status) { case "PENDING": return { @@ -268,22 +322,35 @@ export default function PaymentsRecentTable({ color: "bg-yellow-100 text-yellow-800", icon: , }; - case "APPROVED": + case "PARTIALLY_PAID": return { - label: "Approved", + label: "Partially Paid", + color: "bg-blue-100 text-blue-800", + icon: , + }; + case "PAID": + return { + label: "Paid", color: "bg-green-100 text-green-800", icon: , }; - case "CANCELLED": + case "OVERPAID": return { - label: "Cancelled", + label: "Overpaid", + color: "bg-purple-100 text-purple-800", + icon: , + }; + case "DENIED": + return { + label: "Denied", color: "bg-red-100 text-red-800", - icon: , + icon: , }; default: return { label: status - ? status.charAt(0).toUpperCase() + status.slice(1) + ? (status as string).charAt(0).toUpperCase() + + (status as string).slice(1).toLowerCase() : "Unknown", color: "bg-gray-100 text-gray-800", icon: , @@ -318,10 +385,11 @@ export default function PaymentsRecentTable({ {allowCheckbox && Select} Payment ID + Claim ID Patient Name Amount - Date - Method + Claim Submitted on + Status Actions @@ -374,12 +442,39 @@ export default function PaymentsRecentTable({ ? `PAY-${payment.id.toString().padStart(4, "0")}` : "N/A"} - {payment.patientName} + + + {typeof payment.claimId === "number" + ? `CLM-${payment.claimId.toString().padStart(4, "0")}` + : "N/A"} + + + +
+ + + {getInitials(payment.patientName)} + + + +
+
+ {payment.patientName} +
+
+ PID-{payment.patientId?.toString().padStart(4, "0")} +
+
+
+
+ {/* 💰 Billed / Paid / Due breakdown */}
- Total Billed: $ + Total Billed: $ {Number(totalBilled).toFixed(2)} @@ -400,7 +495,27 @@ export default function PaymentsRecentTable({ {formatDateToHumanReadable(payment.paymentDate)} - {payment.paymentMethod} + + +
+ {(() => { + const { label, color, icon } = getStatusInfo( + payment.status + ); + return ( + + + {icon} + {label} + + + ); + })()} +
+
+
{allowDelete && ( @@ -428,18 +543,6 @@ export default function PaymentsRecentTable({ )} - {allowView && ( - - )}
@@ -457,17 +560,6 @@ export default function PaymentsRecentTable({ entityName={`ClaimID : ${currentPayment?.claimId}`} /> - {/* /will hanlde both modal later */} - {/* {isViewPaymentOpen && currentPayment && ( - setIsViewPaymentOpen(false)} - onOpenChange={(open) => setIsViewPaymentOpen(open)} - onEditClaim={(payment) => handleEditPayment(payment)} - payment={currentPayment} - /> - )} */} - {isEditPaymentOpen && currentPayment && ( { updatePaymentMutation.mutate(updatedPayment); }} + isUpdatingServiceLine={updatePaymentMutation.isPending} + onUpdateStatus={(paymentId, status) => { + updatePaymentStatusMutation.mutate({ paymentId, status }); + }} + isUpdatingStatus={updatePaymentStatusMutation.isPending} /> )} diff --git a/apps/Frontend/src/pages/payments-page.tsx b/apps/Frontend/src/pages/payments-page.tsx index 6c79881..ef5aaa9 100644 --- a/apps/Frontend/src/pages/payments-page.tsx +++ b/apps/Frontend/src/pages/payments-page.tsx @@ -8,6 +8,7 @@ import { CardTitle, CardContent, CardFooter, + CardDescription, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; @@ -47,7 +48,6 @@ import { DialogFooter, } from "@/components/ui/dialog"; import PaymentsRecentTable from "@/components/payments/payments-recent-table"; -import { Appointment, Patient } from "@repo/db/types"; export default function PaymentsPage() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); @@ -55,122 +55,15 @@ export default function PaymentsPage() { 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< - Patient[] - >({ - queryKey: ["/api/patients"], - enabled: !!user, - }); - - // Fetch appointments - const { - data: appointments = [] as Appointment[], - isLoading: isLoadingAppointments, - } = useQuery({ - queryKey: ["/api/appointments"], - enabled: !!user, - }); const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen); }; - // Sample payment data - const samplePayments = [ - { - id: "PMT-1001", - patientId: patients[0]?.id || 1, - amount: 75.0, - date: new Date(new Date().setDate(new Date().getDate() - 2)), - method: "Credit Card", - status: "completed", - description: "Co-pay for cleaning", - }, - { - id: "PMT-1002", - patientId: patients[0]?.id || 1, - amount: 150.0, - date: new Date(new Date().setDate(new Date().getDate() - 7)), - method: "Insurance", - status: "processing", - description: "Insurance claim for x-rays", - }, - { - id: "PMT-1003", - patientId: patients[0]?.id || 1, - amount: 350.0, - date: new Date(new Date().setDate(new Date().getDate() - 14)), - method: "Check", - status: "completed", - description: "Payment for root canal", - }, - { - id: "PMT-1004", - patientId: patients[0]?.id || 1, - amount: 120.0, - date: new Date(new Date().setDate(new Date().getDate() - 30)), - method: "Credit Card", - status: "completed", - description: "Filling procedure", - }, - ]; - - // Sample outstanding balances - const sampleOutstanding = [ - { - id: "INV-5001", - patientId: patients[0]?.id || 1, - 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", - }, - { - id: "INV-5002", - patientId: patients[0]?.id || 1, - 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", - }, - ]; - - // Calculate summary data - const totalOutstanding = sampleOutstanding.reduce( - (sum, item) => sum + item.amount, - 0 - ); - const totalCollected = samplePayments - .filter((payment) => payment.status === "completed") - .reduce((sum, payment) => sum + payment.amount, 0); - const pendingAmount = samplePayments - .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); - toast({ - title: "Payment form opened", - description: `Recording payment for ${patient?.firstName} ${patient?.lastName}${invoiceId ? ` (Invoice: ${invoiceId})` : ""}`, - }); - }; - // Image upload handlers for OCR const handleImageDrop = (e: React.DragEvent) => { e.preventDefault(); @@ -210,295 +103,6 @@ export default function PaymentsPage() { // 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); } @@ -585,12 +189,10 @@ export default function PaymentsPage() {
-
- ${totalOutstanding.toFixed(2)} -
+
$0

- From {sampleOutstanding.length} outstanding invoices + From 0 outstanding invoices

@@ -604,17 +206,10 @@ export default function PaymentsPage() {
-
- ${totalCollected.toFixed(2)} -
+
${0}

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

@@ -628,23 +223,16 @@ export default function PaymentsPage() {
-
- ${pendingAmount.toFixed(2)} -
+
$0

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

- {/* OCR Image Upload Section */} + {/* OCR Image Upload Section - not working rn*/}

@@ -757,316 +345,20 @@ export default function PaymentsPage() {

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

- Outstanding Balances -

-
- - - - {sampleOutstanding.length > 0 ? ( - - - - Patient - Claimed procedure codes - Amount - Paid - Adjustment - Balance - Action - - - - {sampleOutstanding.map((invoice) => { - const patient = patients.find( - (p) => p.id === invoice.patientId - ) || { firstName: "Sample", lastName: "Patient" }; - - return ( - - - {patient.firstName} {patient.lastName} - - {invoice.description} - ${invoice.amount.toFixed(2)} - - { - 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" - /> - - - - - - ); - })} - -
- ) : ( -
- -

- No outstanding balances -

-

- All patient accounts are current -

-
- )} -
- - {sampleOutstanding.length > 0 && ( - -
- - Download statement -
-
- Total: ${totalOutstanding.toFixed(2)} -
-
- )} -
-
- - + {/* Recent Payments table */} + + + Payment's Records + + View and manage all recents patient's claims payments + + + + + +
- - {/* 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 -
- )} -
- - - - - -
-
); } diff --git a/packages/db/types/payment-types.ts b/packages/db/types/payment-types.ts index 0e5f6b6..f038e4f 100644 --- a/packages/db/types/payment-types.ts +++ b/packages/db/types/payment-types.ts @@ -1,15 +1,11 @@ import { PaymentUncheckedCreateInputObjectSchema, - ClaimUncheckedCreateInputObjectSchema, ServiceLineTransactionCreateInputObjectSchema, - 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 ========== @@ -30,6 +26,7 @@ export type ServiceLineTransactionRecord = }>; // Enum for payment +export { PaymentStatusSchema }; export type PaymentStatus = z.infer; export type PaymentMethod = z.infer;