mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-13 11:13:57 +00:00
Update May 12 by Elvis
This commit is contained in:
153
app/admin/clients/page.tsx
Normal file
153
app/admin/clients/page.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user