feat: add Copayment and Adjustment columns to payments table

- Added copayment and adjustment fields (Decimal, default 0) to Payment
  model in schema and directly to DB via ALTER TABLE
- Added PATCH /api/payments/:id/copayment and /adjustment routes
- Added inline-editable Copayment and Adjustment columns after MH Paid
  with same click-to-edit format; Copayment in blue, Adjustment in orange

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-06 21:14:00 -04:00
parent c5af6c1fa6
commit 4bd501250d
250 changed files with 4656 additions and 185 deletions

View File

@@ -455,6 +455,60 @@ router.patch(
}
);
// PATCH /api/payments/:id/copayment
router.patch(
"/:id/copayment",
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 val = parseFloat(req.body.copayment);
if (isNaN(val) || val < 0) {
return res.status(400).json({ message: "Invalid copayment value" });
}
const updated = await prisma.payment.update({
where: { id: paymentId },
data: { copayment: val, updatedById: userId },
});
return res.json({ ...updated, copayment: Number(updated.copayment) });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Failed to update copayment";
return res.status(500).json({ message });
}
}
);
// PATCH /api/payments/:id/adjustment
router.patch(
"/:id/adjustment",
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 val = parseFloat(req.body.adjustment);
if (isNaN(val) || val < 0) {
return res.status(400).json({ message: "Invalid adjustment value" });
}
const updated = await prisma.payment.update({
where: { id: paymentId },
data: { adjustment: val, updatedById: userId },
});
return res.json({ ...updated, adjustment: Number(updated.adjustment) });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Failed to update adjustment";
return res.status(500).json({ message });
}
}
);
// PATCH /api/payments/:id/mh-payment-check
router.patch(
"/:id/mh-payment-check",

View File

@@ -101,6 +101,10 @@ export default function PaymentsRecentTable({
const [isMhChecking, setIsMhChecking] = useState(false);
const [editingMhPaidId, setEditingMhPaidId] = useState<number | null>(null);
const [editingMhPaidValue, setEditingMhPaidValue] = useState<string>("");
const [editingCopaymentId, setEditingCopaymentId] = useState<number | null>(null);
const [editingCopaymentValue, setEditingCopaymentValue] = useState<string>("");
const [editingAdjustmentId, setEditingAdjustmentId] = useState<number | null>(null);
const [editingAdjustmentValue, setEditingAdjustmentValue] = useState<string>("");
const [isRevertOpen, setIsRevertOpen] = useState(false);
const [revertPaymentId, setRevertPaymentId] = useState<number | null>(null);
@@ -591,6 +595,8 @@ export default function PaymentsRecentTable({
<TableHead>Status</TableHead>
<TableHead>Attachments</TableHead>
<TableHead>MH Paid</TableHead>
<TableHead>Copayment</TableHead>
<TableHead>Adjustment</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead>Payment ID</TableHead>
<TableHead>Claim ID</TableHead>
@@ -844,6 +850,98 @@ export default function PaymentsRecentTable({
)}
</TableCell>
{/* Copayment */}
<TableCell>
{editingCopaymentId === payment.id ? (
<input
type="number"
min="0"
step="0.01"
autoFocus
className="w-24 border border-blue-400 rounded px-1 py-0.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
value={editingCopaymentValue}
onChange={(e) => setEditingCopaymentValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") e.currentTarget.blur();
else if (e.key === "Escape") setEditingCopaymentId(null);
}}
onBlur={async () => {
const val = parseFloat(editingCopaymentValue);
if (!isNaN(val) && val >= 0) {
try {
const res = await apiRequest("PATCH", `/api/payments/${payment.id}/copayment`, { copayment: val });
if (res.ok) {
await queryClient.invalidateQueries({ queryKey: QK_PAYMENTS_RECENT_BASE });
} else {
toast({ title: "Error", description: "Failed to save copayment.", variant: "destructive" });
}
} catch {
toast({ title: "Error", description: "Failed to save copayment.", variant: "destructive" });
}
}
setEditingCopaymentId(null);
}}
/>
) : (
<span
className="text-sm font-medium text-blue-700 cursor-pointer hover:underline hover:text-blue-900"
title="Click to edit"
onClick={() => {
setEditingCopaymentId(payment.id);
setEditingCopaymentValue(Number(payment.copayment ?? 0).toFixed(2));
}}
>
${Number(payment.copayment ?? 0).toFixed(2)}
</span>
)}
</TableCell>
{/* Adjustment */}
<TableCell>
{editingAdjustmentId === payment.id ? (
<input
type="number"
min="0"
step="0.01"
autoFocus
className="w-24 border border-blue-400 rounded px-1 py-0.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
value={editingAdjustmentValue}
onChange={(e) => setEditingAdjustmentValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") e.currentTarget.blur();
else if (e.key === "Escape") setEditingAdjustmentId(null);
}}
onBlur={async () => {
const val = parseFloat(editingAdjustmentValue);
if (!isNaN(val) && val >= 0) {
try {
const res = await apiRequest("PATCH", `/api/payments/${payment.id}/adjustment`, { adjustment: val });
if (res.ok) {
await queryClient.invalidateQueries({ queryKey: QK_PAYMENTS_RECENT_BASE });
} else {
toast({ title: "Error", description: "Failed to save adjustment.", variant: "destructive" });
}
} catch {
toast({ title: "Error", description: "Failed to save adjustment.", variant: "destructive" });
}
}
setEditingAdjustmentId(null);
}}
/>
) : (
<span
className="text-sm font-medium text-orange-700 cursor-pointer hover:underline hover:text-orange-900"
title="Click to edit"
onClick={() => {
setEditingAdjustmentId(payment.id);
setEditingAdjustmentValue(Number(payment.adjustment ?? 0).toFixed(2));
}}
>
${Number(payment.adjustment ?? 0).toFixed(2)}
</span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
{allowDelete && (