feat: add provider column, commission tracking, and report provider filter

- Claims & Payments: save npiProviderId when submitting MH claim; sync between claim and payment on update
- Claims table: add Provider column showing rendering provider name
- Payments table: add Provider column + purple Commissioned badge on status
- Claim edit modal: add Rendering Provider dropdown (defaults to Mary Scannell)
- Payment edit modal: add Rendering Provider dropdown + Commissioned metadata display
- Reports page: add Provider filter dropdown (dynamic from NPI providers settings)
- Reports page: remove Collections by Doctor report type and Select Doctor dropdown
- Commission section: new section in reports page with date range + provider filter, shows eligible paid claims/payments per provider, multi-select checkboxes, Pay Commission modal with print + save, marks payments as commissioned so they are excluded from future cycles
- DB: add CommissionBatch and CommissionBatchItem tables; backfill Payment.npiProviderId from linked claims
- Backend: PATCH /api/payments/:id/provider syncs to linked claim; PUT /api/claims/:id syncs to linked payment; new /api/commissions routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-15 23:51:39 -04:00
parent 25a20e8a16
commit 7360b1930b
366 changed files with 10822 additions and 388 deletions

View File

@@ -200,10 +200,17 @@ router.post("/:claimId", async (req: Request, res: Response): Promise<any> => {
const claimId = parseIntOrError(req.params.claimId, "Claim ID");
// Inherit npiProviderId from the linked claim so commission queries work
const linkedClaim = await prisma.claim.findUnique({
where: { id: claimId },
select: { npiProviderId: true },
});
const validated = insertPaymentSchema.safeParse({
...req.body,
claimId,
userId,
...(linkedClaim?.npiProviderId ? { npiProviderId: linkedClaim.npiProviderId } : {}),
});
if (!validated.success) {
@@ -427,6 +434,48 @@ router.patch(
}
);
// PATCH /api/payments/:id/provider
router.patch(
"/:id/provider",
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 { npiProviderId } = req.body;
const existing = await storage.getPayment(paymentId);
if (!existing) return res.status(404).json({ message: "Payment not found" });
// Update payment and linked claim atomically so the claims form
// picks up the new provider when it prefills from the existing claim.
const ops: Parameters<typeof prisma.$transaction>[0] = [
prisma.payment.update({
where: { id: paymentId },
data: { npiProviderId: npiProviderId ?? null, updatedById: userId },
include: { npiProvider: true },
}),
];
if (existing.claimId) {
ops.push(
prisma.claim.update({
where: { id: existing.claimId },
data: { npiProviderId: npiProviderId ?? null },
}) as any
);
}
const [updated] = await prisma.$transaction(ops);
return res.json(updated);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Failed to update provider";
return res.status(500).json({ message });
}
}
);
// PATCH /api/payments/:id/mh-paid-amount
router.patch(
"/:id/mh-paid-amount",