mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-13 08:58:31 +00:00
575 lines
22 KiB
TypeScript
575 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useCallback, startTransition } from "react";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { ChevronLeft, ChevronRight, Plus, Trash2, RefreshCw, Ban, Check } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
import {
|
||
adminGetSchedule, adminCreateSchedule, adminDeleteSchedule,
|
||
adminListSlots, adminGenerateSlots, adminUpdateSlot, adminDeleteSlot,
|
||
adminGetBlockedDates, adminAddBlockedDate, adminRemoveBlockedDate,
|
||
WeeklySchedule, BlockedDate, TimeSlotApi,
|
||
} from "@/lib/api/bookings";
|
||
import { useLanguage } from "@/contexts/LanguageContext";
|
||
|
||
const DURATIONS = [30, 45, 60, 90, 120];
|
||
|
||
function toDateStr(d: Date): string {
|
||
return d.toISOString().slice(0, 10);
|
||
}
|
||
|
||
function monthStart(year: number, month: number) {
|
||
return new Date(year, month, 1);
|
||
}
|
||
|
||
function daysInMonth(year: number, month: number) {
|
||
return new Date(year, month + 1, 0).getDate();
|
||
}
|
||
|
||
// Monday-first weekday index (0=Mon, 6=Sun)
|
||
function weekdayMon(date: Date) {
|
||
return (date.getDay() + 6) % 7;
|
||
}
|
||
|
||
// ── Schedule Tab ──────────────────────────────────────────────────────────────
|
||
|
||
function ScheduleTab() {
|
||
const { t } = useLanguage();
|
||
const DAY_NAMES = Array.from({ length: 7 }, (_, i) => t(`admin.day.${i}`));
|
||
|
||
const [schedule, setSchedule] = useState<WeeklySchedule[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [form, setForm] = useState({
|
||
day_of_week: 0,
|
||
start_time: "09:00",
|
||
end_time: "18:00",
|
||
slot_duration_minutes: 60,
|
||
});
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
const [genFrom, setGenFrom] = useState(toDateStr(new Date()));
|
||
const [genTo, setGenTo] = useState(() => {
|
||
const d = new Date();
|
||
d.setDate(d.getDate() + 30);
|
||
return toDateStr(d);
|
||
});
|
||
const [generating, setGenerating] = useState(false);
|
||
|
||
useEffect(() => {
|
||
adminGetSchedule()
|
||
.then(setSchedule)
|
||
.catch(() => toast.error(t("admin.error")))
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
const addEntry = async () => {
|
||
setSaving(true);
|
||
try {
|
||
const entry = await adminCreateSchedule({
|
||
day_of_week: Number(form.day_of_week),
|
||
start_time: form.start_time + ":00",
|
||
end_time: form.end_time + ":00",
|
||
slot_duration_minutes: Number(form.slot_duration_minutes),
|
||
});
|
||
setSchedule((prev) => [...prev, entry].sort((a, b) => a.day_of_week - b.day_of_week));
|
||
toast.success(t("admin.planning.schedule_added"));
|
||
} catch {
|
||
toast.error(t("admin.error"));
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const removeEntry = async (id: string) => {
|
||
try {
|
||
await adminDeleteSchedule(id);
|
||
setSchedule((prev) => prev.filter((e) => e.id !== id));
|
||
toast.success(t("admin.planning.schedule_deleted"));
|
||
} catch {
|
||
toast.error(t("admin.error"));
|
||
}
|
||
};
|
||
|
||
const generate = async () => {
|
||
setGenerating(true);
|
||
try {
|
||
const res = await adminGenerateSlots(genFrom, genTo);
|
||
toast.success(t("admin.planning.generated", { n: res.created }));
|
||
} catch {
|
||
toast.error(t("admin.error"));
|
||
} finally {
|
||
setGenerating(false);
|
||
}
|
||
};
|
||
|
||
const grouped = DAY_NAMES.map((name, idx) => ({
|
||
name,
|
||
entries: schedule.filter((e) => e.day_of_week === idx),
|
||
}));
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{t("admin.planning.weekly_title")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loading ? (
|
||
<div className="space-y-2">
|
||
{Array.from({ length: 3 }).map((_, i) => (
|
||
<div key={i} className="h-10 bg-muted animate-pulse rounded" />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{grouped.map(({ name, entries }) => (
|
||
<div key={name} className="flex items-start gap-3">
|
||
<span className="w-24 text-sm font-medium pt-1 shrink-0">{name}</span>
|
||
<div className="flex flex-wrap gap-2 flex-1">
|
||
{entries.length === 0 ? (
|
||
<span className="text-xs text-muted-foreground pt-1">{t("admin.planning.not_available")}</span>
|
||
) : (
|
||
entries.map((e) => (
|
||
<div key={e.id} className="flex items-center gap-1.5 bg-muted rounded px-2 py-1 text-xs">
|
||
<span>{e.start_time.slice(0, 5)} – {e.end_time.slice(0, 5)}</span>
|
||
<span className="text-muted-foreground">({e.slot_duration_minutes} min)</span>
|
||
<button
|
||
onClick={() => removeEntry(e.id)}
|
||
className="text-muted-foreground hover:text-destructive ml-1"
|
||
>
|
||
<Trash2 className="h-3 w-3" />
|
||
</button>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{t("admin.planning.add_title")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.day")}</Label>
|
||
<select
|
||
className="w-full mt-1 text-sm border border-input rounded-md px-3 py-2 bg-background"
|
||
value={form.day_of_week}
|
||
onChange={(e) => setForm((f) => ({ ...f, day_of_week: Number(e.target.value) }))}
|
||
>
|
||
{DAY_NAMES.map((d, i) => (
|
||
<option key={i} value={i}>{d}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.start")}</Label>
|
||
<Input
|
||
type="time"
|
||
className="mt-1"
|
||
value={form.start_time}
|
||
onChange={(e) => setForm((f) => ({ ...f, start_time: e.target.value }))}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.end")}</Label>
|
||
<Input
|
||
type="time"
|
||
className="mt-1"
|
||
value={form.end_time}
|
||
onChange={(e) => setForm((f) => ({ ...f, end_time: e.target.value }))}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.duration_min")}</Label>
|
||
<select
|
||
className="w-full mt-1 text-sm border border-input rounded-md px-3 py-2 bg-background"
|
||
value={form.slot_duration_minutes}
|
||
onChange={(e) => setForm((f) => ({ ...f, slot_duration_minutes: Number(e.target.value) }))}
|
||
>
|
||
{DURATIONS.map((d) => (
|
||
<option key={d} value={d}>{d} min</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<Button onClick={addEntry} disabled={saving} className="mt-3" size="sm">
|
||
<Plus className="h-4 w-4 mr-1.5" />
|
||
{saving ? t("admin.planning.adding") : t("admin.add")}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{t("admin.planning.generate_title")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-xs text-muted-foreground mb-3">{t("admin.planning.generate_desc")}</p>
|
||
<div className="flex flex-wrap gap-3 items-end">
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.from")}</Label>
|
||
<Input type="date" className="mt-1 w-40" value={genFrom}
|
||
onChange={(e) => setGenFrom(e.target.value)} />
|
||
</div>
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.to")}</Label>
|
||
<Input type="date" className="mt-1 w-40" value={genTo}
|
||
onChange={(e) => setGenTo(e.target.value)} />
|
||
</div>
|
||
<Button onClick={generate} disabled={generating} size="sm">
|
||
<RefreshCw className={`h-4 w-4 mr-1.5 ${generating ? "animate-spin" : ""}`} />
|
||
{generating ? t("admin.planning.generating") : t("admin.planning.generate_btn")}
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Calendar Tab ──────────────────────────────────────────────────────────────
|
||
|
||
function CalendarTab() {
|
||
const { t, locale } = useLanguage();
|
||
const DAY_NAMES = Array.from({ length: 7 }, (_, i) => t(`admin.day.${i}`));
|
||
const MONTH_NAMES = Array.from({ length: 12 }, (_, i) => t(`admin.month.${i}`));
|
||
|
||
const today = new Date();
|
||
const [year, setYear] = useState(today.getFullYear());
|
||
const [month, setMonth] = useState(today.getMonth());
|
||
const [slots, setSlots] = useState<TimeSlotApi[]>([]);
|
||
const [loadingSlots, setLoadingSlots] = useState(false);
|
||
const [selectedDay, setSelectedDay] = useState<string | null>(null);
|
||
|
||
const loadSlots = useCallback(() => {
|
||
setLoadingSlots(true);
|
||
const from = toDateStr(monthStart(year, month));
|
||
const last = new Date(year, month + 1, 0);
|
||
const to = toDateStr(last);
|
||
adminListSlots(from, to)
|
||
.then(setSlots)
|
||
.catch(() => toast.error(t("admin.error")))
|
||
.finally(() => setLoadingSlots(false));
|
||
}, [year, month]);
|
||
|
||
useEffect(() => { startTransition(() => loadSlots()); }, [loadSlots]);
|
||
|
||
const prevMonth = () => {
|
||
if (month === 0) { setYear((y) => y - 1); setMonth(11); }
|
||
else setMonth((m) => m - 1);
|
||
setSelectedDay(null);
|
||
};
|
||
|
||
const nextMonth = () => {
|
||
if (month === 11) { setYear((y) => y + 1); setMonth(0); }
|
||
else setMonth((m) => m + 1);
|
||
setSelectedDay(null);
|
||
};
|
||
|
||
const blockSlot = async (slot: TimeSlotApi) => {
|
||
try {
|
||
const updated = await adminUpdateSlot(slot.id, !slot.is_blocked);
|
||
setSlots((prev) => prev.map((s) => s.id === slot.id ? updated : s));
|
||
toast.success(slot.is_blocked ? t("admin.planning.slot_unblocked") : t("admin.planning.slot_blocked"));
|
||
} catch {
|
||
toast.error(t("admin.error"));
|
||
}
|
||
};
|
||
|
||
const deleteSlot = async (id: string) => {
|
||
try {
|
||
await adminDeleteSlot(id);
|
||
setSlots((prev) => prev.filter((s) => s.id !== id));
|
||
toast.success(t("admin.planning.slot_deleted"));
|
||
} catch {
|
||
toast.error(t("admin.error"));
|
||
}
|
||
};
|
||
|
||
const firstDay = monthStart(year, month);
|
||
const totalDays = daysInMonth(year, month);
|
||
const startOffset = weekdayMon(firstDay);
|
||
const cells: (number | null)[] = [
|
||
...Array(startOffset).fill(null),
|
||
...Array.from({ length: totalDays }, (_, i) => i + 1),
|
||
];
|
||
while (cells.length % 7 !== 0) cells.push(null);
|
||
|
||
const slotsByDay: Record<string, TimeSlotApi[]> = {};
|
||
slots.forEach((s) => {
|
||
(slotsByDay[s.date] ||= []).push(s);
|
||
});
|
||
|
||
const selectedSlots = selectedDay ? (slotsByDay[selectedDay] ?? []) : [];
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<Button variant="outline" size="icon" onClick={prevMonth}><ChevronLeft className="h-4 w-4" /></Button>
|
||
<h3 className="font-medium">{MONTH_NAMES[month]} {year}</h3>
|
||
<Button variant="outline" size="icon" onClick={nextMonth}><ChevronRight className="h-4 w-4" /></Button>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardContent className="p-3">
|
||
<div className="grid grid-cols-7 gap-px">
|
||
{DAY_NAMES.map((d) => (
|
||
<div key={d} className="text-center text-xs font-medium text-muted-foreground py-2">
|
||
{d.slice(0, 3)}
|
||
</div>
|
||
))}
|
||
{cells.map((day, i) => {
|
||
if (!day) return <div key={i} />;
|
||
const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||
const daySlots = slotsByDay[dateStr] ?? [];
|
||
const available = daySlots.filter((s) => !s.is_blocked && !s.is_booked).length;
|
||
const booked = daySlots.filter((s) => s.is_booked).length;
|
||
const blocked = daySlots.filter((s) => s.is_blocked).length;
|
||
const isToday = dateStr === toDateStr(today);
|
||
const isSelected = dateStr === selectedDay;
|
||
|
||
return (
|
||
<button
|
||
key={i}
|
||
onClick={() => setSelectedDay(isSelected ? null : dateStr)}
|
||
className={`rounded-lg p-1.5 text-left transition-colors min-h-[60px] ${
|
||
isSelected ? "bg-primary/10 ring-1 ring-primary" :
|
||
isToday ? "bg-muted ring-1 ring-muted-foreground/30" : "hover:bg-muted/50"
|
||
}`}
|
||
>
|
||
<div className={`text-xs font-medium mb-1 ${isToday ? "text-primary" : ""}`}>{day}</div>
|
||
{daySlots.length > 0 && (
|
||
<div className="space-y-0.5">
|
||
{available > 0 && <div className="text-[10px] text-green-600">{available} {t("admin.status.free").toLowerCase()}</div>}
|
||
{booked > 0 && <div className="text-[10px] text-blue-600">{booked} {t("admin.status.booked").toLowerCase()}</div>}
|
||
{blocked > 0 && <div className="text-[10px] text-red-500">{blocked} {t("admin.status.blocked").toLowerCase()}</div>}
|
||
</div>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{selectedDay && (
|
||
<Card>
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-sm">
|
||
{t("admin.planning.slots_for")} {new Date(selectedDay + "T00:00:00").toLocaleDateString(locale, {
|
||
weekday: "long", day: "numeric", month: "long"
|
||
})}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loadingSlots ? (
|
||
<div className="text-sm text-muted-foreground">{t("admin.loading")}</div>
|
||
) : selectedSlots.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">{t("admin.planning.no_slots")}</p>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{selectedSlots
|
||
.sort((a, b) => a.start_time.localeCompare(b.start_time))
|
||
.map((slot) => (
|
||
<div key={slot.id} className="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm font-medium">{slot.start_time.slice(0, 5)} – {slot.end_time.slice(0, 5)}</span>
|
||
{slot.is_booked && <Badge variant="secondary" className="text-xs">{t("admin.status.booked")}</Badge>}
|
||
{slot.is_blocked && <Badge variant="destructive" className="text-xs">{t("admin.status.blocked")}</Badge>}
|
||
{!slot.is_booked && !slot.is_blocked && <Badge variant="outline" className="text-xs text-green-600 border-green-300">{t("admin.status.free")}</Badge>}
|
||
</div>
|
||
{!slot.is_booked && (
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant="ghost" size="icon"
|
||
className="h-7 w-7"
|
||
title={slot.is_blocked ? t("admin.planning.unblock_title") : t("admin.planning.block_btn")}
|
||
onClick={() => blockSlot(slot)}
|
||
>
|
||
{slot.is_blocked
|
||
? <Check className="h-3.5 w-3.5 text-green-600" />
|
||
: <Ban className="h-3.5 w-3.5 text-amber-500" />
|
||
}
|
||
</Button>
|
||
<Button
|
||
variant="ghost" size="icon"
|
||
className="h-7 w-7"
|
||
title={t("admin.delete")}
|
||
onClick={() => deleteSlot(slot.id)}
|
||
>
|
||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Blocked Dates Tab ─────────────────────────────────────────────────────────
|
||
|
||
function BlockedDatesTab() {
|
||
const { t, locale } = useLanguage();
|
||
const [dates, setDates] = useState<BlockedDate[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [newDate, setNewDate] = useState(toDateStr(new Date()));
|
||
const [newReason, setNewReason] = useState("");
|
||
const [adding, setAdding] = useState(false);
|
||
|
||
useEffect(() => {
|
||
adminGetBlockedDates()
|
||
.then(setDates)
|
||
.catch(() => toast.error(t("admin.error")))
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
const add = async () => {
|
||
setAdding(true);
|
||
try {
|
||
const entry = await adminAddBlockedDate(newDate, newReason || undefined);
|
||
setDates((prev) => [...prev, entry].sort((a, b) => a.date.localeCompare(b.date)));
|
||
setNewReason("");
|
||
toast.success(t("admin.planning.date_blocked"));
|
||
} catch {
|
||
toast.error(t("admin.error"));
|
||
} finally {
|
||
setAdding(false);
|
||
}
|
||
};
|
||
|
||
const remove = async (id: string) => {
|
||
try {
|
||
await adminRemoveBlockedDate(id);
|
||
setDates((prev) => prev.filter((d) => d.id !== id));
|
||
toast.success(t("admin.planning.date_unblocked"));
|
||
} catch {
|
||
toast.error(t("admin.error"));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{t("admin.planning.block_title")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-xs text-muted-foreground mb-3">{t("admin.planning.block_desc")}</p>
|
||
<div className="flex flex-wrap gap-3 items-end">
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.date_lbl")}</Label>
|
||
<Input type="date" className="mt-1 w-44" value={newDate}
|
||
onChange={(e) => setNewDate(e.target.value)} />
|
||
</div>
|
||
<div>
|
||
<Label className="text-xs">{t("admin.planning.reason")}</Label>
|
||
<Input className="mt-1 w-56" placeholder={t("admin.planning.reason_ph")} value={newReason}
|
||
onChange={(e) => setNewReason(e.target.value)} />
|
||
</div>
|
||
<Button onClick={add} disabled={adding} size="sm">
|
||
<Plus className="h-4 w-4 mr-1.5" />
|
||
{adding ? t("admin.planning.blocking") : t("admin.planning.block_btn")}
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{t("admin.planning.blocked_list")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{loading ? (
|
||
<div className="space-y-2">
|
||
{Array.from({ length: 3 }).map((_, i) => (
|
||
<div key={i} className="h-10 bg-muted animate-pulse rounded" />
|
||
))}
|
||
</div>
|
||
) : dates.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">{t("admin.planning.no_blocked")}</p>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{dates.map((d) => (
|
||
<div key={d.id} className="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||
<div>
|
||
<span className="text-sm font-medium">
|
||
{new Date(d.date + "T00:00:00").toLocaleDateString(locale, {
|
||
weekday: "long", day: "numeric", month: "long", year: "numeric",
|
||
})}
|
||
</span>
|
||
{d.reason && <span className="text-xs text-muted-foreground ml-2">— {d.reason}</span>}
|
||
</div>
|
||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => remove(d.id)}>
|
||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||
|
||
type Tab = "schedule" | "calendar" | "blocked";
|
||
|
||
export default function PlanningPage() {
|
||
const { t } = useLanguage();
|
||
const [tab, setTab] = useState<Tab>("schedule");
|
||
|
||
const tabs: { id: Tab; label: string }[] = [
|
||
{ id: "schedule", label: t("admin.planning.tab_schedule") },
|
||
{ id: "calendar", label: t("admin.planning.tab_calendar") },
|
||
{ id: "blocked", label: t("admin.planning.tab_blocked") },
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h2 className="font-serif text-2xl font-semibold">{t("admin.planning.title")}</h2>
|
||
<p className="text-sm text-muted-foreground mt-1">{t("admin.planning.subtitle")}</p>
|
||
</div>
|
||
|
||
<div className="flex gap-1 bg-muted p-1 rounded-lg w-fit">
|
||
{tabs.map((item) => (
|
||
<button
|
||
key={item.id}
|
||
onClick={() => setTab(item.id)}
|
||
className={`px-4 py-1.5 text-sm rounded-md transition-colors ${
|
||
tab === item.id
|
||
? "bg-background shadow-sm font-medium"
|
||
: "text-muted-foreground hover:text-foreground"
|
||
}`}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{tab === "schedule" && <ScheduleTab />}
|
||
{tab === "calendar" && <CalendarTab />}
|
||
{tab === "blocked" && <BlockedDatesTab />}
|
||
</div>
|
||
);
|
||
}
|