Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; | |
| import { Label } from "../ui/label"; | |
| import { Button } from "../ui/button"; | |
| import { Input } from "../ui/input"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "../ui/select"; | |
| import { useLanguage } from "../../lib/languageContext"; | |
| import { translations } from "../../lib/translations"; | |
| import { | |
| fetchSheikhAvailability, | |
| type AvailabilitySlotDto, | |
| } from "../../lib/api/sheikhApi"; | |
| import sheikhApi from "../../lib/api/sheikhApi"; | |
| const updateSheikhAvailability = sheikhApi.updateSheikhAvailability; | |
| const DAY_ORDER: Record<string, number> = { | |
| SATURDAY: 0, SUNDAY: 1, MONDAY: 2, TUESDAY: 3, | |
| WEDNESDAY: 4, THURSDAY: 5, FRIDAY: 6, | |
| }; | |
| function sortSlots<T extends { dayOfWeek: string }>(arr: T[]): T[] { | |
| return [...arr].sort( | |
| (a, b) => (DAY_ORDER[a.dayOfWeek.toUpperCase()] ?? 99) - (DAY_ORDER[b.dayOfWeek.toUpperCase()] ?? 99) | |
| ); | |
| } | |
| const ALL_DAYS = [ | |
| "SUNDAY", | |
| "MONDAY", | |
| "TUESDAY", | |
| "WEDNESDAY", | |
| "THURSDAY", | |
| "FRIDAY", | |
| ] as const; | |
| type DayKey = (typeof ALL_DAYS)[number]; | |
| // "HH:mm:ss" or "HH:mm" → "HH:mm" | |
| function fmt(time: string): string { | |
| return time.slice(0, 5); | |
| } | |
| function capitalise(day: string): string { | |
| return day.charAt(0).toUpperCase() + day.slice(1).toLowerCase(); | |
| } | |
| interface RowState extends AvailabilitySlotDto { | |
| editing: boolean; | |
| editStart: string; | |
| editEnd: string; | |
| } | |
| interface NewRow { | |
| day: DayKey | ""; | |
| startTime: string; | |
| endTime: string; | |
| } | |
| export default function SheikhAvailabilityForm() { | |
| const { language, isRTL } = useLanguage(); | |
| const t = translations[language].availability; | |
| const [rows, setRows] = useState<RowState[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [saving, setSaving] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [successMsg, setSuccessMsg] = useState<string | null>(null); | |
| // new-row form | |
| const [adding, setAdding] = useState(false); | |
| const [newRow, setNewRow] = useState<NewRow>({ day: "", startTime: "", endTime: "" }); | |
| useEffect(() => { | |
| fetchSheikhAvailability() | |
| .then((data) => | |
| setRows( | |
| sortSlots(data).map((s) => ({ | |
| ...s, | |
| editing: false, | |
| editStart: fmt(s.startTime), | |
| editEnd: fmt(s.endTime), | |
| })) | |
| ) | |
| ) | |
| .catch((e) => setError(e.message ?? "Failed to load availability")) | |
| .finally(() => setLoading(false)); | |
| }, []); | |
| // days already taken → exclude from "add" dropdown | |
| const takenDays = rows.map((r) => r.dayOfWeek.toUpperCase() as DayKey); | |
| const availableDays = ALL_DAYS.filter((d) => !takenDays.includes(d)); | |
| // ── helpers ────────────────────────────────────────────────────────────── | |
| function flash(msg: string) { | |
| setSuccessMsg(msg); | |
| setTimeout(() => setSuccessMsg(null), 3000); | |
| } | |
| async function putAll(updated: AvailabilitySlotDto[]) { | |
| setSaving(true); | |
| setError(null); | |
| const payload = updated.map((s) => ({ | |
| dayOfWeek: s.dayOfWeek, | |
| startTime: s.startTime, | |
| endTime: s.endTime, | |
| })); | |
| console.log("[availability] PUT payload:", JSON.stringify({ slots: payload }, null, 2)); | |
| try { | |
| await updateSheikhAvailability(payload); | |
| flash("Availability saved."); | |
| } catch (e: any) { | |
| console.error("[availability] PUT failed:", e); | |
| setError(e.message ?? "Failed to save"); | |
| } finally { | |
| setSaving(false); | |
| } | |
| } | |
| // ── edit ───────────────────────────────────────────────────────────────── | |
| function startEdit(id: number) { | |
| setRows((prev) => | |
| prev.map((r) => (r.id === id ? { ...r, editing: true } : r)) | |
| ); | |
| } | |
| function cancelEdit(id: number) { | |
| setRows((prev) => | |
| prev.map((r) => | |
| r.id === id | |
| ? { ...r, editing: false, editStart: fmt(r.startTime), editEnd: fmt(r.endTime) } | |
| : r | |
| ) | |
| ); | |
| } | |
| async function saveEdit(id: number) { | |
| const row = rows.find((r) => r.id === id)!; | |
| const updated = rows.map((r) => | |
| r.id === id | |
| ? { ...r, startTime: row.editStart + ":00", endTime: row.editEnd + ":00", editing: false } | |
| : r | |
| ); | |
| setRows(updated); | |
| await putAll(updated); | |
| } | |
| // ── delete ──────────────────────────────────────────────────────────────── | |
| async function deleteRow(id: number) { | |
| const updated = rows.filter((r) => r.id !== id); | |
| setRows(updated); | |
| await putAll(updated); | |
| } | |
| // ── add ─────────────────────────────────────────────────────────────────── | |
| async function confirmAdd() { | |
| if (!newRow.day || !newRow.startTime || !newRow.endTime) return; | |
| const fakeId = Date.now(); // temporary local id | |
| const newSlot: AvailabilitySlotDto = { | |
| id: fakeId, | |
| dayOfWeek: newRow.day, | |
| startTime: newRow.startTime + ":00", | |
| endTime: newRow.endTime + ":00", | |
| }; | |
| const updated = [ | |
| ...rows.map((r) => ({ | |
| id: r.id, | |
| dayOfWeek: r.dayOfWeek, | |
| startTime: r.startTime, | |
| endTime: r.endTime, | |
| })), | |
| newSlot, | |
| ]; | |
| setSaving(true); | |
| setError(null); | |
| try { | |
| await updateSheikhAvailability( | |
| updated.map((s) => ({ | |
| dayOfWeek: s.dayOfWeek, | |
| startTime: s.startTime, | |
| endTime: s.endTime, | |
| })) | |
| ); | |
| // re-fetch to get real ids from backend | |
| const fresh = await fetchSheikhAvailability(); | |
| setRows( | |
| sortSlots(fresh).map((s) => ({ | |
| ...s, | |
| editing: false, | |
| editStart: fmt(s.startTime), | |
| editEnd: fmt(s.endTime), | |
| })) | |
| ); | |
| flash("Availability saved."); | |
| } catch (e: any) { | |
| setError(e.message ?? "Failed to save"); | |
| } finally { | |
| setSaving(false); | |
| setAdding(false); | |
| setNewRow({ day: "", startTime: "", endTime: "" }); | |
| } | |
| } | |
| // ───────────────────────────────────────────────────────────────────────── | |
| return ( | |
| <div dir={isRTL ? "rtl" : "ltr"}> | |
| <Card className="max-w-4xl mx-auto mt-6 shadow-xl rounded-2xl"> | |
| <CardHeader> | |
| <CardTitle className="text-2xl text-center">{t.title}</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| {/* feedback */} | |
| {error && <p className="text-sm text-destructive text-center">{error}</p>} | |
| {successMsg && <p className="text-sm text-emerald-600 text-center">{successMsg}</p>} | |
| {loading && ( | |
| <p className="text-center text-muted-foreground py-8">Loading…</p> | |
| )} | |
| {!loading && rows.length === 0 && !adding && ( | |
| <p className="text-center text-muted-foreground py-6">No availability set yet.</p> | |
| )} | |
| {/* existing rows */} | |
| {rows.map((row) => ( | |
| <div | |
| key={row.id} | |
| className="border rounded-xl p-4 bg-muted/40 flex flex-wrap items-end gap-4" | |
| > | |
| {/* day label */} | |
| <div className="w-32"> | |
| <Label className="text-xs text-muted-foreground">Day</Label> | |
| <p className="text-sm font-semibold mt-1">{capitalise(row.dayOfWeek)}</p> | |
| </div> | |
| {/* from */} | |
| <div className="space-y-1"> | |
| <Label className="text-xs text-muted-foreground">{t.from}</Label> | |
| {row.editing ? ( | |
| <Input | |
| type="time" | |
| className="w-32" | |
| value={row.editStart} | |
| onChange={(e) => | |
| setRows((prev) => | |
| prev.map((r) => | |
| r.id === row.id ? { ...r, editStart: e.target.value } : r | |
| ) | |
| ) | |
| } | |
| /> | |
| ) : ( | |
| <p className="text-sm p-2 bg-background border rounded-md w-32 text-center"> | |
| {fmt(row.startTime)} | |
| </p> | |
| )} | |
| </div> | |
| {/* to */} | |
| <div className="space-y-1"> | |
| <Label className="text-xs text-muted-foreground">{t.to}</Label> | |
| {row.editing ? ( | |
| <Input | |
| type="time" | |
| className="w-32" | |
| value={row.editEnd} | |
| onChange={(e) => | |
| setRows((prev) => | |
| prev.map((r) => | |
| r.id === row.id ? { ...r, editEnd: e.target.value } : r | |
| ) | |
| ) | |
| } | |
| /> | |
| ) : ( | |
| <p className="text-sm p-2 bg-background border rounded-md w-32 text-center"> | |
| {fmt(row.endTime)} | |
| </p> | |
| )} | |
| </div> | |
| {/* actions */} | |
| <div className="flex gap-2 ml-auto"> | |
| {row.editing ? ( | |
| <> | |
| <Button | |
| size="sm" | |
| disabled={saving} | |
| onClick={() => saveEdit(row.id)} | |
| > | |
| {saving ? "Saving…" : "Save"} | |
| </Button> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={() => cancelEdit(row.id)} | |
| > | |
| Cancel | |
| </Button> | |
| </> | |
| ) : ( | |
| <> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={() => startEdit(row.id)} | |
| > | |
| Edit | |
| </Button> | |
| <Button | |
| size="sm" | |
| variant="destructive" | |
| disabled={saving} | |
| onClick={() => deleteRow(row.id)} | |
| > | |
| Delete | |
| </Button> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| {/* new row form */} | |
| {adding && ( | |
| <div className="border-2 border-dashed border-emerald-400 rounded-xl p-4 flex flex-wrap items-end gap-4"> | |
| {/* day select */} | |
| <div className="space-y-1"> | |
| <Label className="text-xs text-muted-foreground">Day</Label> | |
| <Select | |
| value={newRow.day} | |
| onValueChange={(v) => setNewRow((p) => ({ ...p, day: v as DayKey }))} | |
| > | |
| <SelectTrigger className="w-36"> | |
| <SelectValue placeholder="Select day" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {availableDays.map((d) => ( | |
| <SelectItem key={d} value={d}> | |
| {capitalise(d)} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| {/* from */} | |
| <div className="space-y-1"> | |
| <Label className="text-xs text-muted-foreground">{t.from}</Label> | |
| <Input | |
| type="time" | |
| className="w-32" | |
| value={newRow.startTime} | |
| onChange={(e) => setNewRow((p) => ({ ...p, startTime: e.target.value }))} | |
| /> | |
| </div> | |
| {/* to */} | |
| <div className="space-y-1"> | |
| <Label className="text-xs text-muted-foreground">{t.to}</Label> | |
| <Input | |
| type="time" | |
| className="w-32" | |
| value={newRow.endTime} | |
| onChange={(e) => setNewRow((p) => ({ ...p, endTime: e.target.value }))} | |
| /> | |
| </div> | |
| <div className="flex gap-2 ml-auto"> | |
| <Button | |
| size="sm" | |
| disabled={saving || !newRow.day || !newRow.startTime || !newRow.endTime} | |
| onClick={confirmAdd} | |
| > | |
| {saving ? "Saving…" : "Confirm"} | |
| </Button> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={() => { | |
| setAdding(false); | |
| setNewRow({ day: "", startTime: "", endTime: "" }); | |
| }} | |
| > | |
| Cancel | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| {/* add day button */} | |
| {!adding && availableDays.length > 0 && ( | |
| <Button | |
| type="button" | |
| variant="outline" | |
| className="w-full border-dashed" | |
| onClick={() => setAdding(true)} | |
| > | |
| + Add Day | |
| </Button> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| } | |