Quran_Tech_Server / F_Pro /src /components /sessions /SheikhAvailabilityForm.tsx
aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
13.8 kB
"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>
);
}