"use client"; import { BanIcon, CheckCircle2Icon, MoreHorizontalIcon, PlusIcon, SearchIcon, Trash2Icon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { type CustomerFormSavePayload } from "@/components/customer-form"; import { DashboardPage, DashboardTableShell, DashboardTableStateRow, DashboardToolbar, } from "@/components/dashboard-page"; import { ListPagination } from "@/components/list-pagination"; import { OptionCombobox, type ComboboxOption, } from "@/components/option-combobox"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ButtonGroup } from "@/components/ui/button-group"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { type PageResult } from "@/lib/api/admin"; import { fetchCompanies, type AdminCompany } from "@/lib/api/company"; import { deleteCustomer, fetchCustomers, saveCustomerProfile, updateCustomerStatus, type AdminCustomer, } from "@/lib/api/customer"; import { Gender, Status } from "@/lib/generated/enums"; import { useI18n } from "@/i18n/provider"; import { EditDialog } from "./_components/edit"; function getLabel( value: string, options: ReadonlyArray<{ value: string; label: string }>, fallback: string, ) { return options.find((item) => item.value === value)?.label ?? fallback; } export default function DashboardCustomersPage() { const t = useI18n(); const [keywordInput, setKeywordInput] = useState(""); const [statusFilterInput, setStatusFilterInput] = useState("all"); const [genderFilterInput, setGenderFilterInput] = useState("all"); const [companyFilterInput, setCompanyFilterInput] = useState("0"); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [genderFilter, setGenderFilter] = useState("all"); const [companyFilter, setCompanyFilter] = useState("0"); const [companyOptions, setCompanyOptions] = useState([ { value: "0", label: t("customer.allCompanies") }, ]); const [companyNameMap, setCompanyNameMap] = useState>( {}, ); const [page, setPage] = useState(1); const [limit, setLimit] = useState(20); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [actionLoadingId, setActionLoadingId] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }); const listStatusOptions = useMemo( () => [ { value: "all", label: t("status.all") }, { value: String(Status.Ok), label: t("status.ok") }, { value: String(Status.Disabled), label: t("status.disabled") }, ], [t], ); const genderOptions = useMemo( () => [ { value: "all", label: t("customer.allGenders") }, { value: String(Gender.Unknown), label: t("customerForm.genderUnknown") }, { value: String(Gender.Male), label: t("customerForm.genderMale") }, { value: String(Gender.Female), label: t("customerForm.genderFemale") }, ], [t], ); useEffect(() => { async function loadCompanies() { try { const data = await fetchCompanies({ status: 0, page: 1, limit: 500 }); const opts: ComboboxOption[] = [ { value: "0", label: t("customer.allCompanies") }, ...data.results.map((item) => ({ value: String(item.id), label: item.name, })), ]; setCompanyOptions(opts); const map: Record = {}; data.results.forEach((item: AdminCompany) => { map[item.id] = item.name; }); setCompanyNameMap(map); } catch { // ignore } } void loadCompanies(); }, [t]); const loadData = useCallback(async () => { setLoading(true); try { const data = await fetchCustomers({ keyword: keyword.trim() || undefined, status: statusFilter === "all" ? undefined : Number(statusFilter), gender: genderFilter === "all" ? undefined : Number(genderFilter), companyId: companyFilter === "0" ? undefined : Number(companyFilter), page, limit, }); setResult(data); } catch (error) { toast.error(error instanceof Error ? error.message : t("customer.loadFailed")); } finally { setLoading(false); } }, [companyFilter, genderFilter, keyword, limit, page, statusFilter, t]); useEffect(() => { void loadData(); }, [loadData]); const companyFilterLabel = useMemo(() => { return ( companyOptions.find((item) => item.value === companyFilterInput)?.label ?? t("customer.allCompanies") ); }, [companyFilterInput, companyOptions, t]); function applyFilters() { setKeyword(keywordInput); setStatusFilter(statusFilterInput); setGenderFilter(genderFilterInput); setCompanyFilter(companyFilterInput); setPage(1); } function handleFilterKeyDown(event: React.KeyboardEvent) { if (event.key !== "Enter") return; event.preventDefault(); applyFilters(); } function handlePageChange(nextPage: number) { if (nextPage < 1 || nextPage === page) return; setPage(nextPage); } function openCreateDialog() { setEditingItem(null); setDialogOpen(true); } function openEditDialog(item: AdminCustomer) { setEditingItem(item); setDialogOpen(true); } function handleDialogOpenChange(open: boolean) { if (saving) return; if (!open) setEditingItem(null); setDialogOpen(open); } async function handleSave(payload: CustomerFormSavePayload) { if (saving) return; setSaving(true); try { await saveCustomerProfile(payload); toast.success( editingItem ? t("customer.updated", { name: editingItem.name }) : t("customer.created", { name: payload.name }), ); setDialogOpen(false); setEditingItem(null); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : t("customer.saveFailed")); } finally { setSaving(false); } } async function handleToggleStatus(item: AdminCustomer) { setActionLoadingId(item.id); try { const nextStatus = item.status === 0 ? 1 : 0; await updateCustomerStatus(item.id, nextStatus); toast.success(t(nextStatus === 0 ? "customer.enabled" : "customer.disabled", { name: item.name })); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : t("customer.statusUpdateFailed")); } finally { setActionLoadingId(null); } } async function handleDelete(item: AdminCustomer) { setActionLoadingId(item.id); try { await deleteCustomer(item.id); toast.success(t("customer.deleted", { name: item.name })); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : t("customer.deleteFailed")); } finally { setActionLoadingId(null); } } function getGenderText(gender: number) { if (gender === Gender.Male) return t("customerForm.genderMale"); if (gender === Gender.Female) return t("customerForm.genderFemale"); return t("customerForm.genderUnknown"); } return ( <> {t("customer.new")} } >
setKeywordInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder={t("customer.keywordPlaceholder")} className="pl-9" />
setGenderFilterInput(v)} />
setCompanyFilterInput(v)} />
setStatusFilterInput(v)} />
{ setLimit(nextLimit); setPage(1); }} /> } > ID {t("customer.columnName")} {t("customer.columnGender")} {t("customer.columnCompany")} {t("customer.columnMobile")} {t("customer.columnEmail")} {t("customer.columnStatus")} {t("customer.columnActions")} {loading || result.results.length === 0 ? ( ) : ( result.results.map((item) => { const actionLoading = actionLoadingId === item.id; return ( {item.id} {item.name} {getGenderText(item.gender)} {item.companyId > 0 ? (companyNameMap[item.companyId] ?? String(item.companyId)) : "-"} {item.primaryMobile || "-"} {item.primaryEmail || "-"} {item.status === 0 ? t("status.ok") : t("status.disabled")} } aria-label={t("customer.moreActions", { name: item.name })} > void handleToggleStatus(item)} > {actionLoadingId === item.id ? ( t("customer.processing") ) : item.status === 0 ? ( <> {t("customer.disable")} ) : ( <> {t("customer.enable")} )} void handleDelete(item)} > {t("customer.delete")} ); }) )}
); }