setting-page - staffmembers done- no form-ui , no users setting
This commit is contained in:
@@ -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>
|
||||
|
||||
99
apps/Frontend/src/components/staffs/staff-form.tsx
Normal file
99
apps/Frontend/src/components/staffs/staff-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
242
apps/Frontend/src/components/staffs/staff-table.tsx
Normal file
242
apps/Frontend/src/components/staffs/staff-table.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
192
apps/Frontend/src/pages/settings-page.tsx
Normal file
192
apps/Frontend/src/pages/settings-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user