Files
ai-agent/web/app/dashboard/customers/page.tsx
T
mlogclub 6a3c641ff9 refactor: migrate customer and skill definition pages to use DashboardCrudPage component
- Replaced existing customer and skill definition page implementations with DashboardCrudPage for improved structure and functionality.
- Simplified state management and data fetching logic.
- Enhanced filtering and column definitions for better user experience.
- Updated translations to include new labels and processing messages.
2026-05-28 09:52:47 +08:00

267 lines
8.3 KiB
TypeScript

"use client";
import { BanIcon, CheckCircle2Icon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { type CustomerFormSavePayload } from "@/components/customer-form";
import {
DashboardCrudPage,
createDashboardStatusColumn,
createDashboardStatusToggleAction,
type DashboardCrudColumn,
type DashboardCrudFilter,
} from "@/components/dashboard/crud";
import { type ComboboxOption } from "@/components/option-combobox";
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";
type TFunction = (key: string, values?: Record<string, string | number>) => string;
function getGenderText(gender: number, t: TFunction) {
if (gender === Gender.Male) return t("customerForm.genderMale");
if (gender === Gender.Female) return t("customerForm.genderFemale");
return t("customerForm.genderUnknown");
}
export default function DashboardCustomersPage() {
const t = useI18n();
const [companyOptions, setCompanyOptions] = useState<ComboboxOption[]>([
{ value: "0", label: t("customer.allCompanies") },
]);
const [companyNameMap, setCompanyNameMap] = useState<Record<number, string>>(
{},
);
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 });
setCompanyOptions([
{ value: "0", label: t("customer.allCompanies") },
...data.results.map((item) => ({
value: String(item.id),
label: item.name,
})),
]);
const map: Record<number, string> = {};
data.results.forEach((item: AdminCompany) => {
map[item.id] = item.name;
});
setCompanyNameMap(map);
} catch {
// Company names are optional display enrichment for this list.
}
}
void loadCompanies();
}, [t]);
const filters = useMemo<DashboardCrudFilter[]>(
() => [
{
name: "keyword",
label: t("customer.columnName"),
placeholder: t("customer.keywordPlaceholder"),
defaultValue: "",
trim: true,
className: "w-full sm:w-72",
},
{
name: "gender",
label: t("customer.columnGender"),
type: "select",
defaultValue: "all",
allValue: "all",
valueType: "number",
options: genderOptions,
className: "w-full sm:w-36",
},
{
name: "companyId",
label: t("customer.columnCompany"),
type: "select",
defaultValue: "0",
allValue: "0",
valueType: "number",
options: companyOptions,
className: "w-full sm:w-56",
},
{
name: "status",
label: t("customer.columnStatus"),
type: "select",
defaultValue: "all",
allValue: "all",
valueType: "number",
options: listStatusOptions,
className: "w-full sm:w-36",
},
],
[companyOptions, genderOptions, listStatusOptions, t],
);
const columns = useMemo<DashboardCrudColumn<AdminCustomer>[]>(
() => [
{
key: "id",
label: "ID",
className: "w-20",
render: (item) => item.id,
},
{
key: "name",
label: t("customer.columnName"),
render: (item) => <span className="font-medium">{item.name}</span>,
},
{
key: "gender",
label: t("customer.columnGender"),
className: "w-20",
render: (item) => (
<span className="text-muted-foreground">
{getGenderText(item.gender, t)}
</span>
),
},
{
key: "company",
label: t("customer.columnCompany"),
render: (item) => (
<span className="text-muted-foreground">
{item.companyId > 0
? (companyNameMap[item.companyId] ?? String(item.companyId))
: "-"}
</span>
),
},
{
key: "mobile",
label: t("customer.columnMobile"),
render: (item) => (
<span className="text-muted-foreground">
{item.primaryMobile || "-"}
</span>
),
},
{
key: "email",
label: t("customer.columnEmail"),
render: (item) => (
<span className="text-muted-foreground">
{item.primaryEmail || "-"}
</span>
),
},
createDashboardStatusColumn<AdminCustomer, number>({
label: t("customer.columnStatus"),
className: "w-24",
getStatus: (item) => item.status,
getLabel: (status) =>
status === Status.Ok ? t("status.ok") : t("status.disabled"),
getBadgeVariant: (status) =>
status === Status.Ok ? "default" : "secondary",
}),
],
[companyNameMap, t],
);
return (
<DashboardCrudPage<AdminCustomer, CustomerFormSavePayload>
filters={filters}
columns={columns}
fetchList={(query) =>
fetchCustomers({
keyword:
typeof query.keyword === "string" ? query.keyword : undefined,
status:
typeof query.status === "number" ? query.status : undefined,
gender:
typeof query.gender === "number" ? query.gender : undefined,
companyId:
typeof query.companyId === "number" ? query.companyId : undefined,
page: Number(query.page),
limit: Number(query.limit),
})
}
getItemId={(item) => item.id}
createItem={saveCustomerProfile}
updateItem={(_item, payload) => saveCustomerProfile(payload)}
deleteItem={(item) => deleteCustomer(item.id)}
canDelete={(item) => item.status !== Status.Deleted}
rowActions={[
createDashboardStatusToggleAction<AdminCustomer, number>({
icon: (item) =>
item.status === Status.Ok ? <BanIcon /> : <CheckCircle2Icon />,
label: (item) =>
item.status === Status.Ok
? t("customer.disable")
: t("customer.enable"),
disabled: (item) => item.status === Status.Deleted,
getNextStatus: (item) =>
item.status === Status.Ok ? Status.Disabled : Status.Ok,
updateStatus: (item, nextStatus) =>
updateCustomerStatus(item.id, nextStatus),
successMessage: (item, nextStatus) =>
t(nextStatus === Status.Ok ? "customer.enabled" : "customer.disabled", {
name: item.name,
}),
errorMessage: t("customer.statusUpdateFailed"),
}),
]}
renderEditDialog={({ open, saving, itemId, onOpenChange, onSubmit }) => (
<EditDialog
open={open}
saving={saving}
itemId={itemId}
onOpenChange={onOpenChange}
onSave={onSubmit}
/>
)}
labels={{
refresh: t("customer.refresh"),
create: t("customer.new"),
query: t("customer.query"),
loading: t("customer.loading"),
empty: t("customer.empty"),
actions: t("customer.columnActions"),
edit: t("customer.edit"),
delete: t("customer.delete"),
processing: t("customer.processing"),
moreActions: (item) => t("customer.moreActions", { name: item.name }),
loadFailed: t("customer.loadFailed"),
saveFailed: t("customer.saveFailed"),
deleteFailed: t("customer.deleteFailed"),
created: (payload) => t("customer.created", { name: payload.name }),
updated: (item) => t("customer.updated", { name: item.name }),
deleted: (item) => t("customer.deleted", { name: item.name }),
}}
/>
);
}