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

@@ -21,6 +21,7 @@ import {
paymentStatusArray,
paymentMethodArray,
} from "@repo/db/types";
import { NpiProvider } from "@repo/db/types";
import {
Select,
SelectContent,
@@ -33,7 +34,7 @@ import { toast } from "@/hooks/use-toast";
import { X } from "lucide-react";
import { DateInput } from "@/components/ui/dateInput";
import { apiRequest } from "@/lib/queryClient";
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
type PaymentEditModalProps = {
isOpen: boolean;
@@ -84,6 +85,57 @@ export default function PaymentEditModal({
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(
(paymentProp ?? null)?.status ?? ("PENDING" as PaymentStatus)
);
const [selectedNpiProviderId, setSelectedNpiProviderId] = useState<number | null>(
(paymentProp ?? null)?.npiProviderId ?? null
);
const [isUpdatingProvider, setIsUpdatingProvider] = useState(false);
// Fetch all NPI providers
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) if no provider set yet
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]);
// Sync provider when payment changes (e.g. fetched from API)
useEffect(() => {
if (payment?.npiProviderId !== undefined) {
setSelectedNpiProviderId(payment.npiProviderId ?? null);
}
}, [payment?.id]);
const handleUpdateProvider = async () => {
if (!payment) return;
setIsUpdatingProvider(true);
try {
const res = await apiRequest("PATCH", `/api/payments/${payment.id}/provider`, {
npiProviderId: selectedNpiProviderId,
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || "Failed to update provider");
}
toast({ title: "Success", description: "Provider updated successfully." });
await refetchPayment(payment.id);
} catch (err: any) {
toast({ title: "Error", description: err?.message ?? "Failed to update provider.", variant: "destructive" });
} finally {
setIsUpdatingProvider(false);
}
};
const [formState, setFormState] = useState(() => {
return {
@@ -588,6 +640,36 @@ export default function PaymentEditModal({
>
{isUpdatingStatus ? "Updating..." : "Update Status"}
</Button>
{/* Provider Selector */}
<div className="pt-3">
<label className="block text-sm text-gray-600 mb-1">
Rendering Provider
</label>
<Select
value={selectedNpiProviderId?.toString() ?? ""}
onValueChange={(val) => setSelectedNpiProviderId(Number(val))}
>
<SelectTrigger>
<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>
<Button
size="sm"
disabled={isUpdatingProvider}
onClick={handleUpdateProvider}
>
{isUpdatingProvider ? "Updating..." : "Update Provider"}
</Button>
</div>
</div>
@@ -607,6 +689,23 @@ export default function PaymentEditModal({
? formatDateToHumanReadable(payment.updatedAt)
: "N/A"}
</p>
{(payment as any).commissionBatchItems?.length > 0 ? (
<div className="pt-2">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800">
Commissioned
</span>
<p className="mt-1 text-xs text-gray-500">
Paid as commission on{" "}
{formatDateToHumanReadable(
(payment as any).commissionBatchItems[0].commissionBatch.createdAt
)}
</p>
</div>
) : (
<p className="pt-1">
<span className="text-gray-400 text-xs">Not yet commissioned</span>
</p>
)}
</div>
</div>
</div>