mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-13 08:58:31 +00:00
154 lines
6.2 KiB
TypeScript
154 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, startTransition } from "react";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Search, Ban, CheckCircle, RefreshCw } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { adminListCustomers, adminBlockCustomer, CustomerApi } from "@/lib/api/customers";
|
|
import { useLanguage } from "@/contexts/LanguageContext";
|
|
import { ApiError } from "@/lib/api";
|
|
|
|
export default function AdminClients() {
|
|
const { t, locale } = useLanguage();
|
|
const [customers, setCustomers] = useState<CustomerApi[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [search, setSearch] = useState("");
|
|
const [query, setQuery] = useState("");
|
|
const [updating, setUpdating] = useState<string | null>(null);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await adminListCustomers(query || undefined);
|
|
setCustomers(res.data);
|
|
} catch (err) {
|
|
toast.error(err instanceof ApiError ? err.message : t("admin.error"));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [query]);
|
|
|
|
useEffect(() => { startTransition(() => load()); }, [load]);
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setQuery(search);
|
|
};
|
|
|
|
const toggleBlock = async (c: CustomerApi) => {
|
|
setUpdating(c.id);
|
|
try {
|
|
const updated = await adminBlockCustomer(c.id, !c.is_blocked);
|
|
setCustomers((prev) => prev.map((x) => (x.id === c.id ? { ...x, is_blocked: updated.is_blocked } : x)));
|
|
toast.success(updated.is_blocked ? t("admin.customers.blocked") : t("admin.customers.unblocked"));
|
|
} catch (err) {
|
|
toast.error(err instanceof ApiError ? err.message : t("admin.error"));
|
|
} finally {
|
|
setUpdating(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="font-serif text-2xl font-semibold">{t("admin.customers.title")}</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">{customers.length} {t("admin.customers.subtitle")}</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={load} disabled={loading}>
|
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
|
{t("admin.refresh")}
|
|
</Button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSearch} className="flex gap-2 max-w-sm">
|
|
<Input
|
|
placeholder={t("admin.customers.search_ph")}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
<Button type="submit" variant="outline" size="icon">
|
|
<Search className="h-4 w-4" />
|
|
</Button>
|
|
</form>
|
|
|
|
<Card>
|
|
{loading ? (
|
|
<div className="p-8 space-y-3">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="h-12 bg-muted animate-pulse rounded" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{t("admin.customers.col_name")}</TableHead>
|
|
<TableHead>{t("admin.customers.col_email")}</TableHead>
|
|
<TableHead>{t("admin.customers.col_phone")}</TableHead>
|
|
<TableHead className="text-center">{t("admin.customers.col_orders")}</TableHead>
|
|
<TableHead className="text-center">{t("admin.customers.col_bookings")}</TableHead>
|
|
<TableHead className="text-right">{t("admin.customers.col_spent")}</TableHead>
|
|
<TableHead>{t("admin.customers.col_joined")}</TableHead>
|
|
<TableHead>{t("admin.status")}</TableHead>
|
|
<TableHead />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{customers.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={9} className="text-center text-muted-foreground py-8">
|
|
{t("admin.customers.none")}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
customers.map((c) => (
|
|
<TableRow key={c.id}>
|
|
<TableCell className="font-medium">{c.full_name || "—"}</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">{c.email}</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">{c.phone ?? "—"}</TableCell>
|
|
<TableCell className="text-center text-sm">{c.orders_count}</TableCell>
|
|
<TableCell className="text-center text-sm">{c.bookings_count}</TableCell>
|
|
<TableCell className="text-right font-semibold">{Number(c.total_spent).toFixed(2)} €</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
{new Date(c.created_at).toLocaleDateString(locale, { day: "2-digit", month: "short", year: "numeric" })}
|
|
</TableCell>
|
|
<TableCell>
|
|
{c.is_blocked ? (
|
|
<Badge variant="destructive">{t("admin.status.blocked")}</Badge>
|
|
) : (
|
|
<Badge variant="secondary">{t("admin.status.active")}</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
disabled={updating === c.id}
|
|
onClick={() => toggleBlock(c)}
|
|
title={c.is_blocked ? t("admin.status.active") : t("admin.status.blocked")}
|
|
>
|
|
{c.is_blocked ? (
|
|
<CheckCircle className="h-4 w-4 text-primary" />
|
|
) : (
|
|
<Ban className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|