Update May 12 by Elvis
This commit is contained in:
574
app/admin/planning/page.tsx
Normal file
574
app/admin/planning/page.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } 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(() => { 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user