ui and minor changes for payment page
This commit is contained in:
@@ -1,32 +1,32 @@
|
|||||||
import express from 'express';
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import routes from './routes';
|
import routes from "./routes";
|
||||||
import { errorHandler } from './middlewares/error.middleware';
|
import { errorHandler } from "./middlewares/error.middleware";
|
||||||
import { apiLogger } from './middlewares/logger.middleware';
|
import { apiLogger } from "./middlewares/logger.middleware";
|
||||||
import authRoutes from './routes/auth'
|
import authRoutes from "./routes/auth";
|
||||||
import { authenticateJWT } from './middlewares/auth.middleware';
|
import { authenticateJWT } from "./middlewares/auth.middleware";
|
||||||
import dotenv from 'dotenv';
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const FRONTEND_URL = process.env.FRONTEND_URL;
|
const FRONTEND_URL = process.env.FRONTEND_URL;
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true })); // For form data
|
app.use(express.urlencoded({ extended: true })); // For form data
|
||||||
app.use(apiLogger);
|
app.use(apiLogger);
|
||||||
|
|
||||||
app.use(cors({
|
app.use(
|
||||||
origin: FRONTEND_URL,
|
cors({
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
origin: FRONTEND_URL,
|
||||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||||
credentials: true,
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
}));
|
credentials: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use("/api/auth", authRoutes);
|
||||||
app.use('/api/auth', authRoutes);
|
app.use("/api", authenticateJWT, routes);
|
||||||
app.use('/api', authenticateJWT, routes);
|
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { forwardToSeleniumClaimAgent } from "../services/seleniumClaimClient";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Prisma } from "@repo/db/generated/prisma";
|
import { Prisma } from "@repo/db/generated/prisma";
|
||||||
import { Decimal } from "@prisma/client/runtime/library";
|
import { Decimal } from "decimal.js";
|
||||||
import {
|
import {
|
||||||
ExtendedClaimSchema,
|
ExtendedClaimSchema,
|
||||||
InputServiceLine,
|
InputServiceLine,
|
||||||
@@ -255,8 +255,9 @@ router.post("/", async (req: Request, res: Response): Promise<any> => {
|
|||||||
(line: InputServiceLine) => ({
|
(line: InputServiceLine) => ({
|
||||||
...line,
|
...line,
|
||||||
totalBilled: Number(line.totalBilled),
|
totalBilled: Number(line.totalBilled),
|
||||||
totalAdjusted: Number(line.totalAdjusted),
|
totalAdjusted: 0,
|
||||||
totalPaid: Number(line.totalPaid),
|
totalPaid: 0,
|
||||||
|
totalDue: Number(line.totalBilled),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
req.body.serviceLines = { create: req.body.serviceLines };
|
req.body.serviceLines = { create: req.body.serviceLines };
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@repo/db/types";
|
} from "@repo/db/types";
|
||||||
import Decimal from "decimal.js";
|
import Decimal from "decimal.js";
|
||||||
import { prisma } from "@repo/db/client";
|
import { prisma } from "@repo/db/client";
|
||||||
|
import { PaymentStatusSchema } from "@repo/db/types";
|
||||||
|
|
||||||
const paymentFilterSchema = z.object({
|
const paymentFilterSchema = z.object({
|
||||||
from: z.string().datetime(),
|
from: z.string().datetime(),
|
||||||
@@ -74,8 +75,8 @@ router.get(
|
|||||||
const parsedClaimId = parseIntOrError(req.params.claimId, "Claim ID");
|
const parsedClaimId = parseIntOrError(req.params.claimId, "Claim ID");
|
||||||
|
|
||||||
const payments = await storage.getPaymentsByClaimId(
|
const payments = await storage.getPaymentsByClaimId(
|
||||||
userId,
|
parsedClaimId,
|
||||||
parsedClaimId
|
userId
|
||||||
);
|
);
|
||||||
if (!payments)
|
if (!payments)
|
||||||
return res.status(404).json({ message: "No payments found for claim" });
|
return res.status(404).json({ message: "No payments found for claim" });
|
||||||
@@ -102,8 +103,8 @@ router.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const payments = await storage.getPaymentsByPatientId(
|
const payments = await storage.getPaymentsByPatientId(
|
||||||
userId,
|
parsedPatientId,
|
||||||
parsedPatientId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!payments)
|
if (!payments)
|
||||||
@@ -152,7 +153,7 @@ router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
|
|
||||||
const id = parseIntOrError(req.params.id, "Payment ID");
|
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" });
|
if (!payment) return res.status(404).json({ message: "Payment not found" });
|
||||||
|
|
||||||
res.status(200).json(payment);
|
res.status(200).json(payment);
|
||||||
@@ -309,6 +310,34 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PATCH /api/payments/:id/status
|
||||||
|
router.patch(
|
||||||
|
"/:id/status",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
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
|
// DELETE /api/payments/:id
|
||||||
router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
|
router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ export default function ClaimsRecentTable({
|
|||||||
)}
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
CML-{claim.id!.toString().padStart(4, "0")}
|
CLM-{claim.id!.toString().padStart(4, "0")}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -29,12 +29,16 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
type PaymentEditModalProps = {
|
type PaymentEditModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onEditServiceLine: (payload: NewTransactionPayload) => void;
|
onEditServiceLine: (payload: NewTransactionPayload) => void;
|
||||||
|
isUpdatingServiceLine?: boolean;
|
||||||
|
onUpdateStatus: (paymentId: number, status: PaymentStatus) => void;
|
||||||
|
isUpdatingStatus?: boolean;
|
||||||
payment: PaymentWithExtras | null;
|
payment: PaymentWithExtras | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +48,9 @@ export default function PaymentEditModal({
|
|||||||
onClose,
|
onClose,
|
||||||
payment,
|
payment,
|
||||||
onEditServiceLine,
|
onEditServiceLine,
|
||||||
|
isUpdatingServiceLine,
|
||||||
|
onUpdateStatus,
|
||||||
|
isUpdatingStatus,
|
||||||
}: PaymentEditModalProps) {
|
}: PaymentEditModalProps) {
|
||||||
if (!payment) return null;
|
if (!payment) return null;
|
||||||
|
|
||||||
@@ -99,7 +106,56 @@ export default function PaymentEditModal({
|
|||||||
|
|
||||||
const handleSavePayment = async () => {
|
const handleSavePayment = async () => {
|
||||||
if (!formState.serviceLineId) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,70 +170,148 @@ export default function PaymentEditModal({
|
|||||||
adjustedAmount: Number(formState.adjustedAmount) || 0,
|
adjustedAmount: Number(formState.adjustedAmount) || 0,
|
||||||
method: formState.method,
|
method: formState.method,
|
||||||
receivedDate: parseLocalDate(formState.receivedDate),
|
receivedDate: parseLocalDate(formState.receivedDate),
|
||||||
payerName: formState.payerName || undefined,
|
payerName: formState.payerName?.trim() || undefined,
|
||||||
notes: formState.notes || undefined,
|
notes: formState.notes?.trim() || undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onEditServiceLine(payload);
|
await onEditServiceLine(payload);
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Payment Transaction added successfully.",
|
||||||
|
});
|
||||||
setExpandedLineId(null);
|
setExpandedLineId(null);
|
||||||
onClose();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast({ title: "Error", description: "Failed to save payment." });
|
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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<div className="relative">
|
||||||
<DialogTitle>Edit Payment</DialogTitle>
|
<DialogHeader>
|
||||||
<DialogDescription>
|
<DialogTitle>Edit Payment</DialogTitle>
|
||||||
View and manage payments applied to service lines.
|
<DialogDescription>
|
||||||
</DialogDescription>
|
View and manage payments applied to service lines.
|
||||||
</DialogHeader>
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* Close button in top-right */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute right-0 top-0"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
{/* Claim + Patient Info */}
|
{/* Claim + Patient Info */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-2 border-b border-gray-200 pb-4">
|
||||||
<h3 className="text-xl font-semibold">
|
<h3 className="text-2xl font-bold text-gray-900">
|
||||||
{payment.claim.patientName}
|
{payment.claim.patientName}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-500">
|
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||||
Claim ID: {payment.claimId.toString().padStart(4, "0")}
|
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
||||||
</p>
|
Claim #{payment.claimId.toString().padStart(4, "0")}
|
||||||
<p className="text-gray-500">
|
</span>
|
||||||
Service Date:{" "}
|
<span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded-full font-medium">
|
||||||
{formatDateToHumanReadable(payment.claim.serviceDate)}
|
Service Date:{" "}
|
||||||
</p>
|
{formatDateToHumanReadable(payment.claim.serviceDate)}
|
||||||
<p>
|
</span>
|
||||||
<span className="text-gray-500">Notes:</span>{" "}
|
</div>
|
||||||
{payment.notes || "N/A"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Payment Summary */}
|
{/* Payment Summary + Metadata */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* Payment Info */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900">Payment Info</h4>
|
<h4 className="font-semibold text-gray-900">Payment Info</h4>
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1 text-sm">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Total Billed:</span> $
|
<span className="text-gray-500">Total Billed:</span>{" "}
|
||||||
{Number(payment.totalBilled || 0).toFixed(2)}
|
<span className="font-medium">
|
||||||
|
${Number(payment.totalBilled || 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Total Paid:</span> $
|
<span className="text-gray-500">Total Paid:</span>{" "}
|
||||||
{Number(payment.totalPaid || 0).toFixed(2)}
|
<span className="font-medium text-green-600">
|
||||||
|
${Number(payment.totalPaid || 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Total Due:</span> $
|
<span className="text-gray-500">Total Due:</span>{" "}
|
||||||
{Number(payment.totalDue || 0).toFixed(2)}
|
<span className="font-medium text-red-600">
|
||||||
|
${Number(payment.totalDue || 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="pt-2">
|
|
||||||
<label className="text-sm text-gray-600">Status</label>
|
{/* Status Selector */}
|
||||||
|
<div className="pt-3">
|
||||||
|
<label className="block text-sm text-gray-600 mb-1">
|
||||||
|
Payment Status
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={paymentStatus}
|
value={paymentStatus}
|
||||||
onValueChange={(value: PaymentStatus) =>
|
onValueChange={(value: PaymentStatus) =>
|
||||||
@@ -196,12 +330,23 @@ export default function PaymentEditModal({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled={isUpdatingStatus}
|
||||||
|
onClick={() =>
|
||||||
|
payment && onUpdateStatus(payment.id, paymentStatus)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isUpdatingStatus ? "Updating..." : "Update Status"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900">Metadata</h4>
|
<h4 className="font-semibold text-gray-900">Metadata</h4>
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1 text-sm">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Created At:</span>{" "}
|
<span className="text-gray-500">Created At:</span>{" "}
|
||||||
{payment.createdAt
|
{payment.createdAt
|
||||||
@@ -209,7 +354,7 @@ export default function PaymentEditModal({
|
|||||||
: "N/A"}
|
: "N/A"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Last Upadated At:</span>{" "}
|
<span className="text-gray-500">Last Updated At:</span>{" "}
|
||||||
{payment.updatedAt
|
{payment.updatedAt
|
||||||
? formatDateToHumanReadable(payment.updatedAt)
|
? formatDateToHumanReadable(payment.updatedAt)
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
@@ -221,176 +366,186 @@ export default function PaymentEditModal({
|
|||||||
{/* Service Lines Payments */}
|
{/* Service Lines Payments */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
|
<h4 className="font-medium text-gray-900 pt-4">Service Lines</h4>
|
||||||
<div className="mt-2 space-y-3">
|
<div className="mt-3 space-y-4">
|
||||||
{payment.claim.serviceLines.length > 0 ? (
|
{payment.claim.serviceLines.length > 0 ? (
|
||||||
<>
|
payment.claim.serviceLines.map((line) => {
|
||||||
{payment.claim.serviceLines.map((line) => {
|
const isExpanded = expandedLineId === line.id;
|
||||||
return (
|
|
||||||
<div
|
return (
|
||||||
key={line.id}
|
<div
|
||||||
className="border p-3 rounded-md bg-gray-50"
|
key={line.id}
|
||||||
>
|
className="border border-gray-200 p-4 rounded-xl bg-white shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||||
|
>
|
||||||
|
{/* Top Info */}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Procedure Code:</span>{" "}
|
<span className="text-gray-500">Procedure Code:</span>{" "}
|
||||||
{line.procedureCode}
|
<span className="font-medium">
|
||||||
|
{line.procedureCode}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Billed:</span> $
|
<span className="text-gray-500">Billed:</span>{" "}
|
||||||
{Number(line.totalBilled || 0).toFixed(2)}
|
<span className="font-semibold">
|
||||||
|
${Number(line.totalBilled || 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Paid:</span> $
|
<span className="text-gray-500">Paid:</span>{" "}
|
||||||
{Number(line.totalPaid || 0).toFixed(2)}
|
<span className="font-semibold text-green-600">
|
||||||
|
${Number(line.totalPaid || 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Adjusted:</span> $
|
<span className="text-gray-500">Adjusted:</span>{" "}
|
||||||
{Number(line.totalAdjusted || 0).toFixed(2)}
|
<span className="font-semibold text-yellow-600">
|
||||||
|
${Number(line.totalAdjusted || 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Due:</span> $
|
<span className="text-gray-500">Due:</span>{" "}
|
||||||
{Number(line.totalDue || 0).toFixed(2)}
|
<span className="font-semibold text-red-600">
|
||||||
|
${Number(line.totalDue || 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="pt-4 flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditServiceLine(line.id)}
|
||||||
|
>
|
||||||
|
{isExpanded ? "Cancel" : "Pay Partially"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePayFullDue(line)}
|
||||||
|
>
|
||||||
|
Pay Full Due
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded Partial Payment Form */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-4 p-4 border-t border-gray-200 bg-gray-50 rounded-lg space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Paid Amount
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Paid Amount"
|
||||||
|
defaultValue={formState.paidAmount}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(
|
||||||
|
"paidAmount",
|
||||||
|
parseFloat(e.target.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Adjusted Amount
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Adjusted Amount"
|
||||||
|
defaultValue={formState.adjustedAmount}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(
|
||||||
|
"adjustedAmount",
|
||||||
|
parseFloat(e.target.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Payment Method
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={formState.method}
|
||||||
|
onValueChange={(value: PaymentMethod) =>
|
||||||
|
setFormState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
method: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a payment method" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{paymentMethodOptions.map((methodOption) => (
|
||||||
|
<SelectItem
|
||||||
|
key={methodOption}
|
||||||
|
value={methodOption}
|
||||||
|
>
|
||||||
|
{methodOption}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Received Date
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={formState.receivedDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField("receivedDate", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Payer Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Payer Name"
|
||||||
|
value={formState.payerName}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField("payerName", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">Notes</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Notes"
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField("notes", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="pt-2">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEditServiceLine(line.id)}
|
disabled={isUpdatingServiceLine}
|
||||||
|
onClick={() => handleSavePayment()}
|
||||||
>
|
>
|
||||||
{expandedLineId === line.id
|
{isUpdatingStatus ? "Updating..." : "Update"}
|
||||||
? "Cancel"
|
|
||||||
: "Edit Payments"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{expandedLineId === line.id && (
|
</div>
|
||||||
<div className="mt-3 space-y-4">
|
);
|
||||||
<div className="space-y-1">
|
})
|
||||||
<label
|
|
||||||
htmlFor={`paid-${line.id}`}
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Paid Amount
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="Paid Amount"
|
|
||||||
defaultValue={formState.paidAmount}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateField(
|
|
||||||
"paidAmount",
|
|
||||||
parseFloat(e.target.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label
|
|
||||||
htmlFor={`adjusted-${line.id}`}
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Adjusted Amount
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id={`adjusted-${line.id}`}
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="Adjusted Amount"
|
|
||||||
defaultValue={formState.adjustedAmount}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateField(
|
|
||||||
"adjustedAmount",
|
|
||||||
parseFloat(e.target.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium">
|
|
||||||
Payment Method
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={formState.method}
|
|
||||||
onValueChange={(value: PaymentMethod) =>
|
|
||||||
setFormState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
method: value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a payment method" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{paymentMethodOptions.map((methodOption) => (
|
|
||||||
<SelectItem
|
|
||||||
key={methodOption}
|
|
||||||
value={methodOption}
|
|
||||||
>
|
|
||||||
{methodOption}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium">
|
|
||||||
Received Date
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={formState.receivedDate}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateField("receivedDate", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium">
|
|
||||||
Payer Name
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Payer Name"
|
|
||||||
value={formState.payerName}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateField("payerName", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label
|
|
||||||
htmlFor={`notes-${line.id}`}
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Notes
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id={`notes-${line.id}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Notes"
|
|
||||||
onChange={(e) =>
|
|
||||||
updateField("notes", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleSavePayment()}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500">No service lines available.</p>
|
<p className="text-gray-500">No service lines available.</p>
|
||||||
)}
|
)}
|
||||||
@@ -400,39 +555,82 @@ export default function PaymentEditModal({
|
|||||||
{/* Transactions Overview */}
|
{/* Transactions Overview */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-gray-900 pt-6">All Transactions</h4>
|
<h4 className="font-medium text-gray-900 pt-6">All Transactions</h4>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-4 space-y-3">
|
||||||
{payment.serviceLineTransactions.length > 0 ? (
|
{payment.serviceLineTransactions.length > 0 ? (
|
||||||
payment.serviceLineTransactions.map((tx) => (
|
payment.serviceLineTransactions.map((tx) => (
|
||||||
<div
|
<div
|
||||||
key={tx.id}
|
key={tx.id}
|
||||||
className="border p-3 rounded-md bg-white shadow-sm"
|
className="border border-gray-200 p-4 rounded-xl bg-white shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||||
>
|
>
|
||||||
<p>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
|
||||||
<span className="text-gray-500">Date:</span>{" "}
|
{/* Transaction ID */}
|
||||||
{formatDateToHumanReadable(tx.receivedDate)}
|
{tx.id && (
|
||||||
</p>
|
<p>
|
||||||
<p>
|
<span className="text-gray-500">Transaction ID:</span>{" "}
|
||||||
<span className="text-gray-500">Paid Amount:</span> $
|
<span className="font-medium">{tx.id}</span>
|
||||||
{Number(tx.paidAmount).toFixed(2)}
|
</p>
|
||||||
</p>
|
)}
|
||||||
<p>
|
|
||||||
<span className="text-gray-500">Adjusted Amount:</span> $
|
{/* Procedure Code */}
|
||||||
{Number(tx.adjustedAmount).toFixed(2)}
|
{tx.serviceLine?.procedureCode && (
|
||||||
</p>
|
<p>
|
||||||
<p>
|
<span className="text-gray-500">Procedure Code:</span>{" "}
|
||||||
<span className="text-gray-500">Method:</span> {tx.method}
|
<span className="font-medium">
|
||||||
</p>
|
{tx.serviceLine.procedureCode}
|
||||||
{tx.payerName && (
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Paid Amount */}
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Payer Name:</span>{" "}
|
<span className="text-gray-500">Paid Amount:</span>{" "}
|
||||||
{tx.payerName}
|
<span className="font-semibold text-green-600">
|
||||||
|
${Number(tx.paidAmount).toFixed(2)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
{tx.notes && (
|
{/* Adjusted Amount */}
|
||||||
|
{Number(tx.adjustedAmount) > 0 && (
|
||||||
|
<p>
|
||||||
|
<span className="text-gray-500">
|
||||||
|
Adjusted Amount:
|
||||||
|
</span>{" "}
|
||||||
|
<span className="font-semibold text-yellow-600">
|
||||||
|
${Number(tx.adjustedAmount).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-500">Notes:</span> {tx.notes}
|
<span className="text-gray-500">Date:</span>{" "}
|
||||||
|
<span>
|
||||||
|
{formatDateToHumanReadable(tx.receivedDate)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
|
{/* Method */}
|
||||||
|
<p>
|
||||||
|
<span className="text-gray-500">Method:</span>{" "}
|
||||||
|
<span className="capitalize">{tx.method}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Payer Name */}
|
||||||
|
{tx.payerName && tx.payerName.trim() !== "" && (
|
||||||
|
<p className="md:col-span-2">
|
||||||
|
<span className="text-gray-500">Payer Name:</span>{" "}
|
||||||
|
<span>{tx.payerName}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
{tx.notes && tx.notes.trim() !== "" && (
|
||||||
|
<p className="md:col-span-2">
|
||||||
|
<span className="text-gray-500">Notes:</span>{" "}
|
||||||
|
<span>{tx.notes}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@@ -443,7 +641,7 @@ export default function PaymentEditModal({
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end space-x-2 pt-6">
|
<div className="flex justify-end space-x-2 pt-6">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="default" onClick={onClose}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
TrendingUp,
|
||||||
|
ThumbsDown,
|
||||||
|
DollarSign,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -30,15 +33,15 @@ import {
|
|||||||
} from "@/components/ui/pagination";
|
} from "@/components/ui/pagination";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
|
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
|
||||||
import PaymentViewModal from "./payment-view-modal";
|
|
||||||
import LoadingScreen from "../ui/LoadingScreen";
|
import LoadingScreen from "../ui/LoadingScreen";
|
||||||
import {
|
import {
|
||||||
ClaimStatus,
|
ClaimStatus,
|
||||||
ClaimWithServiceLines,
|
|
||||||
NewTransactionPayload,
|
NewTransactionPayload,
|
||||||
|
PaymentStatus,
|
||||||
PaymentWithExtras,
|
PaymentWithExtras,
|
||||||
} from "@repo/db/types";
|
} from "@repo/db/types";
|
||||||
import EditPaymentModal from "./payment-edit-modal";
|
import EditPaymentModal from "./payment-edit-modal";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
|
||||||
interface PaymentApiResponse {
|
interface PaymentApiResponse {
|
||||||
payments: PaymentWithExtras[];
|
payments: PaymentWithExtras[];
|
||||||
@@ -47,7 +50,6 @@ interface PaymentApiResponse {
|
|||||||
|
|
||||||
interface PaymentsRecentTableProps {
|
interface PaymentsRecentTableProps {
|
||||||
allowEdit?: boolean;
|
allowEdit?: boolean;
|
||||||
allowView?: boolean;
|
|
||||||
allowDelete?: boolean;
|
allowDelete?: boolean;
|
||||||
allowCheckbox?: boolean;
|
allowCheckbox?: boolean;
|
||||||
onSelectPayment?: (payment: PaymentWithExtras | null) => void;
|
onSelectPayment?: (payment: PaymentWithExtras | null) => void;
|
||||||
@@ -57,7 +59,6 @@ interface PaymentsRecentTableProps {
|
|||||||
|
|
||||||
export default function PaymentsRecentTable({
|
export default function PaymentsRecentTable({
|
||||||
allowEdit,
|
allowEdit,
|
||||||
allowView,
|
|
||||||
allowDelete,
|
allowDelete,
|
||||||
allowCheckbox,
|
allowCheckbox,
|
||||||
onSelectPayment,
|
onSelectPayment,
|
||||||
@@ -66,7 +67,6 @@ export default function PaymentsRecentTable({
|
|||||||
}: PaymentsRecentTableProps) {
|
}: PaymentsRecentTableProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [isViewPaymentOpen, setIsViewPaymentOpen] = useState(false);
|
|
||||||
const [isEditPaymentOpen, setIsEditPaymentOpen] = useState(false);
|
const [isEditPaymentOpen, setIsEditPaymentOpen] = useState(false);
|
||||||
const [isDeletePaymentOpen, setIsDeletePaymentOpen] = useState(false);
|
const [isDeletePaymentOpen, setIsDeletePaymentOpen] = useState(false);
|
||||||
|
|
||||||
@@ -131,17 +131,25 @@ export default function PaymentsRecentTable({
|
|||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async (updated, { paymentId }) => {
|
||||||
setIsEditPaymentOpen(false);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "Payment updated successfully!",
|
description: "Payment updated successfully!",
|
||||||
variant: "default",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getPaymentsQueryKey(),
|
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) => {
|
onError: (error) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
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({
|
const deletePaymentMutation = useMutation({
|
||||||
mutationFn: async (id: number) => {
|
mutationFn: async (id: number) => {
|
||||||
const res = await apiRequest("DELETE", `/api/payments/${id}`);
|
const res = await apiRequest("DELETE", `/api/payments/${id}`);
|
||||||
@@ -182,11 +241,6 @@ export default function PaymentsRecentTable({
|
|||||||
setIsEditPaymentOpen(true);
|
setIsEditPaymentOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewPayment = (payment: PaymentWithExtras) => {
|
|
||||||
setCurrentPayment(payment);
|
|
||||||
setIsViewPaymentOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePayment = (payment: PaymentWithExtras) => {
|
const handleDeletePayment = (payment: PaymentWithExtras) => {
|
||||||
setCurrentPayment(payment);
|
setCurrentPayment(payment);
|
||||||
setIsDeletePaymentOpen(true);
|
setIsDeletePaymentOpen(true);
|
||||||
@@ -231,7 +285,7 @@ export default function PaymentsRecentTable({
|
|||||||
paymentsData?.totalCount || 0
|
paymentsData?.totalCount || 0
|
||||||
);
|
);
|
||||||
|
|
||||||
const getInitialsFromName = (fullName: string) => {
|
const getInitials = (fullName: string) => {
|
||||||
const parts = fullName.trim().split(/\s+/);
|
const parts = fullName.trim().split(/\s+/);
|
||||||
const filteredParts = parts.filter((part) => part.length > 0);
|
const filteredParts = parts.filter((part) => part.length > 0);
|
||||||
if (filteredParts.length === 0) {
|
if (filteredParts.length === 0) {
|
||||||
@@ -260,7 +314,7 @@ export default function PaymentsRecentTable({
|
|||||||
return colorClasses[id % colorClasses.length];
|
return colorClasses[id % colorClasses.length];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusInfo = (status?: ClaimStatus) => {
|
const getStatusInfo = (status?: PaymentStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "PENDING":
|
case "PENDING":
|
||||||
return {
|
return {
|
||||||
@@ -268,22 +322,35 @@ export default function PaymentsRecentTable({
|
|||||||
color: "bg-yellow-100 text-yellow-800",
|
color: "bg-yellow-100 text-yellow-800",
|
||||||
icon: <Clock className="h-3 w-3 mr-1" />,
|
icon: <Clock className="h-3 w-3 mr-1" />,
|
||||||
};
|
};
|
||||||
case "APPROVED":
|
case "PARTIALLY_PAID":
|
||||||
return {
|
return {
|
||||||
label: "Approved",
|
label: "Partially Paid",
|
||||||
|
color: "bg-blue-100 text-blue-800",
|
||||||
|
icon: <DollarSign className="h-3 w-3 mr-1" />,
|
||||||
|
};
|
||||||
|
case "PAID":
|
||||||
|
return {
|
||||||
|
label: "Paid",
|
||||||
color: "bg-green-100 text-green-800",
|
color: "bg-green-100 text-green-800",
|
||||||
icon: <CheckCircle className="h-3 w-3 mr-1" />,
|
icon: <CheckCircle className="h-3 w-3 mr-1" />,
|
||||||
};
|
};
|
||||||
case "CANCELLED":
|
case "OVERPAID":
|
||||||
return {
|
return {
|
||||||
label: "Cancelled",
|
label: "Overpaid",
|
||||||
|
color: "bg-purple-100 text-purple-800",
|
||||||
|
icon: <TrendingUp className="h-3 w-3 mr-1" />,
|
||||||
|
};
|
||||||
|
case "DENIED":
|
||||||
|
return {
|
||||||
|
label: "Denied",
|
||||||
color: "bg-red-100 text-red-800",
|
color: "bg-red-100 text-red-800",
|
||||||
icon: <AlertCircle className="h-3 w-3 mr-1" />,
|
icon: <ThumbsDown className="h-3 w-3 mr-1" />,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
label: status
|
label: status
|
||||||
? status.charAt(0).toUpperCase() + status.slice(1)
|
? (status as string).charAt(0).toUpperCase() +
|
||||||
|
(status as string).slice(1).toLowerCase()
|
||||||
: "Unknown",
|
: "Unknown",
|
||||||
color: "bg-gray-100 text-gray-800",
|
color: "bg-gray-100 text-gray-800",
|
||||||
icon: <AlertCircle className="h-3 w-3 mr-1" />,
|
icon: <AlertCircle className="h-3 w-3 mr-1" />,
|
||||||
@@ -318,10 +385,11 @@ export default function PaymentsRecentTable({
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
{allowCheckbox && <TableHead>Select</TableHead>}
|
{allowCheckbox && <TableHead>Select</TableHead>}
|
||||||
<TableHead>Payment ID</TableHead>
|
<TableHead>Payment ID</TableHead>
|
||||||
|
<TableHead>Claim ID</TableHead>
|
||||||
<TableHead>Patient Name</TableHead>
|
<TableHead>Patient Name</TableHead>
|
||||||
<TableHead>Amount</TableHead>
|
<TableHead>Amount</TableHead>
|
||||||
<TableHead>Date</TableHead>
|
<TableHead>Claim Submitted on</TableHead>
|
||||||
<TableHead>Method</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -374,7 +442,34 @@ export default function PaymentsRecentTable({
|
|||||||
? `PAY-${payment.id.toString().padStart(4, "0")}`
|
? `PAY-${payment.id.toString().padStart(4, "0")}`
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{payment.patientName}</TableCell>
|
|
||||||
|
<TableCell>
|
||||||
|
{typeof payment.claimId === "number"
|
||||||
|
? `CLM-${payment.claimId.toString().padStart(4, "0")}`
|
||||||
|
: "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Avatar
|
||||||
|
className={`h-10 w-10 ${getAvatarColor(Number(payment.id))}`}
|
||||||
|
>
|
||||||
|
<AvatarFallback className="text-white">
|
||||||
|
{getInitials(payment.patientName)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="text-sm font-medium text-gray-900">
|
||||||
|
{payment.patientName}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
PID-{payment.patientId?.toString().padStart(4, "0")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
{/* 💰 Billed / Paid / Due breakdown */}
|
{/* 💰 Billed / Paid / Due breakdown */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -400,7 +495,27 @@ export default function PaymentsRecentTable({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{formatDateToHumanReadable(payment.paymentDate)}
|
{formatDateToHumanReadable(payment.paymentDate)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{payment.paymentMethod}</TableCell>
|
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{(() => {
|
||||||
|
const { label, color, icon } = getStatusInfo(
|
||||||
|
payment.status
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 text-xs font-medium rounded-full ${color}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center">
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
{allowDelete && (
|
{allowDelete && (
|
||||||
@@ -428,18 +543,6 @@ export default function PaymentsRecentTable({
|
|||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{allowView && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => {
|
|
||||||
handleViewPayment(payment);
|
|
||||||
}}
|
|
||||||
className="text-gray-600 hover:text-gray-800 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -457,17 +560,6 @@ export default function PaymentsRecentTable({
|
|||||||
entityName={`ClaimID : ${currentPayment?.claimId}`}
|
entityName={`ClaimID : ${currentPayment?.claimId}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* /will hanlde both modal later */}
|
|
||||||
{/* {isViewPaymentOpen && currentPayment && (
|
|
||||||
<ClaimPaymentModal
|
|
||||||
isOpen={isViewPaymentOpen}
|
|
||||||
onClose={() => setIsViewPaymentOpen(false)}
|
|
||||||
onOpenChange={(open) => setIsViewPaymentOpen(open)}
|
|
||||||
onEditClaim={(payment) => handleEditPayment(payment)}
|
|
||||||
payment={currentPayment}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{isEditPaymentOpen && currentPayment && (
|
{isEditPaymentOpen && currentPayment && (
|
||||||
<EditPaymentModal
|
<EditPaymentModal
|
||||||
isOpen={isEditPaymentOpen}
|
isOpen={isEditPaymentOpen}
|
||||||
@@ -477,6 +569,11 @@ export default function PaymentsRecentTable({
|
|||||||
onEditServiceLine={(updatedPayment) => {
|
onEditServiceLine={(updatedPayment) => {
|
||||||
updatePaymentMutation.mutate(updatedPayment);
|
updatePaymentMutation.mutate(updatedPayment);
|
||||||
}}
|
}}
|
||||||
|
isUpdatingServiceLine={updatePaymentMutation.isPending}
|
||||||
|
onUpdateStatus={(paymentId, status) => {
|
||||||
|
updatePaymentStatusMutation.mutate({ paymentId, status });
|
||||||
|
}}
|
||||||
|
isUpdatingStatus={updatePaymentStatusMutation.isPending}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
|
CardDescription,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -47,7 +48,6 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
import PaymentsRecentTable from "@/components/payments/payments-recent-table";
|
||||||
import { Appointment, Patient } from "@repo/db/types";
|
|
||||||
|
|
||||||
export default function PaymentsPage() {
|
export default function PaymentsPage() {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
@@ -55,122 +55,15 @@ export default function PaymentsPage() {
|
|||||||
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
|
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
const [isExtracting, setIsExtracting] = useState(false);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [showReimbursementWindow, setShowReimbursementWindow] = useState(false);
|
|
||||||
const [extractedPaymentData, setExtractedPaymentData] = useState<any[]>([]);
|
const [extractedPaymentData, setExtractedPaymentData] = useState<any[]>([]);
|
||||||
const [editableData, setEditableData] = useState<any[]>([]);
|
const [editableData, setEditableData] = useState<any[]>([]);
|
||||||
const [paidItems, setPaidItems] = useState<Record<string, boolean>>({});
|
|
||||||
const [adjustmentValues, setAdjustmentValues] = useState<
|
|
||||||
Record<string, number>
|
|
||||||
>({});
|
|
||||||
const [balanceValues, setBalanceValues] = useState<Record<string, number>>(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const { toast } = useToast();
|
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<Appointment[]>({
|
|
||||||
queryKey: ["/api/appointments"],
|
|
||||||
enabled: !!user,
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
const toggleMobileMenu = () => {
|
||||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
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
|
// Image upload handlers for OCR
|
||||||
const handleImageDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
const handleImageDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -210,295 +103,6 @@ export default function PaymentsPage() {
|
|||||||
|
|
||||||
// Simulate OCR extraction process
|
// Simulate OCR extraction process
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
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 {
|
} finally {
|
||||||
setIsExtracting(false);
|
setIsExtracting(false);
|
||||||
}
|
}
|
||||||
@@ -585,12 +189,10 @@ export default function PaymentsPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<DollarSign className="h-5 w-5 text-yellow-500 mr-2" />
|
<DollarSign className="h-5 w-5 text-yellow-500 mr-2" />
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">$0</div>
|
||||||
${totalOutstanding.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
From {sampleOutstanding.length} outstanding invoices
|
From 0 outstanding invoices
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -604,17 +206,10 @@ export default function PaymentsPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<DollarSign className="h-5 w-5 text-green-500 mr-2" />
|
<DollarSign className="h-5 w-5 text-green-500 mr-2" />
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">${0}</div>
|
||||||
${totalCollected.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
From{" "}
|
From 0 completed payments
|
||||||
{
|
|
||||||
samplePayments.filter((p) => p.status === "completed")
|
|
||||||
.length
|
|
||||||
}{" "}
|
|
||||||
completed payments
|
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -628,23 +223,16 @@ export default function PaymentsPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<DollarSign className="h-5 w-5 text-blue-500 mr-2" />
|
<DollarSign className="h-5 w-5 text-blue-500 mr-2" />
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">$0</div>
|
||||||
${pendingAmount.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
From{" "}
|
From 0 pending transactions
|
||||||
{
|
|
||||||
samplePayments.filter((p) => p.status === "processing")
|
|
||||||
.length
|
|
||||||
}{" "}
|
|
||||||
pending transactions
|
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OCR Image Upload Section */}
|
{/* OCR Image Upload Section - not working rn*/}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
<h2 className="text-xl font-medium text-gray-800">
|
||||||
@@ -757,316 +345,20 @@ export default function PaymentsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Outstanding Balances Section */}
|
{/* Recent Payments table */}
|
||||||
<div className="mb-8">
|
<Card>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<CardHeader>
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
<CardTitle>Payment's Records</CardTitle>
|
||||||
Outstanding Balances
|
<CardDescription>
|
||||||
</h2>
|
View and manage all recents patient's claims payments
|
||||||
</div>
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
<Card>
|
<CardContent>
|
||||||
<CardContent className="p-0">
|
<PaymentsRecentTable allowEdit allowDelete />
|
||||||
{sampleOutstanding.length > 0 ? (
|
</CardContent>
|
||||||
<Table>
|
</Card>
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Patient</TableHead>
|
|
||||||
<TableHead>Claimed procedure codes</TableHead>
|
|
||||||
<TableHead>Amount</TableHead>
|
|
||||||
<TableHead>Paid</TableHead>
|
|
||||||
<TableHead>Adjustment</TableHead>
|
|
||||||
<TableHead>Balance</TableHead>
|
|
||||||
<TableHead className="text-right">Action</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{sampleOutstanding.map((invoice) => {
|
|
||||||
const patient = patients.find(
|
|
||||||
(p) => p.id === invoice.patientId
|
|
||||||
) || { firstName: "Sample", lastName: "Patient" };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={invoice.id}>
|
|
||||||
<TableCell>
|
|
||||||
{patient.firstName} {patient.lastName}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{invoice.description}</TableCell>
|
|
||||||
<TableCell>${invoice.amount.toFixed(2)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Checkbox
|
|
||||||
checked={paidItems[invoice.id] || false}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setPaidItems((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[invoice.id]: !!checked,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="0.00"
|
|
||||||
value={adjustmentValues[invoice.id] || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = parseFloat(e.target.value) || 0;
|
|
||||||
setAdjustmentValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[invoice.id]: value,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className="w-20 h-8"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="0.00"
|
|
||||||
value={balanceValues[invoice.id] || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = parseFloat(e.target.value) || 0;
|
|
||||||
setBalanceValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[invoice.id]: value,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className="w-20 h-8"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
handleRecordPayment(
|
|
||||||
invoice.patientId,
|
|
||||||
invoice.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Send an invoice
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<AlertCircle className="h-12 w-12 mx-auto text-gray-400 mb-3" />
|
|
||||||
<h3 className="text-lg font-medium">
|
|
||||||
No outstanding balances
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500 mt-1">
|
|
||||||
All patient accounts are current
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
{sampleOutstanding.length > 0 && (
|
|
||||||
<CardFooter className="flex justify-between px-6 py-4 border-t">
|
|
||||||
<div className="flex items-center text-sm text-gray-500">
|
|
||||||
<ArrowDown className="h-4 w-4 mr-1" />
|
|
||||||
<span>Download statement</span>
|
|
||||||
</div>
|
|
||||||
<div className="font-medium">
|
|
||||||
Total: ${totalOutstanding.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PaymentsRecentTable
|
|
||||||
allowView
|
|
||||||
allowEdit
|
|
||||||
allowDelete
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reimbursement Data Popup */}
|
|
||||||
<Dialog
|
|
||||||
open={showReimbursementWindow}
|
|
||||||
onOpenChange={setShowReimbursementWindow}
|
|
||||||
>
|
|
||||||
<DialogContent className="max-w-6xl max-h-[80vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-xl font-bold">
|
|
||||||
Insurance Reimbursement/Payment Details
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Member Name</TableHead>
|
|
||||||
<TableHead>Member ID</TableHead>
|
|
||||||
<TableHead>Procedure Code</TableHead>
|
|
||||||
<TableHead>Date of Service</TableHead>
|
|
||||||
<TableHead>Billed Amount</TableHead>
|
|
||||||
<TableHead>Allowed Amount</TableHead>
|
|
||||||
<TableHead>Paid Amount</TableHead>
|
|
||||||
<TableHead>Delete/Save</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{editableData.map((payment, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
<Input
|
|
||||||
value={payment.memberName}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateEditableData(
|
|
||||||
index,
|
|
||||||
"memberName",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="border-0 p-1 bg-transparent"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
value={payment.memberId}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateEditableData(index, "memberId", e.target.value)
|
|
||||||
}
|
|
||||||
className="border-0 p-1 bg-transparent"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
value={payment.procedureCode}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateEditableData(
|
|
||||||
index,
|
|
||||||
"procedureCode",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="border-0 p-1 bg-transparent"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
value={payment.dateOfService}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateEditableData(
|
|
||||||
index,
|
|
||||||
"dateOfService",
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="border-0 p-1 bg-transparent"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={payment.billedAmount}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateEditableData(
|
|
||||||
index,
|
|
||||||
"billedAmount",
|
|
||||||
parseFloat(e.target.value) || 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="border-0 p-1 bg-transparent"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={payment.allowedAmount}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateEditableData(
|
|
||||||
index,
|
|
||||||
"allowedAmount",
|
|
||||||
parseFloat(e.target.value) || 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="border-0 p-1 bg-transparent"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-medium text-green-600">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={payment.paidAmount}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateEditableData(
|
|
||||||
index,
|
|
||||||
"paidAmount",
|
|
||||||
parseFloat(e.target.value) || 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="border-0 p-1 bg-transparent text-green-600 font-medium"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => deleteRow(index)}
|
|
||||||
className="h-8 w-8 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => saveRow(index)}
|
|
||||||
className="h-8 w-8 p-0 text-green-500 hover:text-green-700 hover:bg-green-50"
|
|
||||||
>
|
|
||||||
<Save className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{extractedPaymentData.length === 0 && (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
No payment data extracted
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="mt-6">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setShowReimbursementWindow(false)}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
toast({
|
|
||||||
title: "Data Processed",
|
|
||||||
description:
|
|
||||||
"Payment information has been processed successfully",
|
|
||||||
});
|
|
||||||
setShowReimbursementWindow(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Process Payments
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
PaymentUncheckedCreateInputObjectSchema,
|
PaymentUncheckedCreateInputObjectSchema,
|
||||||
ClaimUncheckedCreateInputObjectSchema,
|
|
||||||
ServiceLineTransactionCreateInputObjectSchema,
|
ServiceLineTransactionCreateInputObjectSchema,
|
||||||
ClaimStatusSchema,
|
|
||||||
StaffUncheckedCreateInputObjectSchema,
|
|
||||||
PaymentMethodSchema,
|
PaymentMethodSchema,
|
||||||
PaymentStatusSchema,
|
PaymentStatusSchema,
|
||||||
} from "@repo/db/usedSchemas";
|
} from "@repo/db/usedSchemas";
|
||||||
import { Prisma } from "@repo/db/generated/prisma";
|
import { Prisma } from "@repo/db/generated/prisma";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Decimal } from "decimal.js";
|
|
||||||
|
|
||||||
// ========== BASIC TYPES ==========
|
// ========== BASIC TYPES ==========
|
||||||
|
|
||||||
@@ -30,6 +26,7 @@ export type ServiceLineTransactionRecord =
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
// Enum for payment
|
// Enum for payment
|
||||||
|
export { PaymentStatusSchema };
|
||||||
export type PaymentStatus = z.infer<typeof PaymentStatusSchema>;
|
export type PaymentStatus = z.infer<typeof PaymentStatusSchema>;
|
||||||
export type PaymentMethod = z.infer<typeof PaymentMethodSchema>;
|
export type PaymentMethod = z.infer<typeof PaymentMethodSchema>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user