fix/frontend-ui #6

Merged
kailasdevdas merged 2 commits from fix/frontend-ui into dev 2026-04-14 07:19:58 +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 {AxiosError} from "axios";
import {
getDoctorsApi,
createDoctorApi,
@@ -6,7 +7,6 @@ import {
deleteDoctorApi,
getDoctorTimingApi,
} from "@/api/doctor";
import {getDepartmentsApi} from "@/api/department";
import {
@@ -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 {
Dialog,
DialogContent,
@@ -28,27 +26,32 @@ import {
DialogTitle,
DialogFooter,
} 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 {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 {
departmentId: string;
name: string;
}
const DAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"additional",
];
export default function DoctorPage() {
const [doctors, setDoctors] = useState<any[]>([]);
const [departments, setDepartments] = useState<Department[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [openModal, setOpenModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
@@ -66,16 +69,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);
}
@@ -101,9 +108,8 @@ export default function DoctorPage() {
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,
@@ -124,10 +130,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 +151,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,18 +178,21 @@ 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;
if (!confirm("Delete this doctor?")) return;
try {
await deleteDoctorApi(id);
fetchAll();
} catch (error) {
console.error(error);
}
}
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">
<h1 className="text-2xl font-bold">Doctors</h1>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-3">
<Input
placeholder="Search doctor..."
value={searchText}
@@ -211,7 +212,7 @@ export default function DoctorPage() {
<select
value={filterDepartment}
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>
{departments.map((dep) => (
@@ -234,65 +235,94 @@ export default function DoctorPage() {
</div>
{/* TABLE */}
{error && (
<div className="p-4 text-red-600 bg-red-50 border rounded-md">
{error}
</div>
)}
<Card>
<CardHeader>
<CardTitle>Doctor List</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table className="min-w-[1000px]">
<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">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Designation</TableHead>
<TableHead>Status</TableHead>
<TableHead>Qualification</TableHead>
<TableHead>Departments</TableHead>
<TableHead>Timing</TableHead>
<TableHead>Actions</TableHead>
<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>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={8} className="text-center">
<TableCell colSpan={6} className="text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : filteredDoctors.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center">
<TableCell
colSpan={6}
className="text-center text-muted-foreground py-10"
>
No doctors found
</TableCell>
</TableRow>
) : (
filteredDoctors.map((doc) => (
<TableRow key={doc.doctorId}>
<TableCell>{doc.doctorId}</TableCell>
<TableCell>{doc.name}</TableCell>
<TableCell>{doc.designation}</TableCell>
<TableCell>{doc.workingStatus}</TableCell>
<TableCell>{doc.qualification}</TableCell>
<TableCell className="truncate font-mono text-xs">
{doc.doctorId}
</TableCell>
<TableCell>
{doc.departments
?.map((d: any) => d.departmentName)
.join(", ")}
</TableCell>
<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 className="font-medium truncate" title={doc.name}>
{doc.name}
</div>
<div className="text-xs text-muted-foreground truncate">
{doc.workingStatus}
</div>
))}
</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
size="sm"
variant="outline"
@@ -300,7 +330,6 @@ export default function DoctorPage() {
>
<Pencil className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="destructive"
@@ -308,6 +337,7 @@ export default function DoctorPage() {
>
<Trash className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
@@ -318,15 +348,16 @@ export default function DoctorPage() {
</CardContent>
</Card>
{/* MODAL */}
{/* MODAL */}
<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>
<DialogTitle>{editing ? "Edit Doctor" : "Add Doctor"}</DialogTitle>
</DialogHeader>
<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"
@@ -334,7 +365,6 @@ export default function DoctorPage() {
onChange={handleChange}
disabled={!!editing}
/>
<Input
name="name"
placeholder="Name"
@@ -349,7 +379,7 @@ export default function DoctorPage() {
/>
<Input
name="workingStatus"
placeholder="Working Status"
placeholder="Working Status (e.g. Active)"
value={form.workingStatus}
onChange={handleChange}
/>
@@ -360,87 +390,61 @@ export default function DoctorPage() {
onChange={handleChange}
/>
{/* Departments */}
<div>
<p className="font-medium mb-2">Departments</p>
<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">
<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 selected = form.departments.some(
const isSelected = form.departments.some(
(d: any) => d.departmentId === dep.departmentId,
);
return (
<CommandItem
<Button
key={dep.departmentId}
className="flex justify-between"
onSelect={() =>
handleDepartmentChange(dep.departmentId)
}
type="button"
variant={isSelected ? "default" : "outline"}
size="sm"
className="justify-start"
onClick={() => handleDepartmentToggle(dep.departmentId)}
>
<span>{dep.name}</span>
{selected && <span></span>}
</CommandItem>
{dep.name}
</Button>
);
})}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
</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) => {
const depName = departments.find(
(d) => d.departmentId === dep.departmentId,
)?.name;
return (
<div
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>
{[
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"additional",
].map((day) => (
<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
key={day}
placeholder={day}
className="h-8 text-xs"
placeholder="e.g. 9 AM - 1 PM"
value={dep.timing?.[day] || ""}
onChange={(e) =>
handleTimingChange(
@@ -450,18 +454,23 @@ export default function DoctorPage() {
)
}
/>
</div>
))}
</div>
</div>
);
})}
</div>
)}
</div>
</div>
<DialogFooter>
<DialogFooter className="mt-6">
<Button variant="outline" onClick={() => setOpenModal(false)}>
Cancel
</Button>
<Button onClick={handleSubmit}>
{editing ? "Update" : "Create"}
{editing ? "Update Doctor" : "Create Doctor"}
</Button>
</DialogFooter>
</DialogContent>