Files
DentalManagementMH07/apps/Frontend/src/components/settings/npiProviderTable.tsx
Gitead 75c49ab1df feat: chatbot rendering provider override and NPI provider ordering
- AI chat extracts 'with provider <name>' and routes claim to that provider
- Claim form reads provider from sessionStorage before any async effects run,
  preventing saved claim/procedure data from overriding the chatbot selection
- NPI provider settings table shows Provider #1 / #2 labels with up/down
  reorder buttons; Provider #1 is always the default for claims
- Default provider now uses sortOrder instead of hardcoded 'Mary Scannell'
- Added sortOrder column to NpiProvider schema with migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:17:05 -04:00

236 lines
8.5 KiB
TypeScript
Executable File

import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { Button } from "../ui/button";
import { Edit, Delete, Plus, ChevronUp, ChevronDown } from "lucide-react";
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import { NpiProviderForm } from "./npiProviderForm";
type NpiProvider = {
id: number;
npiNumber: string;
providerName: string;
sortOrder: number;
};
export function NpiProviderTable() {
const queryClient = useQueryClient();
const [currentPage, setCurrentPage] = useState(1);
const [modalOpen, setModalOpen] = useState(false);
const [editingProvider, setEditingProvider] = useState<NpiProvider | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [providerToDelete, setProviderToDelete] = useState<NpiProvider | null>(null);
const providersPerPage = 5;
const { data: providers = [], isLoading, error } = useQuery({
queryKey: ["/api/npiProviders/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/npiProviders/");
if (!res.ok) throw new Error("Failed to fetch NPI providers");
return res.json() as Promise<NpiProvider[]>;
},
});
const reorderMutation = useMutation({
mutationFn: async (orderedIds: number[]) => {
const res = await apiRequest("POST", "/api/npiProviders/reorder", { orderedIds });
if (!res.ok) throw new Error("Failed to reorder NPI providers");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/npiProviders/"] });
},
});
const deleteMutation = useMutation({
mutationFn: async (provider: NpiProvider) => {
const res = await apiRequest("DELETE", `/api/npiProviders/${provider.id}`);
if (!res.ok) throw new Error("Failed to delete NPI provider");
return true;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/npiProviders/"] });
},
});
const handleMove = (index: number, direction: "up" | "down") => {
const swapIndex = direction === "up" ? index - 1 : index + 1;
if (swapIndex < 0 || swapIndex >= providers.length) return;
const newOrder = [...providers];
[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex]!, newOrder[index]!];
reorderMutation.mutate(newOrder.map((p) => p.id));
};
const handleDeleteClick = (provider: NpiProvider) => {
setProviderToDelete(provider);
setIsDeleteDialogOpen(true);
};
const handleConfirmDelete = () => {
if (!providerToDelete) return;
deleteMutation.mutate(providerToDelete, {
onSuccess: () => {
setIsDeleteDialogOpen(false);
setProviderToDelete(null);
},
});
};
const indexOfLast = currentPage * providersPerPage;
const indexOfFirst = indexOfLast - providersPerPage;
const currentProviders = providers.slice(indexOfFirst, indexOfLast);
const totalPages = Math.ceil(providers.length / providersPerPage);
return (
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="flex justify-between items-center p-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">NPI Providers</h2>
<Button
onClick={() => {
setEditingProvider(null);
setModalOpen(true);
}}
>
<Plus className="mr-2 h-4 w-4" /> Add NPI Providers
</Button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase w-28">
Provider #
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
NPI Number
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Provider Name
</th>
<th className="px-4 py-2 w-32" />
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{isLoading ? (
<tr>
<td colSpan={4} className="text-center py-4">
Loading NPI providers...
</td>
</tr>
) : error ? (
<tr>
<td colSpan={4} className="text-center py-4 text-red-600">
Error loading NPI providers
</td>
</tr>
) : currentProviders.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-4">
No NPI providers found.
</td>
</tr>
) : (
currentProviders.map((provider, pageIndex) => {
const globalIndex = indexOfFirst + pageIndex;
const isDefault = globalIndex === 0;
return (
<tr key={provider.id} className={isDefault ? "bg-blue-50" : ""}>
<td className="px-4 py-2">
<span
className={`inline-flex items-center gap-1 text-sm font-medium ${
isDefault ? "text-blue-700" : "text-gray-600"
}`}
>
Provider #{globalIndex + 1}
{isDefault && (
<span className="ml-1 text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">
default
</span>
)}
</span>
</td>
<td className="px-4 py-2 text-sm">{provider.npiNumber}</td>
<td className="px-4 py-2 text-sm">{provider.providerName}</td>
<td className="px-4 py-2 text-right">
<Button
variant="ghost"
size="sm"
disabled={globalIndex === 0 || reorderMutation.isPending}
onClick={() => handleMove(globalIndex, "up")}
title="Move up"
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
disabled={globalIndex === providers.length - 1 || reorderMutation.isPending}
onClick={() => handleMove(globalIndex, "down")}
title="Move down"
>
<ChevronDown className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingProvider(provider);
setModalOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteClick(provider)}
>
<Delete className="h-4 w-4 text-red-600" />
</Button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{providers.length > providersPerPage && (
<div className="px-4 py-3 border-t flex justify-between">
<Button
variant="ghost"
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => p - 1)}
>
Previous
</Button>
<Button
variant="ghost"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((p) => p + 1)}
>
Next
</Button>
</div>
)}
{modalOpen && (
<NpiProviderForm
defaultValues={editingProvider || undefined}
onClose={() => setModalOpen(false)}
/>
)}
<DeleteConfirmationDialog
isOpen={isDeleteDialogOpen}
onConfirm={handleConfirmDelete}
onCancel={() => setIsDeleteDialogOpen(false)}
entityName={providerToDelete?.providerName}
/>
</div>
);
}