Files
gg-backend/frontend/src/pages/Doctor.tsx
T

481 lines
13 KiB
TypeScript
Raw Normal View History

2026-03-17 13:11:00 +05:30
import {useState, useEffect, useCallback} from "react";
2026-04-06 17:46:31 +05:30
import {AxiosError} from "axios";
2026-03-17 13:11:00 +05:30
import {
getDoctorsApi,
createDoctorApi,
updateDoctorApi,
deleteDoctorApi,
getDoctorTimingApi,
} from "@/api/doctor";
import {getDepartmentsApi} from "@/api/department";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Button} from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {Input} from "@/components/ui/input";
2026-04-06 17:46:31 +05:30
import {Badge} from "@/components/ui/badge";
import {Loader2, RefreshCw, Plus, Pencil, Trash} from "lucide-react";
2026-03-17 13:11:00 +05:30
interface Department {
departmentId: string;
name: string;
}
2026-04-06 17:46:31 +05:30
const DAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"additional",
];
2026-03-17 13:11:00 +05:30
export default function DoctorPage() {
const [doctors, setDoctors] = useState<any[]>([]);
const [departments, setDepartments] = useState<Department[]>([]);
const [loading, setLoading] = useState(true);
2026-04-06 17:46:31 +05:30
const [error, setError] = useState("");
2026-03-17 13:11:00 +05:30
const [openModal, setOpenModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [searchText, setSearchText] = useState("");
const [filterDepartment, setFilterDepartment] = useState("");
2026-03-17 13:11:00 +05:30
const [form, setForm] = useState<any>({
doctorId: "",
name: "",
designation: "",
workingStatus: "",
qualification: "",
departments: [],
});
const fetchAll = useCallback(async () => {
setLoading(true);
2026-04-06 17:46:31 +05:30
setError("");
2026-03-17 13:11:00 +05:30
try {
const [docRes, depRes] = await Promise.all([
getDoctorsApi(),
getDepartmentsApi(),
]);
setDoctors(docRes?.data || []);
setDepartments(depRes?.data || []);
} catch (err) {
2026-04-06 17:46:31 +05:30
if (err instanceof AxiosError) {
setError(err.response?.data?.message || "Failed to load data");
} else {
setError("Something went wrong");
}
2026-03-17 13:11:00 +05:30
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchAll();
}, [fetchAll]);
const filteredDoctors = doctors.filter((doc) => {
const matchesSearch =
doc.name.toLowerCase().includes(searchText.toLowerCase()) ||
doc.doctorId.toLowerCase().includes(searchText.toLowerCase());
const matchesDepartment = filterDepartment
? doc.departments?.some((d: any) => d.departmentId === filterDepartment)
: true;
return matchesSearch && matchesDepartment;
});
2026-03-17 13:11:00 +05:30
function handleChange(e: any) {
setForm({...form, [e.target.name]: e.target.value});
}
2026-04-06 17:46:31 +05:30
function handleDepartmentToggle(depId: string) {
2026-03-17 16:22:37 +05:30
const exists = form.departments.find((d: any) => d.departmentId === depId);
if (exists) {
setForm({
...form,
departments: form.departments.filter(
(d: any) => d.departmentId !== depId,
),
});
} else {
setForm({
...form,
departments: [...form.departments, {departmentId: depId, timing: {}}],
2026-03-17 16:22:37 +05:30
});
}
2026-03-17 13:11:00 +05:30
}
2026-03-17 16:22:37 +05:30
function handleTimingChange(depId: string, day: string, value: string) {
2026-03-17 13:11:00 +05:30
setForm({
...form,
2026-03-17 16:22:37 +05:30
departments: form.departments.map((d: any) =>
d.departmentId === depId
2026-04-06 17:46:31 +05:30
? {...d, timing: {...d.timing, [day]: value}}
2026-03-17 16:22:37 +05:30
: d,
),
2026-03-17 13:11:00 +05:30
});
}
function openAdd() {
setEditing(null);
setForm({
doctorId: "",
name: "",
designation: "",
workingStatus: "",
qualification: "",
departments: [],
});
setOpenModal(true);
}
async function openEdit(doc: any) {
setEditing(doc);
try {
2026-03-17 16:22:37 +05:30
const timingRes = await getDoctorTimingApi(doc.doctorId);
const timingData = timingRes?.data?.departments || [];
2026-03-17 13:11:00 +05:30
setForm({
2026-03-17 16:22:37 +05:30
doctorId: doc.doctorId,
name: doc.name,
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
2026-04-06 17:46:31 +05:30
departments: timingData.map((d: any) => ({
departmentId: d.departmentId,
timing: d.timing || {},
})),
2026-03-17 13:11:00 +05:30
});
setOpenModal(true);
} catch (err) {
console.error(err);
}
}
async function handleSubmit() {
try {
if (editing) {
await updateDoctorApi(editing.doctorId, form);
2026-03-17 13:11:00 +05:30
} else {
await createDoctorApi(form);
2026-03-17 13:11:00 +05:30
}
setOpenModal(false);
fetchAll();
2026-04-06 17:46:31 +05:30
} catch (error) {
console.error(error);
2026-03-17 13:11:00 +05:30
}
}
async function handleDelete(id: string) {
2026-04-06 17:46:31 +05:30
if (!confirm("Delete this doctor?")) return;
try {
await deleteDoctorApi(id);
fetchAll();
} catch (error) {
console.error(error);
}
2026-03-17 13:11:00 +05:30
}
return (
<div className="p-6 space-y-6">
{/* HEADER */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
2026-03-17 13:11:00 +05:30
<h1 className="text-2xl font-bold">Doctors</h1>
2026-04-06 17:46:31 +05:30
<div className="flex flex-wrap gap-3">
<Input
placeholder="Search doctor..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[200px]"
/>
<select
value={filterDepartment}
onChange={(e) => setFilterDepartment(e.target.value)}
2026-04-06 17:46:31 +05:30
className="flex h-10 w-[200px] rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="">All Departments</option>
{departments.map((dep) => (
<option key={dep.departmentId} value={dep.departmentId}>
{dep.name}
</option>
))}
</select>
<Button variant="outline" onClick={fetchAll} disabled={loading}>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button onClick={openAdd}>
<Plus className="mr-2 h-4 w-4" />
Add Doctor
</Button>
</div>
2026-03-17 13:11:00 +05:30
</div>
{/* TABLE */}
2026-04-06 17:46:31 +05:30
{error && (
<div className="p-4 text-red-600 bg-red-50 border rounded-md">
{error}
</div>
)}
2026-03-17 13:11:00 +05:30
<Card>
<CardHeader>
<CardTitle>Doctor List</CardTitle>
</CardHeader>
2026-04-06 17:46:31 +05:30
<CardContent className="p-0 sm:p-6">
<div className="rounded-md border overflow-x-auto max-w-full">
<Table className="w-full min-w-[800px] table-fixed">
2026-03-17 13:11:00 +05:30
<TableHeader>
<TableRow>
2026-04-06 17:46:31 +05:30
<TableHead className="w-[80px]">ID</TableHead>
<TableHead className="w-[180px]">Name</TableHead>
<TableHead className="w-[150px]">Designation</TableHead>
<TableHead className="w-[150px]">Qualification</TableHead>
<TableHead className="w-[200px]">Departments</TableHead>
<TableHead className="w-[120px]">Actions</TableHead>
2026-03-17 13:11:00 +05:30
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
2026-04-06 17:46:31 +05:30
<TableCell colSpan={6} className="text-center">
2026-03-17 13:11:00 +05:30
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : filteredDoctors.length === 0 ? (
<TableRow>
2026-04-06 17:46:31 +05:30
<TableCell
colSpan={6}
className="text-center text-muted-foreground py-10"
>
No doctors found
</TableCell>
</TableRow>
2026-03-17 13:11:00 +05:30
) : (
filteredDoctors.map((doc) => (
2026-03-17 16:22:37 +05:30
<TableRow key={doc.doctorId}>
2026-04-06 17:46:31 +05:30
<TableCell className="truncate font-mono text-xs">
{doc.doctorId}
</TableCell>
2026-03-17 16:22:37 +05:30
<TableCell>
2026-04-06 17:46:31 +05:30
<div className="font-medium truncate" title={doc.name}>
{doc.name}
</div>
<div className="text-xs text-muted-foreground truncate">
{doc.workingStatus}
</div>
2026-03-17 16:22:37 +05:30
</TableCell>
2026-03-17 13:11:00 +05:30
2026-04-06 17:46:31 +05:30
<TableCell>
<div className="truncate" title={doc.designation}>
{doc.designation || "-"}
</div>
2026-03-17 13:11:00 +05:30
</TableCell>
2026-04-06 17:46:31 +05:30
<TableCell>
<div className="truncate" title={doc.qualification}>
{doc.qualification || "-"}
</div>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1 max-h-[40px] overflow-hidden">
{doc.departments?.map((d: any) => (
<Badge
key={d.departmentId}
variant="secondary"
className="text-[10px] px-1"
>
{d.departmentName}
</Badge>
))}
{doc.departments?.length === 0 && (
<span className="text-muted-foreground">-</span>
)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
onClick={() => openEdit(doc)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(doc.doctorId)}
>
<Trash className="h-4 w-4" />
</Button>
</div>
2026-03-17 13:11:00 +05:30
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* MODAL */}
<Dialog open={openModal} onOpenChange={setOpenModal}>
2026-04-06 17:46:31 +05:30
<DialogContent className="w-full !max-w-5xl max-h-[90vh] overflow-y-auto">
2026-03-17 13:11:00 +05:30
<DialogHeader>
<DialogTitle>{editing ? "Edit Doctor" : "Add Doctor"}</DialogTitle>
</DialogHeader>
2026-04-06 17:46:31 +05:30
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-4">
<h3 className="font-semibold border-b pb-2">Basic Information</h3>
<Input
name="doctorId"
placeholder="Doctor ID"
value={form.doctorId}
onChange={handleChange}
disabled={!!editing}
/>
<Input
name="name"
placeholder="Name"
value={form.name}
onChange={handleChange}
/>
<Input
name="designation"
placeholder="Designation"
value={form.designation}
onChange={handleChange}
/>
<Input
name="workingStatus"
placeholder="Working Status (e.g. Active)"
value={form.workingStatus}
onChange={handleChange}
/>
<Input
name="qualification"
placeholder="Qualification"
value={form.qualification}
onChange={handleChange}
/>
<div className="p-4 border rounded-md bg-muted/20">
<p className="text-sm font-medium mb-3">Assign Departments</p>
<div className="grid grid-cols-2 gap-2">
{departments.map((dep) => {
const isSelected = form.departments.some(
(d: any) => d.departmentId === dep.departmentId,
);
return (
<Button
key={dep.departmentId}
type="button"
variant={isSelected ? "default" : "outline"}
size="sm"
className="justify-start"
onClick={() => handleDepartmentToggle(dep.departmentId)}
>
{dep.name}
</Button>
);
})}
</div>
</div>
2026-03-17 13:11:00 +05:30
</div>
2026-04-06 17:46:31 +05:30
<div className="space-y-4">
<h3 className="font-semibold border-b pb-2">
Working Hours / Timing
</h3>
{form.departments.length === 0 ? (
<div className="text-sm text-muted-foreground italic py-10 text-center">
Select a department to set timings
2026-03-17 16:22:37 +05:30
</div>
2026-04-06 17:46:31 +05:30
) : (
<div className="space-y-6">
{form.departments.map((dep: any) => {
const depName = departments.find(
(d) => d.departmentId === dep.departmentId,
)?.name;
return (
<div
key={dep.departmentId}
className="space-y-3 p-3 border rounded-lg bg-background shadow-sm"
>
<p className="font-bold text-primary underline">
{depName}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{DAYS.map((day) => (
<div key={day} className="space-y-1">
<label className="text-[10px] uppercase font-bold text-muted-foreground">
{day}
</label>
<Input
className="h-8 text-xs"
placeholder="e.g. 9 AM - 1 PM"
value={dep.timing?.[day] || ""}
onChange={(e) =>
handleTimingChange(
dep.departmentId,
day,
e.target.value,
)
}
/>
</div>
))}
</div>
</div>
);
})}
</div>
)}
</div>
2026-03-17 13:11:00 +05:30
</div>
2026-04-06 17:46:31 +05:30
<DialogFooter className="mt-6">
2026-03-17 13:11:00 +05:30
<Button variant="outline" onClick={() => setOpenModal(false)}>
Cancel
</Button>
<Button onClick={handleSubmit}>
2026-04-06 17:46:31 +05:30
{editing ? "Update Doctor" : "Create Doctor"}
2026-03-17 13:11:00 +05:30
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}