diff --git a/apps/Backend/src/routes/staffs.ts b/apps/Backend/src/routes/staffs.ts index 94713bc..da064f3 100644 --- a/apps/Backend/src/routes/staffs.ts +++ b/apps/Backend/src/routes/staffs.ts @@ -17,7 +17,7 @@ router.post("/", async (req: Request, res: Response): Promise => { 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"); diff --git a/apps/Frontend/src/App.tsx b/apps/Frontend/src/App.tsx index 7ba67ab..882527d 100644 --- a/apps/Frontend/src/App.tsx +++ b/apps/Frontend/src/App.tsx @@ -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() { + diff --git a/apps/Frontend/src/components/staffs/staff-form.tsx b/apps/Frontend/src/components/staffs/staff-form.tsx new file mode 100644 index 0000000..afe8dbb --- /dev/null +++ b/apps/Frontend/src/components/staffs/staff-form.tsx @@ -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; + +interface StaffFormProps { + initialData?: Partial; + onSubmit: (data: Omit) => 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 ( +
+
+ + setName(e.target.value)} + required + disabled={isLoading} + /> +
+
+ + setEmail(e.target.value)} + disabled={isLoading} + /> +
+
+ + setRole(e.target.value)} + required + disabled={isLoading} + /> +
+
+ + setPhone(e.target.value)} + disabled={isLoading} + /> +
+
+ + +
+
+ ); +} diff --git a/apps/Frontend/src/components/staffs/staff-table.tsx b/apps/Frontend/src/components/staffs/staff-table.tsx new file mode 100644 index 0000000..6612af5 --- /dev/null +++ b/apps/Frontend/src/components/staffs/staff-table.tsx @@ -0,0 +1,242 @@ +import React, { useState } from "react"; +import { z } from "zod"; +import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/shared/schemas"; + +type Staff = z.infer; + +const staffCreateSchema = StaffUncheckedCreateInputObjectSchema; +const staffUpdateSchema = ( + StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject +).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(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 ( +
+
+

Staff Members

+ +
+ +
+ + + + + + + + + + + + + {currentStaff.length === 0 ? ( + + + + ) : ( + 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 ( + + + + + + + + + ); + }) + )} + +
+ Staff + + Email + + Role + + Phone + + Joined + + Actions +
+ No staff found. Add new staff to get started. +
+
+ {getInitials(staff.name)} +
+
+
+ {staff.name} +
+
+
+ {staff.email || "—"} + + {staff.role} + + {staff.phone || "—"} + + {formattedDate} + + + +
+
+ + {staff.length > staffPerPage && ( +
+
+

+ Showing{" "} + {indexOfFirstStaff + 1} to{" "} + + {Math.min(indexOfLastStaff, staff.length)} + {" "} + of {staff.length} results +

+ + +
+
+ )} +
+ ); +} diff --git a/apps/Frontend/src/pages/settings-page.tsx b/apps/Frontend/src/pages/settings-page.tsx new file mode 100644 index 0000000..191d385 --- /dev/null +++ b/apps/Frontend/src/pages/settings-page.tsx @@ -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; + +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({ + 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) => { + 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; + }) => { + 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 ( +
+ +
+ +
+ + + + {isError && ( +

+ {(error as Error)?.message || "Failed to load staff data."} +

+ )} +
+
+
+
+
+ ); +}