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

1132 lines
31 KiB
TypeScript
Raw Normal View History

2026-04-08 16:30:50 +05:30
import { useState, useEffect, useCallback } from "react";
import { AxiosError } from "axios";
2026-04-14 17:33:21 +05:30
import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
2026-03-17 13:11:00 +05:30
import {
getDoctorsApi,
createDoctorApi,
updateDoctorApi,
getDoctorTimingApi,
} from "@/api/doctor";
2026-04-08 16:30:50 +05:30
import { getDepartmentsApi } from "@/api/department";
2026-03-17 13:11:00 +05:30
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
2026-04-08 16:30:50 +05:30
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
2026-03-17 13:11:00 +05:30
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
2026-04-08 16:30:50 +05:30
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
2026-04-08 16:30:50 +05:30
import {
Loader2,
RefreshCw,
Plus,
Pencil,
ChevronLeft,
ChevronRight,
} from "lucide-react";
2026-05-20 10:15:53 +05:30
import { Textarea } from "@/components/ui/textarea";
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
2026-04-08 16:30:50 +05:30
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
2026-03-17 13:11:00 +05:30
const [form, setForm] = useState<any>({
doctorId: "",
name: "",
2026-04-14 17:33:21 +05:30
image: "",
2026-03-17 13:11:00 +05:30
designation: "",
workingStatus: "",
qualification: "",
isActive: true,
globalSortOrder: 0,
2026-03-17 13:11:00 +05:30
departments: [],
2026-05-20 10:15:53 +05:30
professionalSummary: "",
seoTitle: "",
metaDescription: "",
ogTitle: "",
ogDescription: "",
ogImage: "",
specializations: [
{
name: "",
description: "",
},
],
focusKeyphrase: "",
slug: "",
tags: [],
2026-03-17 13:11:00 +05:30
});
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]);
2026-05-11 16:27:20 +05:30
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;
})
.sort((a, b) => {
if (!filterDepartment) {
return a.globalSortOrder - b.globalSortOrder;
}
2026-05-11 16:27:20 +05:30
const aDept = a.departments.find(
(d: any) => d.departmentId === filterDepartment,
);
2026-05-11 16:27:20 +05:30
const bDept = b.departments.find(
(d: any) => d.departmentId === filterDepartment,
);
return (
(aDept?.deptSortOrder ?? Number.MAX_SAFE_INTEGER) -
(bDept?.deptSortOrder ?? Number.MAX_SAFE_INTEGER)
);
});
2026-04-08 16:30:50 +05:30
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);
2026-03-17 13:11:00 +05:30
function handleChange(e: any) {
2026-05-20 10:15:53 +05:30
let value =
e.target.type === "number" ? Number(e.target.value) : e.target.value;
2026-05-20 10:15:53 +05:30
if (e.target.name === "slug") {
value = value
.toLowerCase()
.replace(/\s+/g, "-") // replace spaces with -
.replace(/[^\w-]+/g, "") // remove special chars
.replace(/--+/g, "-"); // remove duplicate -
}
setForm({ ...form, [e.target.name]: value });
2026-03-17 13:11:00 +05:30
}
const handleToggleStatus = async (doc: any) => {
try {
2026-05-13 14:19:42 +05:30
const newStatus = !doc.isActive;
const payload = {
isActive: newStatus,
};
2026-05-22 16:34:37 +05:30
await updateDoctorApi(doc.doctorId, payload, "toggleStatus");
2026-05-13 14:19:42 +05:30
fetchAll();
} catch (err) {
console.error("Failed to update status", err);
}
};
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, sortOrder: 0, timing: {} },
],
2026-03-17 16:22:37 +05:30
});
}
2026-03-17 13:11:00 +05:30
}
function handleDeptSortChange(depId: string, value: string) {
setForm({
...form,
departments: form.departments.map((d: any) =>
d.departmentId === depId ? { ...d, sortOrder: Number(value) } : d,
),
});
}
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-08 16:30:50 +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: "",
2026-04-14 17:33:21 +05:30
image: "",
2026-03-17 13:11:00 +05:30
designation: "",
workingStatus: "",
qualification: "",
2026-05-20 10:15:53 +05:30
experience: "",
professionalSummary: "",
isActive: true,
globalSortOrder: 0,
2026-05-20 10:15:53 +05:30
specializations: [
{
name: "",
description: "",
},
],
2026-03-17 13:11:00 +05:30
departments: [],
2026-05-20 10:15:53 +05:30
seoTitle: "",
metaDescription: "",
focusKeyphrase: "",
slug: "",
tags: [],
ogTitle: "",
ogDescription: "",
ogImage: "",
2026-03-17 13:11:00 +05:30
});
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,
2026-04-14 17:33:21 +05:30
image: doc.image || "",
2026-03-17 16:22:37 +05:30
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
isActive: doc.isActive ?? true,
globalSortOrder: doc.globalSortOrder ?? 0,
2026-05-20 10:15:53 +05:30
experience: doc.experience || "",
professionalSummary: doc.professionalSummary || "",
seoTitle: doc.seo?.seoTitle || "",
metaDescription: doc.seo?.metaDescription || "",
focusKeyphrase: doc.seo?.focusKeyphrase || "",
slug: doc.seo?.slug || "",
tags: doc.seo?.tags || [],
ogTitle: doc.seo?.ogTitle || "",
ogDescription: doc.seo?.ogDescription || "",
ogImage: doc.seo?.ogImage || "",
specializations: doc.specializations?.length
? doc.specializations.map((item: any) => ({
name: item.name || "",
description: item.description || "",
}))
: [
{
name: "",
description: "",
},
],
2026-04-06 17:46:31 +05:30
departments: timingData.map((d: any) => ({
departmentId: d.departmentId,
sortOrder: d.deptSortOrder ?? 0,
2026-04-06 17:46:31 +05:30
timing: d.timing || {},
})),
2026-03-17 13:11:00 +05:30
});
setOpenModal(true);
} catch (err) {
console.error("Error fetching doctor details:", err);
2026-03-17 13:11:00 +05:30
}
}
2026-05-20 10:15:53 +05:30
console.log("Current form state:", form); // Debug log to check form state
2026-03-17 13:11:00 +05:30
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
}
}
return (
<div className="p-6 space-y-6">
2026-04-08 16:30:50 +05:30
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<h1 className="text-3xl font-bold">Doctors</h1>
2026-03-17 13:11:00 +05:30
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)}
2026-04-08 16:30:50 +05:30
className="w-[250px] text-base"
/>
<select
value={filterDepartment}
onChange={(e) => setFilterDepartment(e.target.value)}
2026-04-08 16:30:50 +05:30
className="flex h-10 w-[220px] rounded-md border border-input bg-background px-3 py-2 text-base 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>
2026-04-08 16:30:50 +05:30
<Button
variant="outline"
onClick={fetchAll}
disabled={loading}
className="text-base"
>
<RefreshCw className="mr-2 h-5 w-5" />
Refresh
</Button>
2026-04-08 16:30:50 +05:30
<Button onClick={openAdd} className="text-base">
<Plus className="mr-2 h-5 w-5" />
Add Doctor
</Button>
</div>
2026-03-17 13:11:00 +05:30
</div>
2026-04-06 17:46:31 +05:30
{error && (
2026-04-08 16:30:50 +05:30
<div className="p-4 text-red-600 bg-red-50 border rounded-md text-base">
2026-04-06 17:46:31 +05:30
{error}
</div>
)}
2026-03-17 13:11:00 +05:30
<Card>
<CardHeader>
2026-04-08 16:30:50 +05:30
<CardTitle className="text-xl">Doctor List</CardTitle>
2026-03-17 13:11:00 +05:30
</CardHeader>
2026-04-08 16:30:50 +05:30
<CardContent className="p-0 sm:p-6 space-y-4">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[1100px] table-fixed border-separate border-spacing-0">
2026-04-08 16:30:50 +05:30
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
2026-03-17 13:11:00 +05:30
<TableRow>
<TableHead className="w-[80px] bg-background text-sm font-bold">
Priority{" "}
2026-04-08 16:30:50 +05:30
</TableHead>
<TableHead className="w-[180px] bg-background text-sm font-bold">
Doctor Info
2026-04-08 16:30:50 +05:30
</TableHead>
<TableHead className="w-[150px] bg-background text-sm font-bold">
Designation
2026-04-08 16:30:50 +05:30
</TableHead>
<TableHead className="w-[220px] bg-background text-sm font-bold">
Departments (Hierarchy)
2026-04-08 16:30:50 +05:30
</TableHead>
<TableHead className="w-[80px] bg-background text-sm font-bold">
Status (Active)
</TableHead>
<TableHead className="w-[80px] bg-background text-right text-sm font-bold">
2026-04-08 16:30:50 +05:30
Actions
</TableHead>
2026-03-17 13:11:00 +05:30
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
2026-04-08 16:30:50 +05:30
<TableCell colSpan={6} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
2026-03-17 13:11:00 +05:30
</TableCell>
</TableRow>
2026-04-08 16:30:50 +05:30
) : currentItems.length === 0 ? (
<TableRow>
2026-04-06 17:46:31 +05:30
<TableCell
colSpan={6}
2026-04-08 16:30:50 +05:30
className="text-center text-muted-foreground py-10 text-base"
2026-04-06 17:46:31 +05:30
>
No doctors found
</TableCell>
</TableRow>
2026-03-17 13:11:00 +05:30
) : (
2026-04-08 16:30:50 +05:30
currentItems.map((doc) => (
<TableRow key={doc.doctorId} className="hover:bg-muted/50">
<TableCell className="font-mono text-sm">
{doc.globalSortOrder}
2026-04-06 17:46:31 +05:30
</TableCell>
2026-03-17 16:22:37 +05:30
<TableCell>
2026-04-08 16:30:50 +05:30
<div
className="font-semibold text-base truncate"
title={doc.name}
>
2026-04-06 17:46:31 +05:30
{doc.name}
</div>
<div className="text-xs text-muted-foreground truncate font-mono">
{doc.doctorId}
2026-04-06 17:46:31 +05:30
</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>
2026-04-08 16:30:50 +05:30
<div
className="truncate text-sm font-medium"
title={doc.designation}
2026-04-08 16:30:50 +05:30
>
{doc.designation || "-"}
</div>
<div className="text-xs italic text-muted-foreground truncate">
{doc.workingStatus}
2026-04-06 17:46:31 +05:30
</div>
</TableCell>
<TableCell>
2026-04-08 16:30:50 +05:30
<div className="flex flex-wrap gap-1">
2026-04-06 17:46:31 +05:30
{doc.departments?.map((d: any) => (
<Badge
key={d.departmentId}
variant="secondary"
className="text-xs px-2 h-6 leading-none flex items-center gap-1"
2026-04-06 17:46:31 +05:30
>
{d.departmentName}
<span className="bg-primary text-primary-foreground px-1 rounded-full text-[10px]">
{d.deptSortOrder}
</span>
2026-04-06 17:46:31 +05:30
</Badge>
))}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Switch
checked={doc.isActive}
onCheckedChange={() => handleToggleStatus(doc)}
/>
<Badge
variant={doc.isActive ? "default" : "secondary"}
>
{doc.isActive ? "Active" : "Hidden"}
</Badge>
</div>
</TableCell>
2026-04-06 17:46:31 +05:30
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
2026-04-08 16:30:50 +05:30
size="icon"
variant="ghost"
className="h-9 w-9"
2026-04-06 17:46:31 +05:30
onClick={() => openEdit(doc)}
>
<Pencil className="h-4 w-4" />
</Button>
</div>
2026-03-17 13:11:00 +05:30
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
2026-04-08 16:30:50 +05:30
{!loading && filteredDoctors.length > 0 && (
<div className="flex items-center justify-between px-2 py-6 border-t">
<div className="text-base text-muted-foreground">
Showing{" "}
<span className="font-semibold">{indexOfFirstItem + 1}</span> to{" "}
<span className="font-semibold">
{Math.min(indexOfLastItem, filteredDoctors.length)}
</span>{" "}
of{" "}
<span className="font-semibold">{filteredDoctors.length}</span>{" "}
doctors
</div>
<div className="flex items-center gap-6">
<div className="text-base font-semibold">
Page {currentPage} of {totalPages}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
className="h-10 w-10"
onClick={() =>
setCurrentPage((prev) => Math.max(prev - 1, 1))
}
disabled={currentPage === 1}
>
<ChevronLeft className="h-5 w-5" />
</Button>
<Button
variant="outline"
size="icon"
className="h-10 w-10"
onClick={() =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
disabled={currentPage === totalPages || totalPages === 0}
>
<ChevronRight className="h-5 w-5" />
</Button>
</div>
</div>
</div>
)}
2026-03-17 13:11:00 +05:30
</CardContent>
</Card>
<Dialog open={openModal} onOpenChange={setOpenModal}>
2026-04-14 17:33:21 +05:30
<DialogContent className="w-full !max-w-5xl h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="p-6 border-b bg-background z-10">
2026-04-08 16:30:50 +05:30
<DialogTitle className="text-2xl">
{editing ? "Edit Doctor" : "Add Doctor"}
</DialogTitle>
2026-03-17 13:11:00 +05:30
</DialogHeader>
2026-04-14 17:33:21 +05:30
<div className="flex-1 overflow-y-auto p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2">
Profile & Visibility
2026-04-14 17:33:21 +05:30
</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-semibold">
Doctor Photo
</label>
<BytescaleUploader
value={form.image}
folderPath="/doctors"
onChange={(url) => setForm({ ...form, image: url })}
/>
</div>
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30">
<Label
htmlFor="isActive"
className="text-base font-semibold cursor-pointer"
>
Active
</Label>
<Switch
id="isActive"
checked={form.isActive}
onCheckedChange={(val) =>
setForm({ ...form, isActive: val })
}
/>
</div>
<div className="space-y-1">
2026-05-11 10:57:52 +05:30
<Label
htmlFor="globalSortOrder"
className="text-sm font-semibold"
>
Sort Priority (Lower numbers show first)
</Label>
<Input
id="globalSortOrder"
name="globalSortOrder"
type="number"
value={form.globalSortOrder}
onChange={handleChange}
className="text-base"
/>
</div>
2026-04-14 17:33:21 +05:30
<div className="space-y-1">
<label className="text-sm font-semibold">Doctor ID</label>
<Input
name="doctorId"
placeholder="GG-DOC-001"
value={form.doctorId}
onChange={handleChange}
disabled={!!editing}
className="text-base"
/>
</div>
<div className="space-y-1">
<label className="text-sm font-semibold">Full Name</label>
<Input
name="name"
placeholder="Dr. John Doe"
value={form.name}
onChange={handleChange}
className="text-base"
/>
</div>
<div className="space-y-1">
<label className="text-sm font-semibold">Designation</label>
<Input
name="designation"
placeholder="Senior Consultant"
value={form.designation}
onChange={handleChange}
className="text-base"
/>
</div>
<div className="space-y-1">
<label className="text-sm font-semibold">
Working Status
</label>
<Input
name="workingStatus"
placeholder="Active / On Call"
value={form.workingStatus}
onChange={handleChange}
className="text-base"
/>
</div>
2026-05-20 10:15:53 +05:30
<div className="space-y-1">
<label className="text-sm font-semibold">
Brief Professional Summary
</label>
<Textarea
name="professionalSummary"
placeholder="Write a brief professional summary about the doctor..."
value={form.professionalSummary || ""}
onChange={(e) =>
setForm({
...form,
professionalSummary: e.target.value,
})
}
className="min-h-[120px] text-base"
/>
</div>
2026-04-14 17:33:21 +05:30
<div className="space-y-1">
<label className="text-sm font-semibold">
Qualification
</label>
<Input
name="qualification"
placeholder="MBBS, MD"
value={form.qualification}
onChange={handleChange}
className="text-base"
/>
</div>
2026-05-20 10:15:53 +05:30
<div className="space-y-1">
<label className="text-sm font-semibold">
Years of Experience
</label>
<Input
name="experience"
type="number"
min={0}
placeholder="e.g. 15"
value={form.experience || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
Enter total years of professional experience
</p>
</div>
2026-04-08 16:30:50 +05:30
</div>
2026-04-14 17:33:21 +05:30
<div className="p-5 border rounded-md bg-muted/20">
<p className="text-base font-bold mb-4">Assign Departments</p>
<div className="grid grid-cols-2 gap-3">
{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 text-sm min-h-11 whitespace-normal break-words text-left py-2"
onClick={() =>
handleDepartmentToggle(dep.departmentId)
}
>
{dep.name}
</Button>
);
})}
</div>
2026-04-06 17:46:31 +05:30
</div>
2026-05-20 10:15:53 +05:30
<div className="space-y-4 p-5 border rounded-md bg-muted/20">
<div className="flex items-center justify-between">
<p className="text-base font-bold">Specializations</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
setForm({
...form,
specializations: [
...(form.specializations || []),
{
name: "",
description: "",
},
],
})
}
>
+ Add
</Button>
</div>
{form.specializations?.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
No specializations added
</p>
) : (
<div className="space-y-4">
{form.specializations?.map(
(specialization: any, index: number) => (
<div
key={index}
className="border rounded-lg p-4 space-y-3 bg-background"
>
<div className="flex items-center justify-between">
<p className="font-medium text-sm">
Specialization {index + 1}
</p>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
const updated = form.specializations.filter(
(_: any, i: number) => i !== index,
);
setForm({
...form,
specializations: updated,
});
}}
>
Remove
</Button>
</div>
<div className="space-y-2">
<Label>Name</Label>
<Input
placeholder="e.g. Cardiology"
value={specialization.name}
onChange={(e) => {
const updated = [...form.specializations];
updated[index].name = e.target.value;
setForm({
...form,
specializations: updated,
});
}}
/>
</div>
<div className="space-y-2">
<Label>Doctor-specific Description</Label>
<Textarea
placeholder="Describe how this doctor specializes in this area..."
value={specialization.description}
onChange={(e) => {
const updated = [...form.specializations];
updated[index].description = e.target.value;
setForm({
...form,
specializations: updated,
});
}}
/>
</div>
</div>
),
)}
</div>
)}
</div>
<div className="space-y-4 p-5 border rounded-md bg-muted/20">
<div className="flex items-center justify-between">
<p className="text-base font-bold">SEO Settings</p>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">SEO Title</Label>
<Input
name="seoTitle"
placeholder="Best Cardiologist in Kochi | Dr John Doe"
value={form.seoTitle || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
Title shown in Google search results
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
Meta Description
</Label>
<Textarea
name="metaDescription"
placeholder="Short description shown in Google search..."
value={form.metaDescription || ""}
onChange={(e) =>
setForm({
...form,
metaDescription: e.target.value,
})
}
className="min-h-[100px] text-base"
/>
<p className="text-xs text-muted-foreground">
Recommended: 150160 characters
</p>
</div>
<div className="border-t pt-5 space-y-4">
<div className="flex items-center justify-between">
<p className="text-base font-bold">
Open Graph (Social Preview)
</p>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Title</Label>
<Input
name="ogTitle"
placeholder="Title for WhatsApp / Facebook sharing"
value={form.ogTitle || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
If empty, SEO title will be used
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
OG Description
</Label>
<Textarea
name="ogDescription"
placeholder="Description for social sharing..."
value={form.ogDescription || ""}
onChange={(e) =>
setForm({
...form,
ogDescription: e.target.value,
})
}
className="min-h-[100px] text-base"
/>
<p className="text-xs text-muted-foreground">
If empty, meta description will be used
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Image</Label>
<BytescaleUploader
value={form.ogImage}
folderPath="/doctor-og"
onChange={(url) =>
setForm({
...form,
ogImage: url,
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
Focus Keyphrase
</Label>
<Input
name="focusKeyphrase"
placeholder="best cardiologist in kochi"
value={form.focusKeyphrase || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
Main keyword people may search in Google
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">URL Slug</Label>
<Input
name="slug"
placeholder="dr-john-doe"
value={form.slug || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
URL:
<span className="font-medium">
{" "}
/doctors/
{form.slug || "doctor-slug"}
</span>
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
Tags / Keywords
</Label>
<div className="flex flex-wrap gap-2 border rounded-md p-3 min-h-[48px] bg-background">
{form.tags?.map((tag: string, index: number) => (
<div
key={index}
className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm flex items-center gap-2"
>
{tag}
<button
type="button"
onClick={() => {
const updated = form.tags.filter(
(_: string, i: number) => i !== index,
);
setForm({
...form,
tags: updated,
});
}}
>
×
</button>
</div>
))}
<Input
placeholder="Type keyword and press Enter"
className="border-0 shadow-none focus-visible:ring-0 min-w-[220px]"
onKeyDown={(e) => {
if (
e.key === "Enter" &&
e.currentTarget.value.trim()
) {
e.preventDefault();
setForm({
...form,
tags: [
...(form.tags || []),
e.currentTarget.value.trim(),
],
});
e.currentTarget.value = "";
}
}}
/>
</div>
</div>
</div>
2026-04-06 17:46:31 +05:30
</div>
2026-03-17 13:11:00 +05:30
2026-04-14 17:33:21 +05:30
<div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2">
Department Hierarchy & Timing
2026-04-14 17:33:21 +05:30
</h3>
{form.departments.length === 0 ? (
<div className="text-base text-muted-foreground italic py-24 text-center border-2 border-dashed rounded-lg">
Select a department to configure hierarchy and timing
2026-04-14 17:33:21 +05:30
</div>
) : (
<div className="space-y-8">
{form.departments.map((dep: any) => {
const depName = departments.find(
(d) => d.departmentId === dep.departmentId,
)?.name;
return (
<div
key={dep.departmentId}
className="space-y-4 p-5 border rounded-lg bg-background shadow-sm border-primary/20"
2026-04-14 17:33:21 +05:30
>
<div className="flex items-center justify-between border-b pb-2">
2026-04-14 17:33:21 +05:30
<p className="font-bold text-base text-primary">
{depName}
</p>
<div className="flex items-center gap-2">
<Label className="text-xs font-bold">
Hierarchy Order:
</Label>
<Input
type="number"
className="w-20 h-8 text-sm"
value={dep.sortOrder}
onChange={(e) =>
handleDeptSortChange(
dep.departmentId,
e.target.value,
)
}
/>
</div>
2026-04-14 17:33:21 +05:30
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3">
{DAYS.map((day) => (
<div key={day} className="space-y-1">
<label className="text-xs uppercase font-bold text-muted-foreground">
{day}
</label>
<Input
className="h-9 text-sm"
placeholder="e.g. 09:00 AM - 01:00 PM"
value={dep.timing?.[day] || ""}
onChange={(e) =>
handleTimingChange(
dep.departmentId,
day,
e.target.value,
)
}
/>
</div>
))}
</div>
2026-04-06 17:46:31 +05:30
</div>
2026-04-14 17:33:21 +05:30
);
})}
</div>
)}
</div>
2026-04-06 17:46:31 +05:30
</div>
2026-03-17 13:11:00 +05:30
</div>
2026-04-14 17:33:21 +05:30
<DialogFooter className="p-6 border-t bg-background z-10 mt-0">
2026-04-08 16:30:50 +05:30
<Button
variant="ghost"
onClick={() => setOpenModal(false)}
className="text-base"
>
2026-03-17 13:11:00 +05:30
Cancel
</Button>
2026-04-08 16:30:50 +05:30
<Button onClick={handleSubmit} className="px-10 text-base">
{editing ? "Save Changes" : "Create Doctor Profile"}
2026-03-17 13:11:00 +05:30
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}