setting-page - staffmembers done- no form-ui , no users setting

This commit is contained in:
2025-05-15 13:11:05 +05:30
parent b03b7efcb4
commit 3799568b2d
5 changed files with 536 additions and 1 deletions

View File

@@ -17,7 +17,7 @@ router.post("/", async (req: Request, res: Response): Promise<any> => {
try {
const validatedData = staffCreateSchema.parse(req.body);
const newStaff = await storage.createStaff(validatedData);
res.status(201).json(newStaff);
res.status(200).json(newStaff);
} catch (error) {
console.error("Failed to create staff:", error);
res.status(500).send("Failed to create staff");

View File

@@ -10,6 +10,7 @@ import AppointmentsPage from "./pages/appointments-page";
import PatientsPage from "./pages/patients-page";
import { ProtectedRoute } from "./lib/protected-route";
import { AuthProvider } from "./hooks/use-auth";
import SettingsPage from "./pages/settings-page";
function Router() {
return (
@@ -17,6 +18,7 @@ function Router() {
<ProtectedRoute path="/" component={Dashboard} />
<ProtectedRoute path="/appointments" component={AppointmentsPage} />
<ProtectedRoute path="/patients" component={PatientsPage} />
<ProtectedRoute path="/settings" component={SettingsPage}/>
<Route path="/auth" component={AuthPage} />
<Route component={NotFound} />
</Switch>

View File

@@ -0,0 +1,99 @@
import React, { useState, useEffect } from "react";
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import { z } from "zod";
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
interface StaffFormProps {
initialData?: Partial<Staff>;
onSubmit: (data: Omit<Staff, "id" | "createdAt">) => void;
onCancel: () => void;
isLoading?: boolean;
}
export function StaffForm({ initialData, onSubmit, onCancel, isLoading }: StaffFormProps) {
const [name, setName] = useState(initialData?.name || "");
const [email, setEmail] = useState(initialData?.email || "");
const [role, setRole] = useState(initialData?.role || "Staff");
const [phone, setPhone] = useState(initialData?.phone || "");
useEffect(() => {
setName(initialData?.name || "");
setEmail(initialData?.email || "");
setRole(initialData?.role || "Staff");
setPhone(initialData?.phone || "");
}, [initialData]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
alert("Name is required");
return;
}
onSubmit({ name, email: email || undefined, role, phone: phone || undefined });
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Name *</label>
<input
type="text"
className="mt-1 block w-full border rounded p-2"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
className="mt-1 block w-full border rounded p-2"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Role *</label>
<input
type="text"
className="mt-1 block w-full border rounded p-2"
value={role}
onChange={(e) => setRole(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Phone</label>
<input
type="tel"
className="mt-1 block w-full border rounded p-2"
value={phone}
onChange={(e) => setPhone(e.target.value)}
disabled={isLoading}
/>
</div>
<div className="flex justify-end space-x-2">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border rounded"
disabled={isLoading}
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
disabled={isLoading}
>
{isLoading ? "Saving..." : "Save"}
</button>
</div>
</form>
);
}

View File

@@ -0,0 +1,242 @@
import React, { useState } from "react";
import { z } from "zod";
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
const staffCreateSchema = StaffUncheckedCreateInputObjectSchema;
const staffUpdateSchema = (
StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
).partial();
interface StaffTableProps {
staff: Staff[];
isLoading?: boolean;
isError?: boolean;
onAdd: () => void;
onEdit: (staff: Staff) => void;
onDelete: (id: number) => void;
onView: (staff: Staff) => void;
}
export function StaffTable({
staff,
onEdit,
onView,
onDelete,
onAdd,
}: StaffTableProps) {
const [currentPage, setCurrentPage] = useState<number>(1);
const staffPerPage = 5;
const indexOfLastStaff = currentPage * staffPerPage;
const indexOfFirstStaff = indexOfLastStaff - staffPerPage;
const currentStaff = staff.slice(indexOfFirstStaff, indexOfLastStaff);
const totalPages = Math.ceil(staff.length / staffPerPage);
const getInitials = (name: string) => {
return name
.split(" ")
.map((n: string) => n[0])
.join("")
.toUpperCase();
};
const getAvatarColor = (id: number) => {
const colors = [
"bg-blue-500",
"bg-teal-500",
"bg-amber-500",
"bg-rose-500",
"bg-indigo-500",
"bg-green-500",
"bg-purple-500",
];
return colors[id % colors.length];
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
day: "2-digit",
month: "short",
year: "numeric",
}).format(date);
};
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">Staff Members</h2>
<button
onClick={onAdd}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Add New Staff
</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-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Staff
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Phone
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Joined
</th>
<th className="relative px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentStaff.length === 0 ? (
<tr>
<td colSpan={6} className="text-center py-8 text-gray-500">
No staff found. Add new staff to get started.
</td>
</tr>
) : (
currentStaff.map((staff: Staff) => {
const avatarId = staff.id ?? 0; // fallback if undefined
const formattedDate = staff.createdAt
? formatDate(
typeof staff.createdAt === "string"
? staff.createdAt
: staff.createdAt.toISOString()
)
: "N/A";
return (
<tr key={avatarId} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap flex items-center">
<div
className={`h-10 w-10 rounded-full flex items-center justify-center text-white font-bold ${getAvatarColor(
avatarId
)}`}
>
{getInitials(staff.name)}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{staff.name}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{staff.email || "—"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm capitalize text-gray-900">
{staff.role}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{staff.phone || "—"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formattedDate}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
<button
onClick={() => staff.id !== undefined && onEdit(staff)}
className="text-blue-600 hover:text-blue-900"
aria-label="Edit Staff"
>
Edit
</button>
<button
onClick={() =>
staff.id !== undefined && onDelete(staff.id)
}
className="text-red-600 hover:text-red-900"
aria-label="Delete Staff"
>
Delete
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{staff.length > staffPerPage && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200">
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<p className="text-sm text-gray-700">
Showing{" "}
<span className="font-medium">{indexOfFirstStaff + 1}</span> to{" "}
<span className="font-medium">
{Math.min(indexOfLastStaff, staff.length)}
</span>{" "}
of <span className="font-medium">{staff.length}</span> results
</p>
<nav
className="inline-flex -space-x-px rounded-md shadow-sm"
aria-label="Pagination"
>
<a
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={`relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 ${
currentPage === 1 ? "pointer-events-none opacity-50" : ""
}`}
>
Previous
</a>
{Array.from({ length: totalPages }).map((_, i) => (
<a
key={i}
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage(i + 1);
}}
aria-current={currentPage === i + 1 ? "page" : undefined}
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
currentPage === i + 1
? "z-10 bg-blue-50 border-blue-500 text-blue-600"
: "border-gray-300 text-gray-500 hover:bg-gray-50"
}`}
>
{i + 1}
</a>
))}
<a
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
}}
className={`relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 ${
currentPage === totalPages ? "pointer-events-none opacity-50" : ""
}`}
>
Next
</a>
</nav>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,192 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar";
import { StaffTable } from "@/components/staffs/staff-table";
import { useToast } from "@/hooks/use-toast";
import { Card, CardContent } from "@/components/ui/card";
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas";
import { z } from "zod";
import { apiRequest, queryClient } from "@/lib/queryClient";
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
export default function SettingsPage() {
const { toast } = useToast();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev);
// Fetch staff data
const {
data: staff = [],
isLoading,
isError,
error,
} = useQuery<Staff[]>({
queryKey: ["/api/staffs/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/staffs/");
if (!res.ok) {
throw new Error("Failed to fetch staff");
}
return res.json();
},
staleTime: 1000 * 60 * 5, // 5 minutes cache
});
// Add Staff Mutation
const addStaffMutation = useMutation({
mutationFn: async (newStaff: Omit<Staff, "id" | "createdAt">) => {
const res = await apiRequest("POST", "/api/staffs/", newStaff);
if (!res.ok) {
const errorData = await res.json().catch(() => null);
throw new Error(errorData?.message || "Failed to add staff");
}
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/staffs/"] });
toast({
title: "Staff Added",
description: "Staff member added successfully.",
variant: "default",
});
},
onError: (error: any) => {
toast({
title: "Error",
description: error?.message || "Failed to add staff",
variant: "destructive",
});
},
});
// Update Staff Mutation
const updateStaffMutation = useMutation({
mutationFn: async ({
id,
updatedFields,
}: {
id: number;
updatedFields: Partial<Staff>;
}) => {
const res = await apiRequest("PUT", `/api/staffs/${id}`, updatedFields);
if (!res.ok) {
const errorData = await res.json().catch(() => null);
throw new Error(errorData?.message || "Failed to update staff");
}
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/staffs/"] });
toast({
title: "Staff Updated",
description: "Staff member updated successfully.",
variant: "default",
});
},
onError: (error: any) => {
toast({
title: "Error",
description: error?.message || "Failed to update staff",
variant: "destructive",
});
},
});
// Delete Staff Mutation
const deleteStaffMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("DELETE", `/api/staffs/${id}`);
if (!res.ok) {
const errorData = await res.json().catch(() => null);
throw new Error(errorData?.message || "Failed to delete staff");
}
return id;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/staffs/"] });
toast({
title: "Staff Removed",
description: "Staff member deleted.",
variant: "default",
});
},
onError: (error: any) => {
toast({
title: "Error",
description: error?.message || "Failed to delete staff",
variant: "destructive",
});
},
});
// Handlers for prompts and mutations
const openAddStaffModal = () => {
const name = prompt("Enter staff name:");
if (!name) return;
const email = prompt("Enter staff email (optional):") || undefined;
const role = prompt("Enter staff role:") || "Staff";
const phone = prompt("Enter staff phone (optional):") || undefined;
addStaffMutation.mutate({ name, email, role, phone });
};
const openEditStaffModal = (staff: Staff) => {
if (typeof staff.id !== "number") {
toast({
title: "Error",
description: "Staff ID is missing",
variant: "destructive",
});
return;
}
const name = prompt("Edit staff name:", staff.name);
if (!name) return;
const email = prompt("Edit staff email:", staff.email || "") || undefined;
const role = prompt("Edit staff role:", staff.role || "Staff") || "Staff";
const phone = prompt("Edit staff phone:", staff.phone || "") || undefined;
updateStaffMutation.mutate({
id: staff.id,
updatedFields: { name, email, role, phone },
});
};
const handleDeleteStaff = (id: number) => {
if (confirm("Are you sure you want to delete this staff member?")) {
deleteStaffMutation.mutate(id);
}
};
const handleViewStaff = (staff: Staff) =>
alert(`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`);
return (
<div className="flex h-screen overflow-hidden bg-gray-100">
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
<div className="flex-1 flex flex-col overflow-hidden">
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
<main className="flex-1 overflow-y-auto p-4">
<Card>
<CardContent>
<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>
)}
</CardContent>
</Card>
</main>
</div>
</div>
);
}