[1.0.0] #19

Merged
ashir merged 80 commits from dev into main 2026-04-30 18:37:18 +00:00
2 changed files with 267 additions and 209 deletions
Showing only changes of commit 1d55cfc4b8 - Show all commits
+49
View File
@@ -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<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+144 -135
View File
@@ -1,4 +1,5 @@
import {useState, useEffect, useCallback} from "react"; import {useState, useEffect, useCallback} from "react";
import {AxiosError} from "axios";
import { import {
getDoctorsApi, getDoctorsApi,
createDoctorApi, createDoctorApi,
@@ -6,7 +7,6 @@ import {
deleteDoctorApi, deleteDoctorApi,
getDoctorTimingApi, getDoctorTimingApi,
} from "@/api/doctor"; } from "@/api/doctor";
import {getDepartmentsApi} from "@/api/department"; import {getDepartmentsApi} from "@/api/department";
import { import {
@@ -17,10 +17,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } 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 {Button} from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -28,27 +26,32 @@ import {
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {
Command,
CommandGroup,
CommandItem,
CommandInput,
} from "@/components/ui/command";
import {Input} from "@/components/ui/input"; import {Input} from "@/components/ui/input";
import {Loader2, Plus, Pencil, Trash, RefreshCw} from "lucide-react"; import {Badge} from "@/components/ui/badge";
import {Loader2, RefreshCw, Plus, Pencil, Trash} from "lucide-react";
interface Department { interface Department {
departmentId: string; departmentId: string;
name: string; name: string;
} }
const DAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"additional",
];
export default function DoctorPage() { export default function DoctorPage() {
const [doctors, setDoctors] = useState<any[]>([]); const [doctors, setDoctors] = useState<any[]>([]);
const [departments, setDepartments] = useState<Department[]>([]); const [departments, setDepartments] = useState<Department[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const [editing, setEditing] = useState<any>(null); const [editing, setEditing] = useState<any>(null);
@@ -66,16 +69,20 @@ export default function DoctorPage() {
const fetchAll = useCallback(async () => { const fetchAll = useCallback(async () => {
setLoading(true); setLoading(true);
setError("");
try { try {
const [docRes, depRes] = await Promise.all([ const [docRes, depRes] = await Promise.all([
getDoctorsApi(), getDoctorsApi(),
getDepartmentsApi(), getDepartmentsApi(),
]); ]);
setDoctors(docRes?.data || []); setDoctors(docRes?.data || []);
setDepartments(depRes?.data || []); setDepartments(depRes?.data || []);
} catch (err) { } catch (err) {
console.error(err); if (err instanceof AxiosError) {
setError(err.response?.data?.message || "Failed to load data");
} else {
setError("Something went wrong");
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -101,9 +108,8 @@ export default function DoctorPage() {
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); const exists = form.departments.find((d: any) => d.departmentId === depId);
if (exists) { if (exists) {
setForm({ setForm({
...form, ...form,
@@ -124,10 +130,7 @@ export default function DoctorPage() {
...form, ...form,
departments: form.departments.map((d: any) => departments: form.departments.map((d: any) =>
d.departmentId === depId d.departmentId === depId
? { ? {...d, timing: {...d.timing, [day]: value}}
...d,
timing: {...d.timing, [day]: value},
}
: d, : d,
), ),
}); });
@@ -148,25 +151,20 @@ export default function DoctorPage() {
async function openEdit(doc: any) { async function openEdit(doc: any) {
setEditing(doc); setEditing(doc);
try { try {
const timingRes = await getDoctorTimingApi(doc.doctorId); const timingRes = await getDoctorTimingApi(doc.doctorId);
const timingData = timingRes?.data?.departments || []; const timingData = timingRes?.data?.departments || [];
const mappedDepartments = timingData.map((d: any) => ({
departmentId: d.departmentId,
timing: d.timing || {},
}));
setForm({ setForm({
doctorId: doc.doctorId, doctorId: doc.doctorId,
name: doc.name, name: doc.name,
designation: doc.designation, designation: doc.designation,
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
departments: mappedDepartments, departments: timingData.map((d: any) => ({
departmentId: d.departmentId,
timing: d.timing || {},
})),
}); });
setOpenModal(true); setOpenModal(true);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -180,18 +178,21 @@ export default function DoctorPage() {
} else { } else {
await createDoctorApi(form); await createDoctorApi(form);
} }
setOpenModal(false); setOpenModal(false);
fetchAll(); fetchAll();
} catch (err) { } catch (error) {
console.error(err); console.error(error);
} }
} }
async function handleDelete(id: string) { async function handleDelete(id: string) {
if (!confirm("Delete doctor?")) return; if (!confirm("Delete this doctor?")) return;
try {
await deleteDoctorApi(id); await deleteDoctorApi(id);
fetchAll(); fetchAll();
} catch (error) {
console.error(error);
}
} }
return ( return (
@@ -200,7 +201,7 @@ export default function DoctorPage() {
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
<h1 className="text-2xl font-bold">Doctors</h1> <h1 className="text-2xl font-bold">Doctors</h1>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-3">
<Input <Input
placeholder="Search doctor..." placeholder="Search doctor..."
value={searchText} value={searchText}
@@ -211,7 +212,7 @@ export default function DoctorPage() {
<select <select
value={filterDepartment} value={filterDepartment}
onChange={(e) => setFilterDepartment(e.target.value)} onChange={(e) => setFilterDepartment(e.target.value)}
className="border rounded px-2 py-1" 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> <option value="">All Departments</option>
{departments.map((dep) => ( {departments.map((dep) => (
@@ -234,65 +235,94 @@ export default function DoctorPage() {
</div> </div>
{/* TABLE */} {/* TABLE */}
{error && (
<div className="p-4 text-red-600 bg-red-50 border rounded-md">
{error}
</div>
)}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Doctor List</CardTitle> <CardTitle>Doctor List</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-0 sm:p-6">
<div className="overflow-x-auto"> <div className="rounded-md border overflow-x-auto max-w-full">
<Table className="min-w-[1000px]"> <Table className="w-full min-w-[800px] table-fixed">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>ID</TableHead> <TableHead className="w-[80px]">ID</TableHead>
<TableHead>Name</TableHead> <TableHead className="w-[180px]">Name</TableHead>
<TableHead>Designation</TableHead> <TableHead className="w-[150px]">Designation</TableHead>
<TableHead>Status</TableHead> <TableHead className="w-[150px]">Qualification</TableHead>
<TableHead>Qualification</TableHead> <TableHead className="w-[200px]">Departments</TableHead>
<TableHead>Departments</TableHead> <TableHead className="w-[120px]">Actions</TableHead>
<TableHead>Timing</TableHead>
<TableHead>Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center"> <TableCell colSpan={6} className="text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" /> <Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) : filteredDoctors.length === 0 ? ( ) : filteredDoctors.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center"> <TableCell
colSpan={6}
className="text-center text-muted-foreground py-10"
>
No doctors found No doctors found
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
filteredDoctors.map((doc) => ( filteredDoctors.map((doc) => (
<TableRow key={doc.doctorId}> <TableRow key={doc.doctorId}>
<TableCell>{doc.doctorId}</TableCell> <TableCell className="truncate font-mono text-xs">
<TableCell>{doc.name}</TableCell> {doc.doctorId}
<TableCell>{doc.designation}</TableCell> </TableCell>
<TableCell>{doc.workingStatus}</TableCell>
<TableCell>{doc.qualification}</TableCell>
<TableCell> <TableCell>
{doc.departments <div className="font-medium truncate" title={doc.name}>
?.map((d: any) => d.departmentName) {doc.name}
.join(", ")} </div>
</TableCell> <div className="text-xs text-muted-foreground truncate">
{doc.workingStatus}
<TableCell className="max-w-[250px] whitespace-normal">
{doc.departments?.map((d: any) => (
<div key={d.departmentId}>
<b>{d.departmentName}:</b>{" "}
{JSON.stringify(d.timing)}
</div> </div>
))}
</TableCell> </TableCell>
<TableCell className="flex gap-2"> <TableCell>
<div className="truncate" title={doc.designation}>
{doc.designation || "-"}
</div>
</TableCell>
<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 <Button
size="sm" size="sm"
variant="outline" variant="outline"
@@ -300,7 +330,6 @@ export default function DoctorPage() {
> >
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="destructive" variant="destructive"
@@ -308,6 +337,7 @@ export default function DoctorPage() {
> >
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
</Button> </Button>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))
@@ -318,15 +348,16 @@ export default function DoctorPage() {
</CardContent> </CardContent>
</Card> </Card>
{/* MODAL */}
{/* MODAL */} {/* MODAL */}
<Dialog open={openModal} onOpenChange={setOpenModal}> <Dialog open={openModal} onOpenChange={setOpenModal}>
<DialogContent className="overflow-y-auto max-h-[80vh] "> <DialogContent className="w-full !max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>{editing ? "Edit Doctor" : "Add Doctor"}</DialogTitle> <DialogTitle>{editing ? "Edit Doctor" : "Add Doctor"}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-semibold border-b pb-2">Basic Information</h3>
<Input <Input
name="doctorId" name="doctorId"
placeholder="Doctor ID" placeholder="Doctor ID"
@@ -334,7 +365,6 @@ export default function DoctorPage() {
onChange={handleChange} onChange={handleChange}
disabled={!!editing} disabled={!!editing}
/> />
<Input <Input
name="name" name="name"
placeholder="Name" placeholder="Name"
@@ -349,7 +379,7 @@ export default function DoctorPage() {
/> />
<Input <Input
name="workingStatus" name="workingStatus"
placeholder="Working Status" placeholder="Working Status (e.g. Active)"
value={form.workingStatus} value={form.workingStatus}
onChange={handleChange} onChange={handleChange}
/> />
@@ -360,87 +390,61 @@ export default function DoctorPage() {
onChange={handleChange} onChange={handleChange}
/> />
{/* Departments */} <div className="p-4 border rounded-md bg-muted/20">
<div> <p className="text-sm font-medium mb-3">Assign Departments</p>
<p className="font-medium mb-2">Departments</p> <div className="grid grid-cols-2 gap-2">
<Popover>
<PopoverTrigger asChild>
<Button className="w-full justify-between h-auto min-h-[40px]">
{form.departments.length > 0 ? (
<div className="flex flex-col items-start gap-1 text-left">
{form.departments.map((d: any) => {
const name = departments.find(
(dep) => dep.departmentId === d.departmentId,
)?.name;
return (
<span key={d.departmentId} className="text-sm">
{name}
</span>
);
})}
</div>
) : (
<span>Select Departments</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="Search department..." />
<CommandGroup className="max-h-[250px] overflow-y-auto">
{departments.map((dep) => { {departments.map((dep) => {
const selected = form.departments.some( const isSelected = form.departments.some(
(d: any) => d.departmentId === dep.departmentId, (d: any) => d.departmentId === dep.departmentId,
); );
return ( return (
<CommandItem <Button
key={dep.departmentId} key={dep.departmentId}
className="flex justify-between" type="button"
onSelect={() => variant={isSelected ? "default" : "outline"}
handleDepartmentChange(dep.departmentId) size="sm"
} className="justify-start"
onClick={() => handleDepartmentToggle(dep.departmentId)}
> >
<span>{dep.name}</span> {dep.name}
{selected && <span></span>} </Button>
</CommandItem>
); );
})} })}
</CommandGroup> </div>
</Command> </div>
</PopoverContent>
</Popover>
</div> </div>
<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
</div>
) : (
<div className="space-y-6">
{form.departments.map((dep: any) => { {form.departments.map((dep: any) => {
const depName = departments.find( const depName = departments.find(
(d) => d.departmentId === dep.departmentId, (d) => d.departmentId === dep.departmentId,
)?.name; )?.name;
return ( return (
<div <div
key={dep.departmentId} key={dep.departmentId}
className="tw-border tw-p-3 tw-rounded" className="space-y-3 p-3 border rounded-lg bg-background shadow-sm"
> >
<p className="tw-font-semibold">{depName}</p> <p className="font-bold text-primary underline">
{depName}
{[ </p>
"monday", <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
"tuesday", {DAYS.map((day) => (
"wednesday", <div key={day} className="space-y-1">
"thursday", <label className="text-[10px] uppercase font-bold text-muted-foreground">
"friday", {day}
"saturday", </label>
"sunday",
"additional",
].map((day) => (
<Input <Input
key={day} className="h-8 text-xs"
placeholder={day} placeholder="e.g. 9 AM - 1 PM"
value={dep.timing?.[day] || ""} value={dep.timing?.[day] || ""}
onChange={(e) => onChange={(e) =>
handleTimingChange( handleTimingChange(
@@ -450,18 +454,23 @@ export default function DoctorPage() {
) )
} }
/> />
</div>
))} ))}
</div> </div>
</div>
); );
})} })}
</div> </div>
)}
</div>
</div>
<DialogFooter> <DialogFooter className="mt-6">
<Button variant="outline" onClick={() => setOpenModal(false)}> <Button variant="outline" onClick={() => setOpenModal(false)}>
Cancel Cancel
</Button> </Button>
<Button onClick={handleSubmit}> <Button onClick={handleSubmit}>
{editing ? "Update" : "Create"} {editing ? "Update Doctor" : "Create Doctor"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>