Files
badoHair_fe/app/admin/clients/page.tsx

154 lines
6.2 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } 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(() => { 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>
);
}