diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..cacff11 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/pages/Academics.tsx b/frontend/src/pages/Academics.tsx index dd8aa09..7883e1c 100644 --- a/frontend/src/pages/Academics.tsx +++ b/frontend/src/pages/Academics.tsx @@ -13,11 +13,26 @@ import { } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; -import { Loader2, Trash, RefreshCw, Download } from "lucide-react"; +import { + Loader2, + Trash, + RefreshCw, + Download, + ChevronLeft, + ChevronRight, + Eye, + BookOpen, +} from "lucide-react"; export default function AcademicsPage() { const [records, setRecords] = useState([]); @@ -25,6 +40,12 @@ export default function AcademicsPage() { const [searchText, setSearchText] = useState(""); + const [viewOpen, setViewOpen] = useState(false); + const [viewData, setViewData] = useState(null); + + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const fetchAll = useCallback(async () => { setLoading(true); try { @@ -51,6 +72,20 @@ export default function AcademicsPage() { ); }); + useEffect(() => { + setCurrentPage(1); + }, [searchText]); + + const totalPages = Math.ceil(filteredRecords.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredRecords.slice(indexOfFirstItem, indexOfLastItem); + + function openView(item: any) { + setViewData(item); + setViewOpen(true); + } + async function handleDelete(id: number) { if (!confirm("Delete record?")) return; await deleteAcademicsApi(id); @@ -74,24 +109,29 @@ export default function AcademicsPage() { return (
-
-

Academics & Research

+
+

Academics & Research

-
+
setSearchText(e.target.value)} - className="w-[260px]" + className="w-[280px] text-base" /> - -
@@ -99,65 +139,108 @@ export default function AcademicsPage() { - Academics Records + Academic Records - -
- - + +
+
+ - ID - Name - Phone - Email - Course - Subject - Message - Date - Actions + + ID + + + Full Name + + + Course + + + Subject + + + Applied Date + + + Message + + + Actions + {loading ? ( - - + + - ) : filteredRecords.length === 0 ? ( + ) : currentItems.length === 0 ? ( - + No records found ) : ( - filteredRecords.map((item) => ( - - {item.id} - {item.fullName} - {item.number} - {item.emailId} - - {item.courseName} - {item.subject} - - - {item.message} + currentItems.map((item) => ( + + + {item.id} - +
+ {item.fullName} +
+
+ {item.emailId} +
+
+ {item.number} +
+
+ +
+ {item.courseName || "-"} +
+
+ +
+ {item.subject || "-"} +
+
+ {new Date(item.createdAt).toLocaleDateString()} - - +
+ {item.message || "-"} +
+
+ +
+ + +
)) @@ -165,8 +248,117 @@ export default function AcademicsPage() {
+ + {!loading && filteredRecords.length > 0 && ( +
+
+ Showing{" "} + {indexOfFirstItem + 1} to{" "} + + {Math.min(indexOfLastItem, filteredRecords.length)} + {" "} + of{" "} + {filteredRecords.length}{" "} + records +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+ )}
+ + + + + + Academic Detail View + + + {viewData && ( +
+
+
+
+

+ Applicant Information +

+

+ {viewData.fullName} +

+

{viewData.emailId}

+

{viewData.number}

+
+
+

+ Course & Subject +

+

+ {viewData.courseName || "N/A"} +

+

+ {viewData.subject} +

+
+
+

+ Submission Date +

+

+ {new Date(viewData.createdAt).toLocaleString()} +

+
+
+
+
+

+ Message / Research Inquiry +

+

+ {viewData.message || "No message content provided."} +

+
+
+
+
+ )} + + + +
+
); } diff --git a/frontend/src/pages/Appointment.tsx b/frontend/src/pages/Appointment.tsx index 4eb44d6..0ed6b7c 100644 --- a/frontend/src/pages/Appointment.tsx +++ b/frontend/src/pages/Appointment.tsx @@ -1,7 +1,7 @@ -import {useState, useEffect, useCallback} from "react"; +import { useState, useEffect, useCallback } from "react"; -import {getAppointmentsApi, deleteAppointmentApi} from "@/api/appointment"; -import {exportToExcel} from "@/utils/exportToExcel"; +import { getAppointmentsApi, deleteAppointmentApi } from "@/api/appointment"; +import { exportToExcel } from "@/utils/exportToExcel"; import { Table, @@ -12,12 +12,26 @@ import { TableRow, } from "@/components/ui/table"; -import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; -import {Button} from "@/components/ui/button"; -import {Input} from "@/components/ui/input"; - -import {Loader2, Trash, RefreshCw, Download} from "lucide-react"; +import { + Loader2, + Trash, + RefreshCw, + Download, + ChevronLeft, + ChevronRight, + Eye, +} from "lucide-react"; export default function AppointmentPage() { const [appointments, setAppointments] = useState([]); @@ -25,9 +39,14 @@ export default function AppointmentPage() { const [searchText, setSearchText] = useState(""); const [filterDoctor, setFilterDoctor] = useState(""); - const [filterDepartment, setFilterDepartment] = useState(""); const [filterDate, setFilterDate] = useState(""); + const [viewOpen, setViewOpen] = useState(false); + const [viewData, setViewData] = useState(null); + + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const fetchAll = useCallback(async () => { setLoading(true); try { @@ -54,19 +73,30 @@ export default function AppointmentPage() { ? item.doctor?.name?.toLowerCase().includes(filterDoctor.toLowerCase()) : true; - const matchesDepartment = filterDepartment - ? item.department?.name - ?.toLowerCase() - .includes(filterDepartment.toLowerCase()) - : true; - const matchesDate = filterDate ? new Date(item.date).toISOString().split("T")[0] === filterDate : true; - return matchesSearch && matchesDoctor && matchesDepartment && matchesDate; + return matchesSearch && matchesDoctor && matchesDate; }); + useEffect(() => { + setCurrentPage(1); + }, [searchText, filterDoctor, filterDate]); + + const totalPages = Math.ceil(filteredAppointments.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredAppointments.slice( + indexOfFirstItem, + indexOfLastItem, + ); + + function openView(item: any) { + setViewData(item); + setViewOpen(true); + } + async function handleDelete(id: number) { if (!confirm("Delete appointment?")) return; await deleteAppointmentApi(id); @@ -84,51 +114,41 @@ export default function AppointmentPage() { Date: new Date(item.date).toLocaleDateString(), Message: item.message, })); - exportToExcel(exportData, "appointments"); }; return (
-
-

Appointments

+
+

Appointments

-
+
setSearchText(e.target.value)} - className="w-[220px]" - /> - - setFilterDoctor(e.target.value)} - className="w-[180px]" - /> - - setFilterDepartment(e.target.value)} - className="w-[200px]" + className="w-[220px] text-base" /> setFilterDate(e.target.value)} - className="w-[180px]" + className="w-[160px] text-base" /> - -
@@ -136,72 +156,102 @@ export default function AppointmentPage() { - Appointment List + Appointment List - -
- - + +
+
+ - ID - Name - Phone - Email - Doctor - Department - Appointment Date - Message - Generated on - - Actions + + ID + + + Patient + + + Doctor + + + Date + + + Message + + + Actions + {loading ? ( - - + + - ) : filteredAppointments.length === 0 ? ( + ) : currentItems.length === 0 ? ( - + No appointments found ) : ( - filteredAppointments.map((item) => ( - - {item.id} - {item.name} - {item.mobileNumber} - {item.email} - {item.doctor?.name} - {item.department?.name} - - {/* ✅ DATE ONLY */} - - {new Date(item.date).toLocaleDateString()} - - - - {item.message} + currentItems.map((item) => ( + + + {item.id} - {" "} - {new Date(item.createdAt).toLocaleDateString()} +
+ {item.name} +
+
+ {item.mobileNumber} +
- - +
+ {item.doctor?.name || "-"} +
+
+ {item.department?.name} +
+
+ +
+ {new Date(item.date).toLocaleDateString()} +
+
+ +
+ {item.message || "-"} +
+
+ +
+ + +
)) @@ -209,8 +259,123 @@ export default function AppointmentPage() {
+ + {!loading && filteredAppointments.length > 0 && ( +
+
+ Showing{" "} + {indexOfFirstItem + 1} to{" "} + + {Math.min(indexOfLastItem, filteredAppointments.length)} + {" "} + of{" "} + + {filteredAppointments.length} + +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+ )}
+ + + + + + Appointment Details + + + {viewData && ( +
+
+
+
+

+ Patient Information +

+

+ {viewData.name} +

+

{viewData.mobileNumber}

+

+ {viewData.email || "No email provided"} +

+
+
+

+ Appointment Date +

+

+ {new Date(viewData.date).toLocaleDateString()} +

+

+ Booked on: {new Date(viewData.createdAt).toLocaleString()} +

+
+
+
+
+

+ Doctor / Department +

+

+ {viewData.doctor?.name || "Not Assigned"} +

+

+ {viewData.department?.name || "General"} +

+
+
+

+ Message from Patient +

+

+ {viewData.message || "No message provided."} +

+
+
+
+
+ )} + + + +
+
); } diff --git a/frontend/src/pages/Career.tsx b/frontend/src/pages/Career.tsx index 5782bd8..ec60512 100644 --- a/frontend/src/pages/Career.tsx +++ b/frontend/src/pages/Career.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useCallback } from "react"; import { getCareersApi, deleteCareerApi } from "@/api/career"; - import apiClient from "@/api/client"; import { @@ -15,7 +14,6 @@ import { import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; - import { Dialog, DialogContent, @@ -25,8 +23,17 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; -import { Loader2, Plus, Pencil, Trash, RefreshCw } from "lucide-react"; +import { + Loader2, + Plus, + Pencil, + Trash, + RefreshCw, + ChevronLeft, + ChevronRight, +} from "lucide-react"; export default function CareerPage() { const [careers, setCareers] = useState([]); @@ -37,6 +44,9 @@ export default function CareerPage() { const [searchText, setSearchText] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const [form, setForm] = useState({ post: "", designation: "", @@ -63,10 +73,21 @@ export default function CareerPage() { fetchAll(); }, [fetchAll]); - const filteredCareers = careers.filter((item) => - item.post?.toLowerCase().includes(searchText.toLowerCase()), + const filteredCareers = careers.filter( + (item) => + item.post?.toLowerCase().includes(searchText.toLowerCase()) || + item.designation?.toLowerCase().includes(searchText.toLowerCase()), ); + useEffect(() => { + setCurrentPage(1); + }, [searchText]); + + const totalPages = Math.ceil(filteredCareers.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredCareers.slice(indexOfFirstItem, indexOfLastItem); + function handleChange(e: any) { setForm({ ...form, [e.target.name]: e.target.value }); } @@ -87,7 +108,6 @@ export default function CareerPage() { function openEdit(item: any) { setEditing(item); - setForm({ post: item.post || "", designation: item.designation || "", @@ -97,7 +117,6 @@ export default function CareerPage() { number: item.number || "", status: item.status || "new", }); - setOpenModal(true); } @@ -108,7 +127,6 @@ export default function CareerPage() { } else { await apiClient.post("/careers", form); } - setOpenModal(false); fetchAll(); } catch (err) { @@ -124,24 +142,29 @@ export default function CareerPage() { return (
-
-

Careers

+
+

Careers

-
+
setSearchText(e.target.value)} - className="w-[220px]" + className="w-[250px] text-base" /> - -
@@ -149,67 +172,113 @@ export default function CareerPage() { - Career List + Career Opportunities - -
- - + +
+
+ - ID - Post - Designation - Qualification - Experience - Email - Phone - Status - Actions + + ID + + + Post & Designation + + + Qualification + + + Experience + + + Contact Info + + + Status + + + Actions + {loading ? ( - - + + - ) : filteredCareers.length === 0 ? ( + ) : currentItems.length === 0 ? ( - + No careers found ) : ( - filteredCareers.map((item) => ( - - {item.id} - {item.post} - {item.designation} - {item.qualification} - {item.experienceNeed} - {item.email} - {item.number} - {item.status} - - - + {item.status} + + - + +
+ + + +
)) @@ -217,67 +286,126 @@ export default function CareerPage() {
+ + {!loading && filteredCareers.length > 0 && ( +
+
+ Showing{" "} + {indexOfFirstItem + 1} to{" "} + + {Math.min(indexOfLastItem, filteredCareers.length)} + {" "} + of{" "} + {filteredCareers.length}{" "} + careers +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+ )}
- {/* MODAL */} - + - {editing ? "Edit Career" : "Add Career"} + + {editing ? "Edit Career" : "Add New Career"} + -
- - - - - - - +
+
+ + + + + + + +
- - - diff --git a/frontend/src/pages/Department.tsx b/frontend/src/pages/Department.tsx index b06dc0b..e56ac07 100644 --- a/frontend/src/pages/Department.tsx +++ b/frontend/src/pages/Department.tsx @@ -31,7 +31,16 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Loader2, RefreshCw, Plus, Pencil, Trash, Eye } from "lucide-react"; +import { + Loader2, + RefreshCw, + Plus, + Pencil, + Trash, + Eye, + ChevronLeft, + ChevronRight, +} from "lucide-react"; interface Department { departmentId: string; @@ -56,6 +65,9 @@ export default function DepartmentPage() { const [searchText, setSearchText] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const [form, setForm] = useState({ departmentId: "", name: "", @@ -88,8 +100,22 @@ export default function DepartmentPage() { fetchDepartments(); }, [fetchDepartments]); - const filteredDepartments = departments.filter((dep) => - dep.name.toLowerCase().includes(searchText.toLowerCase()), + const filteredDepartments = departments.filter( + (dep) => + dep.name.toLowerCase().includes(searchText.toLowerCase()) || + dep.departmentId.toLowerCase().includes(searchText.toLowerCase()), + ); + + useEffect(() => { + setCurrentPage(1); + }, [searchText]); + + const totalPages = Math.ceil(filteredDepartments.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredDepartments.slice( + indexOfFirstItem, + indexOfLastItem, ); function handleChange(e: any) { @@ -155,118 +181,153 @@ export default function DepartmentPage() { return (
-
-

Departments

+
+

Departments

setSearchText(e.target.value)} - className="w-[220px]" + className="w-[250px] text-base" /> -
{error && ( -
+
{error}
)} - Department List + Department List - -
- - + +
+
+ - ID - Name - Para1 - Facilities - Services - Actions + + ID + + + Name + + + Para 1 + + + Facilities + + + Services + + + Actions + {loading ? ( - - + + - ) : filteredDepartments.length === 0 ? ( + ) : currentItems.length === 0 ? ( - + No departments found ) : ( - filteredDepartments.map((dep) => ( - - {dep.departmentId} - - -
{dep.name}
+ currentItems.map((dep) => ( + + + {dep.departmentId} -
+
+ {dep.name} +
+ + + +
{truncate(dep.para1)}
-
+
{truncate(dep.facilities)}
-
+
{truncate(dep.services)}
- - + +
+ - + - + +
)) @@ -274,6 +335,52 @@ export default function DepartmentPage() {
+ + {!loading && filteredDepartments.length > 0 && ( +
+
+ Showing{" "} + {indexOfFirstItem + 1} to{" "} + + {Math.min(indexOfLastItem, filteredDepartments.length)} + {" "} + of{" "} + + {filteredDepartments.length} + {" "} + departments +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+ )}
@@ -346,7 +453,6 @@ export default function DepartmentPage() { - {" "} Department Details @@ -355,35 +461,29 @@ export default function DepartmentPage() {

ID: {viewData.departmentId}

-

Name: {viewData.name}

-

Para1:
{viewData.para1}

-

Para2:
{viewData.para2}

-

Para3:
{viewData.para3}

-

Facilities:
{viewData.facilities}

-

Services:
diff --git a/frontend/src/pages/Doctor.tsx b/frontend/src/pages/Doctor.tsx index 8c94639..36ce6a3 100644 --- a/frontend/src/pages/Doctor.tsx +++ b/frontend/src/pages/Doctor.tsx @@ -1,4 +1,5 @@ -import {useState, useEffect, useCallback} from "react"; +import { useState, useEffect, useCallback } from "react"; +import { AxiosError } from "axios"; import { getDoctorsApi, createDoctorApi, @@ -6,8 +7,7 @@ import { deleteDoctorApi, getDoctorTimingApi, } from "@/api/doctor"; - -import {getDepartmentsApi} from "@/api/department"; +import { getDepartmentsApi } from "@/api/department"; import { Table, @@ -17,10 +17,8 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; - -import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; -import {Button} from "@/components/ui/button"; - +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -28,33 +26,49 @@ import { DialogTitle, DialogFooter, } from "@/components/ui/dialog"; - -import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; import { - Command, - CommandGroup, - CommandItem, - CommandInput, -} from "@/components/ui/command"; - -import {Input} from "@/components/ui/input"; -import {Loader2, Plus, Pencil, Trash, RefreshCw} from "lucide-react"; + Loader2, + RefreshCw, + Plus, + Pencil, + Trash, + ChevronLeft, + ChevronRight, +} from "lucide-react"; interface Department { departmentId: string; name: string; } +const DAYS = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + "additional", +]; + export default function DoctorPage() { const [doctors, setDoctors] = useState([]); const [departments, setDepartments] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [openModal, setOpenModal] = useState(false); const [editing, setEditing] = useState(null); const [searchText, setSearchText] = useState(""); const [filterDepartment, setFilterDepartment] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const [form, setForm] = useState({ doctorId: "", name: "", @@ -66,16 +80,20 @@ export default function DoctorPage() { const fetchAll = useCallback(async () => { setLoading(true); + setError(""); try { const [docRes, depRes] = await Promise.all([ getDoctorsApi(), getDepartmentsApi(), ]); - setDoctors(docRes?.data || []); setDepartments(depRes?.data || []); } catch (err) { - console.error(err); + if (err instanceof AxiosError) { + setError(err.response?.data?.message || "Failed to load data"); + } else { + setError("Something went wrong"); + } } finally { setLoading(false); } @@ -97,13 +115,21 @@ export default function DoctorPage() { return matchesSearch && matchesDepartment; }); + useEffect(() => { + setCurrentPage(1); + }, [searchText, filterDepartment]); + + const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); + function handleChange(e: any) { - setForm({...form, [e.target.name]: e.target.value}); + setForm({ ...form, [e.target.name]: e.target.value }); } - function handleDepartmentChange(depId: string) { + function handleDepartmentToggle(depId: string) { const exists = form.departments.find((d: any) => d.departmentId === depId); - if (exists) { setForm({ ...form, @@ -114,7 +140,7 @@ export default function DoctorPage() { } else { setForm({ ...form, - departments: [...form.departments, {departmentId: depId, timing: {}}], + departments: [...form.departments, { departmentId: depId, timing: {} }], }); } } @@ -124,10 +150,7 @@ export default function DoctorPage() { ...form, departments: form.departments.map((d: any) => d.departmentId === depId - ? { - ...d, - timing: {...d.timing, [day]: value}, - } + ? { ...d, timing: { ...d.timing, [day]: value } } : d, ), }); @@ -148,25 +171,20 @@ export default function DoctorPage() { async function openEdit(doc: any) { setEditing(doc); - try { const timingRes = await getDoctorTimingApi(doc.doctorId); const timingData = timingRes?.data?.departments || []; - - const mappedDepartments = timingData.map((d: any) => ({ - departmentId: d.departmentId, - timing: d.timing || {}, - })); - setForm({ doctorId: doc.doctorId, name: doc.name, designation: doc.designation, workingStatus: doc.workingStatus, qualification: doc.qualification, - departments: mappedDepartments, + departments: timingData.map((d: any) => ({ + departmentId: d.departmentId, + timing: d.timing || {}, + })), }); - setOpenModal(true); } catch (err) { console.error(err); @@ -180,38 +198,40 @@ export default function DoctorPage() { } else { await createDoctorApi(form); } - setOpenModal(false); fetchAll(); - } catch (err) { - console.error(err); + } catch (error) { + console.error(error); } } async function handleDelete(id: string) { - if (!confirm("Delete doctor?")) return; - await deleteDoctorApi(id); - fetchAll(); + if (!confirm("Delete this doctor?")) return; + try { + await deleteDoctorApi(id); + fetchAll(); + } catch (error) { + console.error(error); + } } return (

- {/* HEADER */} -
-

Doctors

+
+

Doctors

-
+
setSearchText(e.target.value)} - className="w-[200px]" + className="w-[250px] text-base" /> - -
- {/* TABLE */} + {error && ( +
+ {error} +
+ )} + - Doctor List + Doctor List - -
- - + +
+
+ - ID - Name - Designation - Status - Qualification - Departments - Timing - Actions + + ID + + + Name + + + Designation + + + Qualification + + + Departments + + + Actions + {loading ? ( - - + + - ) : filteredDoctors.length === 0 ? ( + ) : currentItems.length === 0 ? ( - + No doctors found ) : ( - filteredDoctors.map((doc) => ( - - {doc.doctorId} - {doc.name} - {doc.designation} - {doc.workingStatus} - {doc.qualification} + currentItems.map((doc) => ( + + + {doc.doctorId} + - {doc.departments - ?.map((d: any) => d.departmentName) - .join(", ")} +
+ {doc.name} +
+
+ {doc.workingStatus} +
- - {doc.departments?.map((d: any) => ( -
- {d.departmentName}:{" "} - {JSON.stringify(d.timing)} -
- ))} + +
+ {doc.designation || "-"} +
- - + {doc.qualification || "-"} + + - + +
+ {doc.departments?.map((d: any) => ( + + {d.departmentName} + + ))} + {doc.departments?.length === 0 && ( + - + )} +
+
+ + +
+ + +
)) @@ -315,153 +391,212 @@ export default function DoctorPage() {
+ + {!loading && filteredDoctors.length > 0 && ( +
+
+ Showing{" "} + {indexOfFirstItem + 1} to{" "} + + {Math.min(indexOfLastItem, filteredDoctors.length)} + {" "} + of{" "} + {filteredDoctors.length}{" "} + doctors +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+ )}
- {/* MODAL */} - {/* MODAL */} - + - {editing ? "Edit Doctor" : "Add Doctor"} + + {editing ? "Edit Doctor" : "Add Doctor"} + -
- +
+
+

+ Basic Information +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
- - - - - - {/* Departments */} -
-

Departments

- - - - - - - - - - - - {departments.map((dep) => { - const selected = form.departments.some( - (d: any) => d.departmentId === dep.departmentId, - ); - - return ( - - handleDepartmentChange(dep.departmentId) - } - > - {dep.name} - {selected && } - - ); - })} - - - - +
+

Assign Departments

+
+ {departments.map((dep) => { + const isSelected = form.departments.some( + (d: any) => d.departmentId === dep.departmentId, + ); + return ( + + ); + })} +
+
- {form.departments.map((dep: any) => { - const depName = departments.find( - (d) => d.departmentId === dep.departmentId, - )?.name; - - return ( -
-

{depName}

- - {[ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday", - "additional", - ].map((day) => ( - - handleTimingChange( - dep.departmentId, - day, - e.target.value, - ) - } - /> - ))} +
+

+ Working Hours / Timing +

+ {form.departments.length === 0 ? ( +
+ Select a department to configure timing slots
- ); - })} + ) : ( +
+ {form.departments.map((dep: any) => { + const depName = departments.find( + (d) => d.departmentId === dep.departmentId, + )?.name; + return ( +
+
+

+ {depName} +

+ + Timing Slot + +
+
+ {DAYS.map((day) => ( +
+ + + handleTimingChange( + dep.departmentId, + day, + e.target.value, + ) + } + /> +
+ ))} +
+
+ ); + })} +
+ )} +
- - - diff --git a/frontend/src/pages/candidates.tsx b/frontend/src/pages/candidates.tsx index 45a8ea7..b94ec27 100644 --- a/frontend/src/pages/candidates.tsx +++ b/frontend/src/pages/candidates.tsx @@ -13,11 +13,26 @@ import { } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; -import { Loader2, Trash, RefreshCw, Download } from "lucide-react"; +import { + Loader2, + Trash, + RefreshCw, + Download, + ChevronLeft, + ChevronRight, + Eye, + User, +} from "lucide-react"; export default function CandidatePage() { const [candidates, setCandidates] = useState([]); @@ -26,6 +41,12 @@ export default function CandidatePage() { const [searchText, setSearchText] = useState(""); const [filterCareer, setFilterCareer] = useState(""); + const [viewOpen, setViewOpen] = useState(false); + const [viewData, setViewData] = useState(null); + + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const fetchAll = useCallback(async () => { setLoading(true); try { @@ -55,6 +76,23 @@ export default function CandidatePage() { return matchesSearch && matchesCareer; }); + useEffect(() => { + setCurrentPage(1); + }, [searchText, filterCareer]); + + const totalPages = Math.ceil(filteredCandidates.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredCandidates.slice( + indexOfFirstItem, + indexOfLastItem, + ); + + function openView(item: any) { + setViewData(item); + setViewOpen(true); + } + async function handleDelete(id: number) { if (!confirm("Delete candidate?")) return; await deleteCandidateApi(id); @@ -71,7 +109,7 @@ export default function CandidatePage() { Designation: item.career?.designation, Subject: item.subject, CoverLetter: item.coverLetter, - Date: new Date(item.createdAt).toLocaleDateString(), + AppliedDate: new Date(item.createdAt).toLocaleDateString(), })); exportToExcel(exportData, "candidates"); @@ -79,31 +117,36 @@ export default function CandidatePage() { return (
-
-

Candidates

+
+

Candidates

-
+
setSearchText(e.target.value)} - className="w-[220px]" + className="w-[250px] text-base" /> setFilterCareer(e.target.value)} - className="w-[200px]" + className="w-[200px] text-base" /> - -
@@ -111,68 +154,104 @@ export default function CandidatePage() { - Candidate List + Application List - -
- - + +
+
+ - ID - Name - Phone - Email - Career - Designation - Subject - Cover Letter - Applied On - Actions + + ID + + + Full Name + + + Career & Post + + + Contact + + + Applied On + + + Cover Letter + + + Actions + {loading ? ( - - + + - ) : filteredCandidates.length === 0 ? ( + ) : currentItems.length === 0 ? ( - + No candidates found ) : ( - filteredCandidates.map((item) => ( - - {item.id} - {item.fullName} - {item.mobile} - {item.email} - - {item.career?.post} - {item.career?.designation} - - {item.subject} - - - {item.coverLetter} + currentItems.map((item) => ( + + + {item.id} - +
+ {item.fullName} +
+
+ {item.email} +
+
+ +
+ {item.career?.post || "-"} +
+
+ {item.career?.designation} +
+
+ {item.mobile} + {new Date(item.createdAt).toLocaleDateString()} - - +
+ {item.coverLetter || "No cover letter provided."} +
+
+ +
+ + +
)) @@ -180,8 +259,126 @@ export default function CandidatePage() {
+ + {!loading && filteredCandidates.length > 0 && ( +
+
+ Showing{" "} + {indexOfFirstItem + 1} to{" "} + + {Math.min(indexOfLastItem, filteredCandidates.length)} + {" "} + of{" "} + + {filteredCandidates.length} + +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+ )}
+ + + + + + Candidate Details + + + {viewData && ( +
+
+
+
+

+ Personal Information +

+

+ {viewData.fullName} +

+

{viewData.email}

+

{viewData.mobile}

+
+
+

+ Applied For +

+

+ {viewData.career?.post || "General Application"} +

+

+ {viewData.career?.designation} +

+
+
+

+ Application Date +

+

+ {new Date(viewData.createdAt).toLocaleString()} +

+
+
+
+
+

+ Subject +

+

+ {viewData.subject || "N/A"} +

+
+
+

+ Cover Letter / Message +

+

+ {viewData.coverLetter || "No cover letter provided."} +

+
+
+
+
+ )} + + + +
+
); } diff --git a/frontend/src/pages/inquiry.tsx b/frontend/src/pages/inquiry.tsx index 778243b..c2697d7 100644 --- a/frontend/src/pages/inquiry.tsx +++ b/frontend/src/pages/inquiry.tsx @@ -13,11 +13,26 @@ import { } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; -import { Loader2, Trash, RefreshCw, Download } from "lucide-react"; +import { + Loader2, + Trash, + RefreshCw, + Download, + ChevronLeft, + ChevronRight, + Eye, + Mail, +} from "lucide-react"; export default function InquiryPage() { const [inquiries, setInquiries] = useState([]); @@ -25,6 +40,12 @@ export default function InquiryPage() { const [searchText, setSearchText] = useState(""); + const [viewOpen, setViewOpen] = useState(false); + const [viewData, setViewData] = useState(null); + + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const fetchAll = useCallback(async () => { setLoading(true); try { @@ -50,6 +71,23 @@ export default function InquiryPage() { ); }); + useEffect(() => { + setCurrentPage(1); + }, [searchText]); + + const totalPages = Math.ceil(filteredInquiries.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredInquiries.slice( + indexOfFirstItem, + indexOfLastItem, + ); + + function openView(item: any) { + setViewData(item); + setViewOpen(true); + } + async function handleDelete(id: number) { if (!confirm("Delete inquiry?")) return; await deleteInquiryApi(id); @@ -72,24 +110,29 @@ export default function InquiryPage() { return (
-
-

Inquiries

+
+

Inquiries

-
+
setSearchText(e.target.value)} - className="w-[260px]" + className="w-[280px] text-base" /> - -
@@ -97,62 +140,100 @@ export default function InquiryPage() { - Inquiry List + Customer Inquiries - -
- - + +
+
+ - ID - Name - Phone - Email - Subject - Message - Date - Actions + + ID + + + Customer Details + + + Subject + + + Date + + + Message Snippet + + + Actions + {loading ? ( - - + + - ) : filteredInquiries.length === 0 ? ( + ) : currentItems.length === 0 ? ( - + No inquiries found ) : ( - filteredInquiries.map((item) => ( - - {item.id} - {item.fullName} - {item.number} - {item.emailId} - {item.subject} - - - {item.message} + currentItems.map((item) => ( + + + {item.id} - +
+ {item.fullName} +
+
+ {item.emailId} +
+
+ {item.number} +
+
+ +
+ {item.subject || "-"} +
+
+ {new Date(item.createdAt).toLocaleDateString()} - - +
+ {item.message || "-"} +
+
+ +
+ + +
)) @@ -160,8 +241,115 @@ export default function InquiryPage() {
+ + {!loading && filteredInquiries.length > 0 && ( +
+
+ Showing{" "} + {indexOfFirstItem + 1} to{" "} + + {Math.min(indexOfLastItem, filteredInquiries.length)} + {" "} + of{" "} + + {filteredInquiries.length} + +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+ )}
+ + + + + + Inquiry Details + + + {viewData && ( +
+
+
+
+

+ Customer Information +

+

+ {viewData.fullName} +

+

{viewData.emailId}

+

{viewData.number}

+
+
+

+ Received Date +

+

+ {new Date(viewData.createdAt).toLocaleString()} +

+
+
+
+
+

+ Subject +

+

+ {viewData.subject || "No Subject"} +

+
+
+

+ Message +

+

+ {viewData.message || "No message content."} +

+
+
+
+
+ )} + + + +
+
); } diff --git a/frontend/src/pages/newsMedia.tsx b/frontend/src/pages/newsMedia.tsx index fe267c6..1c48817 100644 --- a/frontend/src/pages/newsMedia.tsx +++ b/frontend/src/pages/newsMedia.tsx @@ -19,6 +19,7 @@ import { import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { Dialog, @@ -37,11 +38,13 @@ import { Eye, ChevronLeft, ChevronRight, + Newspaper, } from "lucide-react"; export default function NewsPage() { const [news, setNews] = useState([]); const [loading, setLoading] = useState(true); + const [totalItems, setTotalItems] = useState(0); const [searchText, setSearchText] = useState(""); @@ -66,29 +69,28 @@ export default function NewsPage() { const fetchAll = useCallback(async () => { setLoading(true); try { - const res = await getNewsApi(); + const res = await getNewsApi(currentPage, itemsPerPage); + setNews(res?.data || []); + setTotalItems(res?.meta?.total || 0); } catch (err) { console.error(err); } finally { setLoading(false); } - }, []); + }, [currentPage, itemsPerPage]); useEffect(() => { fetchAll(); }, [fetchAll]); - const filteredNews = news.filter((item) => - item.Headline?.toLowerCase().includes(searchText.toLowerCase()), + const filteredNews = news.filter( + (item) => + item.Headline?.toLowerCase().includes(searchText.toLowerCase()) || + item.Author?.toLowerCase().includes(searchText.toLowerCase()), ); - const totalPages = Math.ceil(filteredNews.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const paginatedData = filteredNews.slice( - startIndex, - startIndex + itemsPerPage, - ); + const totalPages = Math.ceil(totalItems / itemsPerPage); function handleChange(e: any) { setForm({ ...form, [e.target.name]: e.target.value }); @@ -109,7 +111,6 @@ export default function NewsPage() { function openEdit(item: any) { setEditing(item); - setForm({ headline: item.Headline || "", content: item.Content || "", @@ -118,7 +119,6 @@ export default function NewsPage() { date: item.Date ? item.Date.split("T")[0] : "", author: item.Author || "", }); - setOpenModal(true); } @@ -149,18 +149,17 @@ export default function NewsPage() { return (
-
-

News Media

+
+

+ News Media +

-
+
{ - setSearchText(e.target.value); - setCurrentPage(1); - }} - className="w-[250px]" + onChange={(e) => setSearchText(e.target.value)} + className="w-[250px] text-base" /> - -
- + - News List + News Archives - -
- - + +
+
+ - ID - Headline - Author - Date - Content - Actions + + ID + + + Headline + + + Author + + + Date + + + Content Preview + + + Actions + {loading ? ( - - + + - ) : paginatedData.length === 0 ? ( + ) : filteredNews.length === 0 ? ( - - No news found + + No news articles found ) : ( - paginatedData.map((item) => ( - - {item.Id} - - - {item.Headline} + filteredNews.map((item) => ( + + + {item.Id} - - {item.Author} - +
+ {item.Headline} +
+
+ + {item.Author || "-"} + + {item.Date ? new Date(item.Date).toLocaleDateString() : "-"} - - - {item.Content} + +
+ {item.Content} +
- - - - - - - + +
+ + + +
)) @@ -269,129 +301,197 @@ export default function NewsPage() {
- {/* PAGINATION */} -
-

- Page {currentPage} of {totalPages} -

- -
- - - + {!loading && totalItems > 0 && ( +
+
+ Total {totalItems} articles + (Page {currentPage} of{" "} + {totalPages}) +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
-
+ )} - {/* CREATE / EDIT MODAL */} - + - {editing ? "Edit News" : "Add News"} + + {editing ? "Edit News Article" : "Add New News Article"} + -
- - - +
+
+

+ Article Information +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +