npiProvider - v1
This commit is contained in:
@@ -4,6 +4,7 @@ import appointmentsRoutes from "./appointments";
|
||||
import appointmentProceduresRoutes from "./appointments-procedures";
|
||||
import usersRoutes from "./users";
|
||||
import staffsRoutes from "./staffs";
|
||||
import npiProvidersRoutes from "./npiProviders";
|
||||
import claimsRoutes from "./claims";
|
||||
import patientDataExtractionRoutes from "./patientDataExtraction";
|
||||
import insuranceCredsRoutes from "./insuranceCreds";
|
||||
@@ -25,6 +26,7 @@ router.use("/appointments", appointmentsRoutes);
|
||||
router.use("/appointment-procedures", appointmentProceduresRoutes);
|
||||
router.use("/users", usersRoutes);
|
||||
router.use("/staffs", staffsRoutes);
|
||||
router.use("/npiProviders", npiProvidersRouter);
|
||||
router.use("/patientDataExtraction", patientDataExtractionRoutes);
|
||||
router.use("/claims", claimsRoutes);
|
||||
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 { appointmentProceduresStorage } from './appointment-procedures-storage';
|
||||
import { staffStorage } from './staff-storage';
|
||||
import { npiProviderStorage } from './npi-providers-storage';
|
||||
import { claimsStorage } from './claims-storage';
|
||||
import { insuranceCredsStorage } from './insurance-creds-storage';
|
||||
import { generalPdfStorage } from './general-pdf-storage';
|
||||
@@ -22,6 +23,7 @@ export const storage = {
|
||||
...appointmentsStorage,
|
||||
...appointmentProceduresStorage,
|
||||
...staffStorage,
|
||||
...npiProviderStorage,
|
||||
...claimsStorage,
|
||||
...insuranceCredsStorage,
|
||||
...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 { useAuth } from "@/hooks/use-auth";
|
||||
import { Staff } from "@repo/db/types";
|
||||
|
||||
import { NpiProviderTable } from "@/components/settings/npiProviderTable";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { toast } = useToast();
|
||||
@@ -190,7 +190,7 @@ export default function SettingsPage() {
|
||||
|
||||
const [isDeleteStaffOpen, setIsDeleteStaffOpen] = useState(false);
|
||||
const [currentStaff, setCurrentStaff] = useState<Staff | undefined>(
|
||||
undefined
|
||||
undefined,
|
||||
);
|
||||
|
||||
const handleDeleteStaff = (staff: Staff) => {
|
||||
@@ -212,7 +212,7 @@ export default function SettingsPage() {
|
||||
|
||||
const handleViewStaff = (staff: Staff) =>
|
||||
alert(
|
||||
`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`
|
||||
`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`,
|
||||
);
|
||||
|
||||
// MANAGE USER
|
||||
@@ -229,7 +229,7 @@ export default function SettingsPage() {
|
||||
//update user mutation
|
||||
const updateUserMutate = useMutation({
|
||||
mutationFn: async (
|
||||
updates: Partial<{ username: string; password: string }>
|
||||
updates: Partial<{ username: string; password: string }>,
|
||||
) => {
|
||||
if (!user?.id) throw new Error("User not loaded");
|
||||
const res = await apiRequest("PUT", `/api/users/${user.id}`, updates);
|
||||
@@ -258,110 +258,113 @@ export default function SettingsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="mt-8">
|
||||
<StaffTable
|
||||
staff={staff}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
onAdd={openAddStaffModal}
|
||||
onEdit={openEditStaffModal}
|
||||
onDelete={handleDeleteStaff}
|
||||
onView={handleViewStaff}
|
||||
/>
|
||||
{isError && (
|
||||
<p className="mt-4 text-red-600">
|
||||
{(error as Error)?.message || "Failed to load staff data."}
|
||||
</p>
|
||||
)}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="mt-8">
|
||||
<StaffTable
|
||||
staff={staff}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
onAdd={openAddStaffModal}
|
||||
onEdit={openEditStaffModal}
|
||||
onDelete={handleDeleteStaff}
|
||||
onView={handleViewStaff}
|
||||
/>
|
||||
{isError && (
|
||||
<p className="mt-4 text-red-600">
|
||||
{(error as Error)?.message || "Failed to load staff data."}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<DeleteConfirmationDialog
|
||||
isOpen={isDeleteStaffOpen}
|
||||
onConfirm={handleConfirmDeleteStaff}
|
||||
onCancel={() => setIsDeleteStaffOpen(false)}
|
||||
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 />
|
||||
<DeleteConfirmationDialog
|
||||
isOpen={isDeleteStaffOpen}
|
||||
onConfirm={handleConfirmDeleteStaff}
|
||||
onCancel={() => setIsDeleteStaffOpen(false)}
|
||||
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>
|
||||
|
||||
{/* NpiProvider Section */}
|
||||
<div className="mt-6">
|
||||
<NpiProviderTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user