Files
badoHair_fe/app/admin/services/page.tsx
2026-05-21 22:24:22 +00:00

278 lines
10 KiB
TypeScript

"use client";
import { useState, useEffect, startTransition } from "react";
import { Card } 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 {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Plus, Pencil, Trash2, Clock } from "lucide-react";
import { toast } from "sonner";
import {
adminListServices, adminCreateService, adminUpdateService, adminDeleteService,
AdminServiceApi, ServicePayload, formatDuration,
} from "@/lib/api/services";
import { ApiError } from "@/lib/api";
import { useLanguage } from "@/contexts/LanguageContext";
const EMPTY: ServicePayload = {
name: "",
description: "",
duration_minutes: 60,
price: 0,
is_active: true,
};
export default function AdminServices() {
const { t } = useLanguage();
const [services, setServices] = useState<AdminServiceApi[]>([]);
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<AdminServiceApi | null>(null);
const [form, setForm] = useState<ServicePayload>(EMPTY);
const [saving, setSaving] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(null);
const load = async () => {
setLoading(true);
try {
setServices(await adminListServices());
} catch (err) {
toast.error(err instanceof ApiError ? err.message : t("admin.error"));
} finally {
setLoading(false);
}
};
useEffect(() => { startTransition(() => load()); }, []);
const openCreate = () => {
setEditing(null);
setForm(EMPTY);
setDialogOpen(true);
};
const openEdit = (s: AdminServiceApi) => {
setEditing(s);
setForm({
name: s.name,
description: s.description ?? "",
duration_minutes: s.duration_minutes,
price: s.price,
is_active: s.is_active,
});
setDialogOpen(true);
};
const handleSave = async () => {
if (!form.name.trim()) { toast.error(t("admin.services.name_req")); return; }
setSaving(true);
try {
if (editing) {
const updated = await adminUpdateService(editing.id, form);
setServices((prev) => prev.map((s) => (s.id === editing.id ? updated : s)));
toast.success(t("admin.services.updated"));
} else {
const created = await adminCreateService(form);
setServices((prev) => [...prev, created]);
toast.success(t("admin.services.created"));
}
setDialogOpen(false);
} catch (err) {
toast.error(err instanceof ApiError ? err.message : t("admin.error"));
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!deleteId) return;
try {
await adminDeleteService(deleteId);
setServices((prev) => prev.filter((s) => s.id !== deleteId));
toast.success(t("admin.services.deleted"));
} catch (err) {
toast.error(err instanceof ApiError ? err.message : t("admin.error"));
} finally {
setDeleteId(null);
}
};
const set = (k: keyof ServicePayload, v: ServicePayload[keyof ServicePayload]) =>
setForm((prev) => ({ ...prev, [k]: v }));
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="font-serif text-2xl font-semibold">{t("admin.services.title")}</h2>
<p className="text-sm text-muted-foreground mt-1">{t("admin.services.subtitle")}</p>
</div>
<Button onClick={openCreate}>
<Plus className="h-4 w-4 mr-2" /> {t("admin.services.new_btn")}
</Button>
</div>
<Card>
{loading ? (
<div className="p-8 space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-12 bg-muted animate-pulse rounded" />
))}
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("admin.services.col_name")}</TableHead>
<TableHead>{t("admin.services.col_desc")}</TableHead>
<TableHead>{t("admin.services.col_duration")}</TableHead>
<TableHead>{t("admin.services.col_price")}</TableHead>
<TableHead>{t("admin.status")}</TableHead>
<TableHead className="text-right">{t("admin.actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{services.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-10">
{t("admin.services.none")}
</TableCell>
</TableRow>
) : (
services.map((s) => (
<TableRow key={s.id}>
<TableCell className="font-medium">{s.name}</TableCell>
<TableCell className="text-sm text-muted-foreground max-w-[250px] truncate">
{s.description ?? "—"}
</TableCell>
<TableCell>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Clock className="h-3.5 w-3.5" />
{formatDuration(s.duration_minutes)}
</div>
</TableCell>
<TableCell className="font-semibold">
{s.price === 0 ? t("admin.services.free") : `${s.price}`}
</TableCell>
<TableCell>
<Badge variant={s.is_active ? "default" : "secondary"}>
{s.is_active ? t("admin.status.active") : t("admin.status.inactive")}
</Badge>
</TableCell>
<TableCell className="text-right space-x-1">
<Button variant="ghost" size="icon" onClick={() => openEdit(s)}>
<Pencil className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => setDeleteId(s.id)}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{editing ? t("admin.services.edit_title") : t("admin.services.create_title")}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div>
<Label htmlFor="s-name">{t("admin.services.col_name")} *</Label>
<Input
id="s-name"
value={form.name}
onChange={(e) => set("name", e.target.value)}
className="mt-1"
placeholder={t("admin.services.name_ph")}
/>
</div>
<div>
<Label htmlFor="s-desc">{t("admin.services.col_desc")}</Label>
<Input
id="s-desc"
value={form.description ?? ""}
onChange={(e) => set("description", e.target.value)}
className="mt-1"
placeholder={t("admin.services.desc_ph")}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="s-dur">{t("admin.services.duration")}</Label>
<Input
id="s-dur"
type="number"
min={5}
max={480}
value={form.duration_minutes}
onChange={(e) => set("duration_minutes", parseInt(e.target.value) || 60)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="s-price">{t("admin.services.price")}</Label>
<Input
id="s-price"
type="number"
min={0}
step={0.01}
value={form.price}
onChange={(e) => set("price", parseFloat(e.target.value) || 0)}
className="mt-1"
/>
</div>
</div>
<div className="flex items-center gap-2">
<input
id="s-active"
type="checkbox"
checked={form.is_active}
onChange={(e) => set("is_active", e.target.checked)}
className="h-4 w-4"
/>
<Label htmlFor="s-active" className="font-normal cursor-pointer">
{t("admin.services.active_label")}
</Label>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}>{t("admin.cancel")}</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? t("admin.saving") : t("admin.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={!!deleteId} onOpenChange={(o) => !o && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("admin.services.delete_title")}</AlertDialogTitle>
<AlertDialogDescription>{t("admin.irreversible")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("admin.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>{t("admin.delete")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}