chore: import clean Agent Desk snapshot (6/7)
This commit is contained in:
266
web/app/(dashboard)/dashboard/customers/page.tsx
Normal file
266
web/app/(dashboard)/dashboard/customers/page.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
"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 }),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user