absoulute full due added
This commit is contained in:
@@ -7,11 +7,11 @@ import {
|
|||||||
insertPaymentSchema,
|
insertPaymentSchema,
|
||||||
NewTransactionPayload,
|
NewTransactionPayload,
|
||||||
newTransactionPayloadSchema,
|
newTransactionPayloadSchema,
|
||||||
updatePaymentSchema,
|
paymentMethodOptions,
|
||||||
} from "@repo/db/types";
|
} from "@repo/db/types";
|
||||||
import Decimal from "decimal.js";
|
|
||||||
import { prisma } from "@repo/db/client";
|
import { prisma } from "@repo/db/client";
|
||||||
import { PaymentStatusSchema } from "@repo/db/types";
|
import { PaymentStatusSchema } from "@repo/db/types";
|
||||||
|
import * as paymentService from "../services/paymentService";
|
||||||
|
|
||||||
const paymentFilterSchema = z.object({
|
const paymentFilterSchema = z.object({
|
||||||
from: z.string().datetime(),
|
from: z.string().datetime(),
|
||||||
@@ -201,9 +201,6 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
const paymentId = parseIntOrError(req.params.id, "Payment ID");
|
const paymentId = parseIntOrError(req.params.id, "Payment ID");
|
||||||
const paymentRecord = await storage.getPaymentById(paymentId);
|
|
||||||
if (!paymentRecord)
|
|
||||||
return res.status(404).json({ message: "Payment not found" });
|
|
||||||
|
|
||||||
const validated = newTransactionPayloadSchema.safeParse(
|
const validated = newTransactionPayloadSchema.safeParse(
|
||||||
req.body.data as NewTransactionPayload
|
req.body.data as NewTransactionPayload
|
||||||
@@ -215,124 +212,15 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status, serviceLineTransactions } = validated.data;
|
const { serviceLineTransactions } = validated.data;
|
||||||
|
|
||||||
// validation if req is valid
|
const updatedPayment = await paymentService.updatePayment(
|
||||||
for (const txn of serviceLineTransactions) {
|
paymentId,
|
||||||
const line = paymentRecord.claim.serviceLines.find(
|
serviceLineTransactions,
|
||||||
(sl) => sl.id === txn.serviceLineId
|
userId
|
||||||
);
|
);
|
||||||
if (!line)
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json({ message: `Invalid service line: ${txn.serviceLineId}` });
|
|
||||||
|
|
||||||
const paidAmount = new Decimal(txn.paidAmount ?? 0);
|
res.status(200).json(updatedPayment);
|
||||||
const adjustedAmount = new Decimal(txn.adjustedAmount ?? 0);
|
|
||||||
if (paidAmount.lt(0) || adjustedAmount.lt(0)) {
|
|
||||||
return res.status(400).json({ message: "Amounts cannot be negative" });
|
|
||||||
}
|
|
||||||
if (paidAmount.eq(0) && adjustedAmount.eq(0)) {
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.json({ message: "Must provide a payment or adjustment" });
|
|
||||||
}
|
|
||||||
if (paidAmount.gt(line.totalDue)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
message: `Paid amount exceeds due for service line ${txn.serviceLineId}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap everything in a transaction
|
|
||||||
const result = await prisma.$transaction(async (tx) => {
|
|
||||||
// 1. Create all new service line transactions
|
|
||||||
for (const txn of serviceLineTransactions) {
|
|
||||||
await tx.serviceLineTransaction.create({
|
|
||||||
data: {
|
|
||||||
paymentId,
|
|
||||||
serviceLineId: txn.serviceLineId,
|
|
||||||
transactionId: txn.transactionId,
|
|
||||||
paidAmount: new Decimal(txn.paidAmount || 0),
|
|
||||||
adjustedAmount: new Decimal(txn.adjustedAmount || 0),
|
|
||||||
method: txn.method,
|
|
||||||
receivedDate: txn.receivedDate,
|
|
||||||
payerName: txn.payerName,
|
|
||||||
notes: txn.notes,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Recalculate that specific service line's totals
|
|
||||||
const aggLine = await tx.serviceLineTransaction.aggregate({
|
|
||||||
_sum: {
|
|
||||||
paidAmount: true,
|
|
||||||
adjustedAmount: true,
|
|
||||||
},
|
|
||||||
where: { serviceLineId: txn.serviceLineId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const serviceLine = await tx.serviceLine.findUniqueOrThrow({
|
|
||||||
where: { id: txn.serviceLineId },
|
|
||||||
select: { totalBilled: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPaid = aggLine._sum.paidAmount || new Decimal(0);
|
|
||||||
const totalAdjusted = aggLine._sum.adjustedAmount || new Decimal(0);
|
|
||||||
const totalDue = serviceLine.totalBilled
|
|
||||||
.minus(totalPaid)
|
|
||||||
.minus(totalAdjusted);
|
|
||||||
|
|
||||||
await tx.serviceLine.update({
|
|
||||||
where: { id: txn.serviceLineId },
|
|
||||||
data: {
|
|
||||||
totalPaid,
|
|
||||||
totalAdjusted,
|
|
||||||
totalDue,
|
|
||||||
status:
|
|
||||||
totalDue.lte(0) && totalPaid.gt(0)
|
|
||||||
? "PAID"
|
|
||||||
: totalPaid.gt(0)
|
|
||||||
? "PARTIALLY_PAID"
|
|
||||||
: "UNPAID",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Recalculate payment totals
|
|
||||||
const aggPayment = await tx.serviceLineTransaction.aggregate({
|
|
||||||
_sum: {
|
|
||||||
paidAmount: true,
|
|
||||||
adjustedAmount: true,
|
|
||||||
},
|
|
||||||
where: { paymentId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const payment = await tx.payment.findUniqueOrThrow({
|
|
||||||
where: { id: paymentId },
|
|
||||||
select: { totalBilled: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPaid = aggPayment._sum.paidAmount || new Decimal(0);
|
|
||||||
const totalAdjusted = aggPayment._sum.adjustedAmount || new Decimal(0);
|
|
||||||
const totalDue = payment.totalBilled
|
|
||||||
.minus(totalPaid)
|
|
||||||
.minus(totalAdjusted);
|
|
||||||
|
|
||||||
const updatedPayment = await tx.payment.update({
|
|
||||||
where: { id: paymentId },
|
|
||||||
data: {
|
|
||||||
totalPaid,
|
|
||||||
totalAdjusted,
|
|
||||||
totalDue,
|
|
||||||
status,
|
|
||||||
updatedById: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedPayment;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(result);
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message =
|
const message =
|
||||||
err instanceof Error ? err.message : "Failed to update payment";
|
err instanceof Error ? err.message : "Failed to update payment";
|
||||||
@@ -340,6 +228,94 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PUT /api/payments/:id/pay-absolute-full-claim
|
||||||
|
router.put(
|
||||||
|
"/:id/pay-absolute-full-claim",
|
||||||
|
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 paymentRecord = await storage.getPaymentById(paymentId);
|
||||||
|
if (!paymentRecord) {
|
||||||
|
return res.status(404).json({ message: "Payment not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceLineTransactions = paymentRecord.claim.serviceLines
|
||||||
|
.filter((line) => line.totalDue.gt(0))
|
||||||
|
.map((line) => ({
|
||||||
|
serviceLineId: line.id,
|
||||||
|
paidAmount: line.totalDue.toNumber(),
|
||||||
|
adjustedAmount: 0,
|
||||||
|
method: paymentMethodOptions[1],
|
||||||
|
receivedDate: new Date(),
|
||||||
|
notes: "Full claim payment",
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (serviceLineTransactions.length === 0) {
|
||||||
|
return res.status(400).json({ message: "No outstanding balance" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use updatePayment for consistency & validation
|
||||||
|
const updatedPayment = await paymentService.updatePayment(
|
||||||
|
paymentId,
|
||||||
|
serviceLineTransactions,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json(updatedPayment);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ message: "Failed to pay full claim" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// PUT /api/payments/:id/revert-full-claim
|
||||||
|
router.put(
|
||||||
|
"/:id/revert-full-claim",
|
||||||
|
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 paymentRecord = await storage.getPaymentById(paymentId);
|
||||||
|
if (!paymentRecord) {
|
||||||
|
return res.status(404).json({ message: "Payment not found" });
|
||||||
|
}
|
||||||
|
// Build reversal transactions (negating what’s already paid/adjusted)
|
||||||
|
const serviceLineTransactions = paymentRecord.claim.serviceLines
|
||||||
|
.filter((line) => line.totalPaid.gt(0) || line.totalAdjusted.gt(0))
|
||||||
|
.map((line) => ({
|
||||||
|
serviceLineId: line.id,
|
||||||
|
paidAmount: line.totalPaid.negated().toNumber(), // negative to undo
|
||||||
|
adjustedAmount: line.totalAdjusted.negated().toNumber(),
|
||||||
|
method: paymentMethodOptions[4],
|
||||||
|
receivedDate: new Date(),
|
||||||
|
notes: "Reverted full claim",
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (serviceLineTransactions.length === 0) {
|
||||||
|
return res.status(400).json({ message: "Nothing to revert" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPayment = await paymentService.updatePayment(
|
||||||
|
paymentId,
|
||||||
|
serviceLineTransactions,
|
||||||
|
userId,
|
||||||
|
{ isReversal: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json(updatedPayment);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ message: "Failed to revert claim payments" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// PATCH /api/payments/:id/status
|
// PATCH /api/payments/:id/status
|
||||||
router.patch(
|
router.patch(
|
||||||
"/:id/status",
|
"/:id/status",
|
||||||
|
|||||||
144
apps/Backend/src/services/paymentService.ts
Normal file
144
apps/Backend/src/services/paymentService.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import Decimal from "decimal.js";
|
||||||
|
import { NewTransactionPayload, Payment, PaymentStatus } from "@repo/db/types";
|
||||||
|
import { storage } from "../storage";
|
||||||
|
import { prisma } from "@repo/db/client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate transactions against a payment record
|
||||||
|
*/
|
||||||
|
export async function validateTransactions(
|
||||||
|
paymentId: number,
|
||||||
|
serviceLineTransactions: NewTransactionPayload["serviceLineTransactions"],
|
||||||
|
options?: { isReversal?: boolean }
|
||||||
|
) {
|
||||||
|
const paymentRecord = await storage.getPaymentById(paymentId);
|
||||||
|
if (!paymentRecord) {
|
||||||
|
throw new Error("Payment not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const txn of serviceLineTransactions) {
|
||||||
|
const line = paymentRecord.claim.serviceLines.find(
|
||||||
|
(sl) => sl.id === txn.serviceLineId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!line) {
|
||||||
|
throw new Error(`Invalid service line: ${txn.serviceLineId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const paidAmount = new Decimal(txn.paidAmount ?? 0);
|
||||||
|
const adjustedAmount = new Decimal(txn.adjustedAmount ?? 0);
|
||||||
|
|
||||||
|
if (!options?.isReversal && (paidAmount.lt(0) || adjustedAmount.lt(0))) {
|
||||||
|
throw new Error("Amounts cannot be negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paidAmount.eq(0) && adjustedAmount.eq(0)) {
|
||||||
|
throw new Error("Must provide a payment or adjustment");
|
||||||
|
}
|
||||||
|
if (!options?.isReversal && paidAmount.gt(line.totalDue)) {
|
||||||
|
throw new Error(
|
||||||
|
`Paid amount exceeds due for service line ${txn.serviceLineId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paymentRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply transactions to a payment & recalc totals
|
||||||
|
*/
|
||||||
|
export async function applyTransactions(
|
||||||
|
paymentId: number,
|
||||||
|
serviceLineTransactions: NewTransactionPayload["serviceLineTransactions"],
|
||||||
|
userId: number
|
||||||
|
): Promise<Payment> {
|
||||||
|
return prisma.$transaction(async (tx) => {
|
||||||
|
// 1. Insert service line transactions + recalculate each service line
|
||||||
|
for (const txn of serviceLineTransactions) {
|
||||||
|
await tx.serviceLineTransaction.create({
|
||||||
|
data: {
|
||||||
|
paymentId,
|
||||||
|
serviceLineId: txn.serviceLineId,
|
||||||
|
transactionId: txn.transactionId,
|
||||||
|
paidAmount: new Decimal(txn.paidAmount),
|
||||||
|
adjustedAmount: new Decimal(txn.adjustedAmount || 0),
|
||||||
|
method: txn.method,
|
||||||
|
receivedDate: txn.receivedDate,
|
||||||
|
payerName: txn.payerName,
|
||||||
|
notes: txn.notes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recalculate service line totals
|
||||||
|
const aggLine = await tx.serviceLineTransaction.aggregate({
|
||||||
|
_sum: { paidAmount: true, adjustedAmount: true },
|
||||||
|
where: { serviceLineId: txn.serviceLineId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const serviceLine = await tx.serviceLine.findUniqueOrThrow({
|
||||||
|
where: { id: txn.serviceLineId },
|
||||||
|
select: { totalBilled: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPaid = aggLine._sum.paidAmount || new Decimal(0);
|
||||||
|
const totalAdjusted = aggLine._sum.adjustedAmount || new Decimal(0);
|
||||||
|
const totalDue = serviceLine.totalBilled
|
||||||
|
.minus(totalPaid)
|
||||||
|
.minus(totalAdjusted);
|
||||||
|
|
||||||
|
await tx.serviceLine.update({
|
||||||
|
where: { id: txn.serviceLineId },
|
||||||
|
data: {
|
||||||
|
totalPaid,
|
||||||
|
totalAdjusted,
|
||||||
|
totalDue,
|
||||||
|
status:
|
||||||
|
totalDue.lte(0) && totalPaid.gt(0)
|
||||||
|
? "PAID"
|
||||||
|
: totalPaid.gt(0)
|
||||||
|
? "PARTIALLY_PAID"
|
||||||
|
: "UNPAID",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Recalc payment totals
|
||||||
|
const aggPayment = await tx.serviceLineTransaction.aggregate({
|
||||||
|
_sum: { paidAmount: true, adjustedAmount: true },
|
||||||
|
where: { paymentId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const payment = await tx.payment.findUniqueOrThrow({
|
||||||
|
where: { id: paymentId },
|
||||||
|
select: { totalBilled: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPaid = aggPayment._sum.paidAmount || new Decimal(0);
|
||||||
|
const totalAdjusted = aggPayment._sum.adjustedAmount || new Decimal(0);
|
||||||
|
const totalDue = payment.totalBilled.minus(totalPaid).minus(totalAdjusted);
|
||||||
|
|
||||||
|
let status: PaymentStatus;
|
||||||
|
if (totalDue.lte(0) && totalPaid.gt(0)) status = "PAID";
|
||||||
|
else if (totalPaid.gt(0)) status = "PARTIALLY_PAID";
|
||||||
|
else status = "PENDING";
|
||||||
|
|
||||||
|
return tx.payment.update({
|
||||||
|
where: { id: paymentId },
|
||||||
|
data: { totalPaid, totalAdjusted, totalDue, status, updatedById: userId },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for updating payments
|
||||||
|
*/
|
||||||
|
export async function updatePayment(
|
||||||
|
paymentId: number,
|
||||||
|
serviceLineTransactions: NewTransactionPayload["serviceLineTransactions"],
|
||||||
|
userId: number,
|
||||||
|
options?: { isReversal?: boolean }
|
||||||
|
): Promise<Payment> {
|
||||||
|
await validateTransactions(paymentId, serviceLineTransactions, options);
|
||||||
|
return applyTransactions(paymentId, serviceLineTransactions, userId);
|
||||||
|
}
|
||||||
@@ -161,7 +161,6 @@ export default function PaymentEditModal({
|
|||||||
|
|
||||||
const payload: NewTransactionPayload = {
|
const payload: NewTransactionPayload = {
|
||||||
paymentId: payment.id,
|
paymentId: payment.id,
|
||||||
status: paymentStatus,
|
|
||||||
serviceLineTransactions: [
|
serviceLineTransactions: [
|
||||||
{
|
{
|
||||||
serviceLineId: formState.serviceLineId,
|
serviceLineId: formState.serviceLineId,
|
||||||
@@ -213,7 +212,6 @@ export default function PaymentEditModal({
|
|||||||
|
|
||||||
const payload: NewTransactionPayload = {
|
const payload: NewTransactionPayload = {
|
||||||
paymentId: payment.id,
|
paymentId: payment.id,
|
||||||
status: paymentStatus,
|
|
||||||
serviceLineTransactions: [
|
serviceLineTransactions: [
|
||||||
{
|
{
|
||||||
serviceLineId: line.id,
|
serviceLineId: line.id,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
} 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";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { ConfirmationDialog } from "../ui/confirmationDialog";
|
||||||
|
|
||||||
interface PaymentApiResponse {
|
interface PaymentApiResponse {
|
||||||
payments: PaymentWithExtras[];
|
payments: PaymentWithExtras[];
|
||||||
@@ -81,6 +82,9 @@ export default function PaymentsRecentTable({
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isRevertOpen, setIsRevertOpen] = useState(false);
|
||||||
|
const [revertPaymentId, setRevertPaymentId] = useState<number | null>(null);
|
||||||
|
|
||||||
const handleSelectPayment = (payment: PaymentWithExtras) => {
|
const handleSelectPayment = (payment: PaymentWithExtras) => {
|
||||||
const isSelected = selectedPaymentId === payment.id;
|
const isSelected = selectedPaymentId === payment.id;
|
||||||
const newSelectedId = isSelected ? null : payment.id;
|
const newSelectedId = isSelected ? null : payment.id;
|
||||||
@@ -210,6 +214,57 @@ export default function PaymentsRecentTable({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fullPaymentMutation = useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
paymentId,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
paymentId: number;
|
||||||
|
type: "pay" | "revert";
|
||||||
|
}) => {
|
||||||
|
const endpoint =
|
||||||
|
type === "pay"
|
||||||
|
? `/api/payments/${paymentId}/pay-absolute-full-claim`
|
||||||
|
: `/api/payments/${paymentId}/revert-full-claim`;
|
||||||
|
const response = await apiRequest("PUT", endpoint);
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || "Failed to update Payment");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Payment updated successfully!",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: getPaymentsQueryKey() });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Operation failed: ${error.message}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePayAbsoluteFullDue = (paymentId: number) => {
|
||||||
|
fullPaymentMutation.mutate({ paymentId, type: "pay" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevert = () => {
|
||||||
|
if (!revertPaymentId) return;
|
||||||
|
|
||||||
|
fullPaymentMutation.mutate({
|
||||||
|
paymentId: revertPaymentId,
|
||||||
|
type: "revert",
|
||||||
|
});
|
||||||
|
|
||||||
|
setRevertPaymentId(null);
|
||||||
|
setIsRevertOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
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}`);
|
||||||
@@ -543,6 +598,25 @@ export default function PaymentsRecentTable({
|
|||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{/* Pay Full Due */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePayAbsoluteFullDue(payment.id)}
|
||||||
|
>
|
||||||
|
Pay Full Due
|
||||||
|
</Button>
|
||||||
|
{/* Revert Full Due */}
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setRevertPaymentId(payment.id);
|
||||||
|
setIsRevertOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revert Full Due
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -553,6 +627,17 @@ export default function PaymentsRecentTable({
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Revert Confirmation Dialog */}
|
||||||
|
<ConfirmationDialog
|
||||||
|
isOpen={isRevertOpen}
|
||||||
|
title="Confirm Revert"
|
||||||
|
message={`Do you want to revert all Service Line payments for Payment ID: ${revertPaymentId}?`}
|
||||||
|
confirmLabel="Revert"
|
||||||
|
confirmColor="bg-yellow-600 hover:bg-yellow-700"
|
||||||
|
onConfirm={handleRevert}
|
||||||
|
onCancel={() => setIsRevertOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
<DeleteConfirmationDialog
|
||||||
isOpen={isDeletePaymentOpen}
|
isOpen={isDeletePaymentOpen}
|
||||||
onConfirm={handleConfirmDeletePayment}
|
onConfirm={handleConfirmDeletePayment}
|
||||||
|
|||||||
44
apps/Frontend/src/components/ui/confirmationDialog.tsx
Normal file
44
apps/Frontend/src/components/ui/confirmationDialog.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export const ConfirmationDialog = ({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmLabel = "Confirm",
|
||||||
|
cancelLabel = "Cancel",
|
||||||
|
confirmColor = "bg-blue-600 hover:bg-blue-700",
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
message: string | React.ReactNode;
|
||||||
|
confirmLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
confirmColor?: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
|
<div className="bg-white p-6 rounded-md shadow-md w-[90%] max-w-md">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">{title}</h2>
|
||||||
|
<p>{message}</p>
|
||||||
|
<div className="mt-6 flex justify-end space-x-4">
|
||||||
|
<button
|
||||||
|
className="bg-gray-200 px-4 py-2 rounded hover:bg-gray-300"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${confirmColor} text-white px-4 py-2 rounded`}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -82,7 +82,6 @@ export type PaymentWithExtras = Prisma.PaymentGetPayload<{
|
|||||||
|
|
||||||
export const newTransactionPayloadSchema = z.object({
|
export const newTransactionPayloadSchema = z.object({
|
||||||
paymentId: z.number(),
|
paymentId: z.number(),
|
||||||
status: PaymentStatusSchema,
|
|
||||||
serviceLineTransactions: z.array(
|
serviceLineTransactions: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
serviceLineId: z.number(),
|
serviceLineId: z.number(),
|
||||||
|
|||||||
Reference in New Issue
Block a user