npiProvider - v1
This commit is contained in:
@@ -4,6 +4,7 @@ import appointmentsRoutes from "./appointments";
|
|||||||
import appointmentProceduresRoutes from "./appointments-procedures";
|
import appointmentProceduresRoutes from "./appointments-procedures";
|
||||||
import usersRoutes from "./users";
|
import usersRoutes from "./users";
|
||||||
import staffsRoutes from "./staffs";
|
import staffsRoutes from "./staffs";
|
||||||
|
import npiProvidersRoutes from "./npiProviders";
|
||||||
import claimsRoutes from "./claims";
|
import claimsRoutes from "./claims";
|
||||||
import patientDataExtractionRoutes from "./patientDataExtraction";
|
import patientDataExtractionRoutes from "./patientDataExtraction";
|
||||||
import insuranceCredsRoutes from "./insuranceCreds";
|
import insuranceCredsRoutes from "./insuranceCreds";
|
||||||
@@ -25,6 +26,7 @@ router.use("/appointments", appointmentsRoutes);
|
|||||||
router.use("/appointment-procedures", appointmentProceduresRoutes);
|
router.use("/appointment-procedures", appointmentProceduresRoutes);
|
||||||
router.use("/users", usersRoutes);
|
router.use("/users", usersRoutes);
|
||||||
router.use("/staffs", staffsRoutes);
|
router.use("/staffs", staffsRoutes);
|
||||||
|
router.use("/npiProviders", npiProvidersRouter);
|
||||||
router.use("/patientDataExtraction", patientDataExtractionRoutes);
|
router.use("/patientDataExtraction", patientDataExtractionRoutes);
|
||||||
router.use("/claims", claimsRoutes);
|
router.use("/claims", claimsRoutes);
|
||||||
router.use("/insuranceCreds", insuranceCredsRoutes);
|
router.use("/insuranceCreds", insuranceCredsRoutes);
|
||||||
|
|||||||
101
apps/Backend/src/routes/npiProviders.ts
Normal file
101
apps/Backend/src/routes/npiProviders.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { npiProviderStorage } from "../storage/npiProviders";
|
||||||
|
import { insertNpiProviderSchema } from "@repo/db/types";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
if (!req.user?.id) {
|
||||||
|
return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers = await npiProviderStorage.getNpiProvidersByUser(
|
||||||
|
req.user.id,
|
||||||
|
);
|
||||||
|
res.status(200).json(providers);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Failed to fetch NPI providers",
|
||||||
|
details: String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
if (!req.user?.id) {
|
||||||
|
return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = insertNpiProviderSchema.safeParse({
|
||||||
|
...req.body,
|
||||||
|
userId: req.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
const flat = parsed.error.flatten();
|
||||||
|
const firstError =
|
||||||
|
Object.values(flat.fieldErrors)[0]?.[0] || "Invalid input";
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
message: firstError,
|
||||||
|
details: flat.fieldErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = await npiProviderStorage.createNpiProvider(parsed.data);
|
||||||
|
res.status(201).json(provider);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code === "P2002") {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: "This NPI already exists for the user",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Failed to create NPI provider",
|
||||||
|
details: String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put("/:id", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (isNaN(id)) return res.status(400).send("Invalid ID");
|
||||||
|
|
||||||
|
const provider = await npiProviderStorage.updateNpiProvider(id, req.body);
|
||||||
|
res.status(200).json(provider);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Failed to update NPI provider",
|
||||||
|
details: String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/:id", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
if (!req.user?.id) {
|
||||||
|
return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
if (isNaN(id)) return res.status(400).send("Invalid ID");
|
||||||
|
|
||||||
|
const ok = await npiProviderStorage.deleteNpiProvider(req.user.id, id);
|
||||||
|
if (!ok) {
|
||||||
|
return res.status(404).json({ message: "NPI provider not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Failed to delete NPI provider",
|
||||||
|
details: String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -5,6 +5,7 @@ import { patientsStorage } from './patients-storage';
|
|||||||
import { appointmentsStorage } from './appointments-storage';
|
import { appointmentsStorage } from './appointments-storage';
|
||||||
import { appointmentProceduresStorage } from './appointment-procedures-storage';
|
import { appointmentProceduresStorage } from './appointment-procedures-storage';
|
||||||
import { staffStorage } from './staff-storage';
|
import { staffStorage } from './staff-storage';
|
||||||
|
import { npiProviderStorage } from './npi-providers-storage';
|
||||||
import { claimsStorage } from './claims-storage';
|
import { claimsStorage } from './claims-storage';
|
||||||
import { insuranceCredsStorage } from './insurance-creds-storage';
|
import { insuranceCredsStorage } from './insurance-creds-storage';
|
||||||
import { generalPdfStorage } from './general-pdf-storage';
|
import { generalPdfStorage } from './general-pdf-storage';
|
||||||
@@ -22,6 +23,7 @@ export const storage = {
|
|||||||
...appointmentsStorage,
|
...appointmentsStorage,
|
||||||
...appointmentProceduresStorage,
|
...appointmentProceduresStorage,
|
||||||
...staffStorage,
|
...staffStorage,
|
||||||
|
...npiProviderStorage,
|
||||||
...claimsStorage,
|
...claimsStorage,
|
||||||
...insuranceCredsStorage,
|
...insuranceCredsStorage,
|
||||||
...generalPdfStorage,
|
...generalPdfStorage,
|
||||||
|
|||||||
50
apps/Backend/src/storage/npi-providers-storage.ts
Normal file
50
apps/Backend/src/storage/npi-providers-storage.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
import { InsertNpiProvider, NpiProvider } from "@repo/db/types";
|
||||||
|
|
||||||
|
export interface INpiProviderStorage {
|
||||||
|
getNpiProvider(id: number): Promise<NpiProvider | null>;
|
||||||
|
getNpiProvidersByUser(userId: number): Promise<NpiProvider[]>;
|
||||||
|
createNpiProvider(data: InsertNpiProvider): Promise<NpiProvider>;
|
||||||
|
updateNpiProvider(
|
||||||
|
id: number,
|
||||||
|
updates: Partial<NpiProvider>,
|
||||||
|
): Promise<NpiProvider | null>;
|
||||||
|
deleteNpiProvider(userId: number, id: number): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const npiProviderStorage: INpiProviderStorage = {
|
||||||
|
async getNpiProvider(id: number) {
|
||||||
|
return db.npiProvider.findUnique({ where: { id } });
|
||||||
|
},
|
||||||
|
|
||||||
|
async getNpiProvidersByUser(userId: number) {
|
||||||
|
return db.npiProvider.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async createNpiProvider(data: InsertNpiProvider) {
|
||||||
|
return db.npiProvider.create({
|
||||||
|
data: data as NpiProvider,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateNpiProvider(id: number, updates: Partial<NpiProvider>) {
|
||||||
|
return db.npiProvider.update({
|
||||||
|
where: { id },
|
||||||
|
data: updates,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteNpiProvider(userId: number, id: number) {
|
||||||
|
try {
|
||||||
|
await db.npiProvider.delete({
|
||||||
|
where: { id, userId },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
149
apps/Frontend/src/components/settings/npiProviderForm.tsx
Normal file
149
apps/Frontend/src/components/settings/npiProviderForm.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
defaultValues?: {
|
||||||
|
id?: number;
|
||||||
|
npiNumber: string;
|
||||||
|
providerName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NpiProviderForm({ onClose, defaultValues }: Props) {
|
||||||
|
const [npiNumber, setNpiNumber] = useState(
|
||||||
|
defaultValues?.npiNumber || ""
|
||||||
|
);
|
||||||
|
const [providerName, setProviderName] = useState(
|
||||||
|
defaultValues?.providerName || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const payload = {
|
||||||
|
npiNumber: npiNumber.trim(),
|
||||||
|
providerName: providerName.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = defaultValues?.id
|
||||||
|
? `/api/npiProviders/${defaultValues.id}`
|
||||||
|
: "/api/npiProviders/";
|
||||||
|
|
||||||
|
const method = defaultValues?.id ? "PUT" : "POST";
|
||||||
|
|
||||||
|
const res = await apiRequest(method, url, payload);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error(err?.message || "Failed to save NPI provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: `NPI provider ${
|
||||||
|
defaultValues?.id ? "updated" : "created"
|
||||||
|
}.`,
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["/api/npiProviders/"],
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNpiNumber(defaultValues?.npiNumber || "");
|
||||||
|
setProviderName(defaultValues?.providerName || "");
|
||||||
|
}, [defaultValues]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!npiNumber || !providerName) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "All fields are required.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md shadow-lg">
|
||||||
|
<h2 className="text-lg font-bold mb-4">
|
||||||
|
{defaultValues?.id
|
||||||
|
? "Edit NPI Provider"
|
||||||
|
: "Create NPI Provider"}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">
|
||||||
|
NPI Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={npiNumber}
|
||||||
|
onChange={(e) => setNpiNumber(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full"
|
||||||
|
placeholder="e.g., 1457649092"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">
|
||||||
|
Provider Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={providerName}
|
||||||
|
onChange={(e) => setProviderName(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full"
|
||||||
|
placeholder="e.g., Kai Gao"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-600 hover:underline"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{mutation.isPending
|
||||||
|
? defaultValues?.id
|
||||||
|
? "Updating..."
|
||||||
|
: "Creating..."
|
||||||
|
: defaultValues?.id
|
||||||
|
? "Update"
|
||||||
|
: "Create"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
199
apps/Frontend/src/components/settings/npiProviderTable.tsx
Normal file
199
apps/Frontend/src/components/settings/npiProviderTable.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
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 } from "lucide-react";
|
||||||
|
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
|
||||||
|
import { NpiProviderForm } from "./npiProviderForm";
|
||||||
|
|
||||||
|
type NpiProvider = {
|
||||||
|
id: number;
|
||||||
|
npiNumber: string;
|
||||||
|
providerName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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 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 Provider
|
||||||
|
</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">
|
||||||
|
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" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-center py-4">
|
||||||
|
Loading NPI providers...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : error ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-center py-4 text-red-600">
|
||||||
|
Error loading NPI providers
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : currentProviders.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-center py-4">
|
||||||
|
No NPI providers found.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
currentProviders.map((provider) => (
|
||||||
|
<tr key={provider.id}>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
{provider.npiNumber}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
{provider.providerName}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-right">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
|||||||
import { CredentialTable } from "@/components/settings/insuranceCredTable";
|
import { CredentialTable } from "@/components/settings/insuranceCredTable";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { Staff } from "@repo/db/types";
|
import { Staff } from "@repo/db/types";
|
||||||
|
import { NpiProviderTable } from "@/components/settings/npiProviderTable";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -190,7 +190,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
const [isDeleteStaffOpen, setIsDeleteStaffOpen] = useState(false);
|
const [isDeleteStaffOpen, setIsDeleteStaffOpen] = useState(false);
|
||||||
const [currentStaff, setCurrentStaff] = useState<Staff | undefined>(
|
const [currentStaff, setCurrentStaff] = useState<Staff | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteStaff = (staff: Staff) => {
|
const handleDeleteStaff = (staff: Staff) => {
|
||||||
@@ -212,7 +212,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
const handleViewStaff = (staff: Staff) =>
|
const handleViewStaff = (staff: Staff) =>
|
||||||
alert(
|
alert(
|
||||||
`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`
|
`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// MANAGE USER
|
// MANAGE USER
|
||||||
@@ -229,7 +229,7 @@ export default function SettingsPage() {
|
|||||||
//update user mutation
|
//update user mutation
|
||||||
const updateUserMutate = useMutation({
|
const updateUserMutate = useMutation({
|
||||||
mutationFn: async (
|
mutationFn: async (
|
||||||
updates: Partial<{ username: string; password: string }>
|
updates: Partial<{ username: string; password: string }>,
|
||||||
) => {
|
) => {
|
||||||
if (!user?.id) throw new Error("User not loaded");
|
if (!user?.id) throw new Error("User not loaded");
|
||||||
const res = await apiRequest("PUT", `/api/users/${user.id}`, updates);
|
const res = await apiRequest("PUT", `/api/users/${user.id}`, updates);
|
||||||
@@ -258,110 +258,113 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<StaffTable
|
<StaffTable
|
||||||
staff={staff}
|
staff={staff}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
onAdd={openAddStaffModal}
|
onAdd={openAddStaffModal}
|
||||||
onEdit={openEditStaffModal}
|
onEdit={openEditStaffModal}
|
||||||
onDelete={handleDeleteStaff}
|
onDelete={handleDeleteStaff}
|
||||||
onView={handleViewStaff}
|
onView={handleViewStaff}
|
||||||
/>
|
/>
|
||||||
{isError && (
|
{isError && (
|
||||||
<p className="mt-4 text-red-600">
|
<p className="mt-4 text-red-600">
|
||||||
{(error as Error)?.message || "Failed to load staff data."}
|
{(error as Error)?.message || "Failed to load staff data."}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
<DeleteConfirmationDialog
|
||||||
isOpen={isDeleteStaffOpen}
|
isOpen={isDeleteStaffOpen}
|
||||||
onConfirm={handleConfirmDeleteStaff}
|
onConfirm={handleConfirmDeleteStaff}
|
||||||
onCancel={() => setIsDeleteStaffOpen(false)}
|
onCancel={() => setIsDeleteStaffOpen(false)}
|
||||||
entityName={currentStaff?.name}
|
entityName={currentStaff?.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Modal Overlay */}
|
|
||||||
{modalOpen && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-md shadow-lg">
|
|
||||||
<h2 className="text-lg font-bold mb-4">
|
|
||||||
{editingStaff ? "Edit Staff" : "Add Staff"}
|
|
||||||
</h2>
|
|
||||||
<StaffForm
|
|
||||||
initialData={editingStaff || undefined}
|
|
||||||
onSubmit={handleFormSubmit}
|
|
||||||
onCancel={handleModalCancel}
|
|
||||||
isLoading={isAdding || isUpdating}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* User Setting section */}
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardContent className="space-y-4 py-6">
|
|
||||||
<h3 className="text-lg font-semibold">User Settings</h3>
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(e.currentTarget);
|
|
||||||
const password =
|
|
||||||
formData.get("password")?.toString().trim() || undefined;
|
|
||||||
|
|
||||||
updateUserMutate.mutate({
|
|
||||||
username: usernameUser?.trim() || undefined,
|
|
||||||
password: password || undefined,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium">Username</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
value={usernameUser}
|
|
||||||
onChange={(e) => setUsernameUser(e.target.value)}
|
|
||||||
className="mt-1 p-2 border rounded w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium">
|
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
className="mt-1 p-2 border rounded w-full"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Leave blank to keep current password.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
|
||||||
disabled={updateUserMutate.isPending}
|
|
||||||
>
|
|
||||||
{updateUserMutate.isPending ? "Saving..." : "Save Changes"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Credential Section */}
|
|
||||||
<div className="mt-6">
|
|
||||||
<CredentialTable />
|
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Modal Overlay */}
|
||||||
|
{modalOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md shadow-lg">
|
||||||
|
<h2 className="text-lg font-bold mb-4">
|
||||||
|
{editingStaff ? "Edit Staff" : "Add Staff"}
|
||||||
|
</h2>
|
||||||
|
<StaffForm
|
||||||
|
initialData={editingStaff || undefined}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
onCancel={handleModalCancel}
|
||||||
|
isLoading={isAdding || isUpdating}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* User Setting section */}
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardContent className="space-y-4 py-6">
|
||||||
|
<h3 className="text-lg font-semibold">User Settings</h3>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const password =
|
||||||
|
formData.get("password")?.toString().trim() || undefined;
|
||||||
|
|
||||||
|
updateUserMutate.mutate({
|
||||||
|
username: usernameUser?.trim() || undefined,
|
||||||
|
password: password || undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value={usernameUser}
|
||||||
|
onChange={(e) => setUsernameUser(e.target.value)}
|
||||||
|
className="mt-1 p-2 border rounded w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium">New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
className="mt-1 p-2 border rounded w-full"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Leave blank to keep current password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||||
|
disabled={updateUserMutate.isPending}
|
||||||
|
>
|
||||||
|
{updateUserMutate.isPending ? "Saving..." : "Save Changes"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Credential Section */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<CredentialTable />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* NpiProvider Section */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<NpiProviderTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ model User {
|
|||||||
patients Patient[]
|
patients Patient[]
|
||||||
appointments Appointment[]
|
appointments Appointment[]
|
||||||
staff Staff[]
|
staff Staff[]
|
||||||
|
npiProviders NpiProvider[]
|
||||||
claims Claim[]
|
claims Claim[]
|
||||||
insuranceCredentials InsuranceCredential[]
|
insuranceCredentials InsuranceCredential[]
|
||||||
updatedPayments Payment[] @relation("PaymentUpdatedBy")
|
updatedPayments Payment[] @relation("PaymentUpdatedBy")
|
||||||
@@ -114,6 +115,20 @@ model Staff {
|
|||||||
claims Claim[] @relation("ClaimStaff")
|
claims Claim[] @relation("ClaimStaff")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model NpiProvider {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int
|
||||||
|
npiNumber String
|
||||||
|
providerName String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([userId, npiNumber])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
enum ProcedureSource {
|
enum ProcedureSource {
|
||||||
COMBO
|
COMBO
|
||||||
MANUAL
|
MANUAL
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export * from "./databaseBackup-types";
|
|||||||
export * from "./notifications-types";
|
export * from "./notifications-types";
|
||||||
export * from "./cloudStorage-types";
|
export * from "./cloudStorage-types";
|
||||||
export * from "./payments-reports-types";
|
export * from "./payments-reports-types";
|
||||||
export * from "./patientConnection-types";
|
export * from "./patientConnection-types";
|
||||||
|
export * from "./npiProviders-types";
|
||||||
14
packages/db/types/npiProviders-types.ts
Normal file
14
packages/db/types/npiProviders-types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { NpiProviderUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
|
|
||||||
|
export type NpiProvider = z.infer<
|
||||||
|
typeof NpiProviderUncheckedCreateInputObjectSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const insertNpiProviderSchema = (
|
||||||
|
NpiProviderUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({ id: true });
|
||||||
|
|
||||||
|
export type InsertNpiProvider = z.infer<
|
||||||
|
typeof insertNpiProviderSchema
|
||||||
|
>;
|
||||||
@@ -5,6 +5,7 @@ export * from '../shared/schemas/objects/PatientUncheckedCreateInput.schema';
|
|||||||
export * from '../shared/schemas/enums/PatientStatus.schema';
|
export * from '../shared/schemas/enums/PatientStatus.schema';
|
||||||
export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema';
|
export * from '../shared/schemas/objects/UserUncheckedCreateInput.schema';
|
||||||
export * from '../shared/schemas/objects/StaffUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/StaffUncheckedCreateInput.schema'
|
||||||
|
export * from '../shared/schemas/objects/NpiProviderUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema'
|
||||||
|
|||||||
Reference in New Issue
Block a user