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:
co-authored by
Claude Sonnet 4.6
parent
25a20e8a16
commit
7360b1930b
@@ -14,8 +14,10 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
||||
import React, { useState } from "react";
|
||||
import { ClaimStatus, ClaimWithServiceLines } from "@repo/db/types";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ClaimStatus, ClaimWithServiceLines, NpiProvider } from "@repo/db/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import {
|
||||
safeParseMissingTeeth,
|
||||
splitTeeth,
|
||||
@@ -41,6 +43,28 @@ export default function ClaimEditModal({
|
||||
const [status, setStatus] = useState<ClaimStatus>(
|
||||
claim?.status ?? ("PENDING" as ClaimStatus)
|
||||
);
|
||||
const [selectedNpiProviderId, setSelectedNpiProviderId] = useState<number | null>(
|
||||
(claim as any)?.npiProviderId ?? null
|
||||
);
|
||||
|
||||
const { data: npiProviders = [] } = useQuery<NpiProvider[]>({
|
||||
queryKey: ["/api/npiProviders/"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/npiProviders/");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
// Default to Mary Scannell (or first provider) only when no provider is set
|
||||
useEffect(() => {
|
||||
if (!npiProviders.length) return;
|
||||
if (selectedNpiProviderId !== null) return;
|
||||
const mary = npiProviders.find((p) =>
|
||||
p.providerName.toLowerCase().includes("mary scannell")
|
||||
);
|
||||
const fallback = mary ?? npiProviders[0];
|
||||
if (fallback) setSelectedNpiProviderId(fallback.id);
|
||||
}, [npiProviders]);
|
||||
|
||||
if (!claim) return null;
|
||||
|
||||
@@ -48,7 +72,9 @@ export default function ClaimEditModal({
|
||||
const updatedClaim: ClaimWithServiceLines = {
|
||||
...claim,
|
||||
status,
|
||||
};
|
||||
npiProviderId: selectedNpiProviderId,
|
||||
npiProvider: npiProviders.find((p) => p.id === selectedNpiProviderId) ?? null,
|
||||
} as ClaimWithServiceLines;
|
||||
|
||||
onSave(updatedClaim);
|
||||
onOpenChange(false);
|
||||
@@ -109,6 +135,25 @@ export default function ClaimEditModal({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-gray-500">Rendering Provider:</span>
|
||||
<Select
|
||||
value={selectedNpiProviderId?.toString() ?? ""}
|
||||
onValueChange={(val) => setSelectedNpiProviderId(Number(val))}
|
||||
>
|
||||
<SelectTrigger className="mt-1 w-full">
|
||||
<SelectValue placeholder="Select Provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{npiProviders.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id.toString()}>
|
||||
{p.npiNumber} — {p.providerName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -788,6 +788,10 @@ export function ClaimForm({
|
||||
mimeType: f.type,
|
||||
}));
|
||||
|
||||
const selectedNpiProviderId = npiProvider?.npiNumber
|
||||
? npiProviders.find((p) => p.npiNumber === npiProvider.npiNumber)?.id ?? null
|
||||
: null;
|
||||
|
||||
const createdClaim = await onSubmit({
|
||||
...formToCreateClaim,
|
||||
serviceLines: filteredServiceLines,
|
||||
@@ -796,6 +800,7 @@ export function ClaimForm({
|
||||
insuranceProvider: "MassHealth",
|
||||
appointmentId: appointmentIdToUse!,
|
||||
claimFiles: claimFilesMeta,
|
||||
...(selectedNpiProviderId ? { npiProviderId: selectedNpiProviderId } : {}),
|
||||
});
|
||||
|
||||
// 4. sending form data to selenium service
|
||||
|
||||
@@ -136,6 +136,9 @@ export default function ClaimsRecentTable({
|
||||
mutationFn: async (claim: ClaimWithServiceLines) => {
|
||||
const response = await apiRequest("PUT", `/api/claims/${claim.id}`, {
|
||||
status: claim.status,
|
||||
...((claim as any).npiProviderId != null
|
||||
? { npiProviderId: (claim as any).npiProviderId }
|
||||
: {}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
@@ -315,6 +318,7 @@ export default function ClaimsRecentTable({
|
||||
<TableHead>Total Billed</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Attachments</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -448,6 +452,12 @@ export default function ClaimsRecentTable({
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="text-sm text-gray-900">
|
||||
{claim.npiProvider?.providerName ?? "—"}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end space-x-2">
|
||||
{allowDelete && (
|
||||
|
||||
Reference in New Issue
Block a user