背景:A3 需把行业 / 客户 / 客户组 / 客户级字段立为资产根节点;用户确认两级行业树与混搭 UI(行业 B / 客户列表 A / 详情 A / 新建 B / 客户组 B / 字段 A)。 改动: - 新增 ADR-002,行业改为两级树,客户挂末级行业;同步 PRD / 技术架构 / 开发计划。 - 新增 client 模块:industries / customers / customer-groups / customer-fields 接口、真实 PrismaCustomerDirectory、客户状态机与客户转交、客户组数据范围解析、跨客户 404。 - 新增迁移与幂等种子(电商/金融两级行业、2 客户、1 客户组、4 条字段)。 - 前端新增行业/客户/客户组/字段审批页面与路由、导航更名(客户管理 / 行业管理 / 客户组),复用现有 token。 - 补充 shared 契约单测、client 集成用例、A3 浏览器 e2e。 影响面:client 模块与 client 表、shared 契约、web 路由/导航、ScopeContext 数据范围、OpenAPI 与种子;A1 客户转交占位由 client 实现替换。 回滚:回滚本提交;数据库保留新增表或按报告执行 down SQL;CUSTOMER_DIRECTORY 可切回空集实现。 验证:format:check / lint / typecheck / test 全绿(shared 131 / api 90 / web 125,共 346);test:int 5 文件 39 条;A3 e2e 1/1;build 8/8;真实浏览器六页验证通过。
354 lines
8.9 KiB
TypeScript
354 lines
8.9 KiB
TypeScript
import type {
|
|
ApplyCustomerFieldInput,
|
|
ApproveCustomerFieldInput,
|
|
CreateCustomerGroupInput,
|
|
CreateCustomerInput,
|
|
CreateIndustryInput,
|
|
CustomerDetail,
|
|
CustomerField,
|
|
CustomerFieldListItem,
|
|
CustomerFieldListQuery,
|
|
CustomerGroupDetail,
|
|
CustomerGroupSummary,
|
|
CustomerListQuery,
|
|
CustomerSummary,
|
|
IndustryDetail,
|
|
IndustryListQuery,
|
|
IndustryTreeItem,
|
|
SetCustomerGroupsInput,
|
|
SetCustomerStatusInput,
|
|
SetIndustryStatusInput,
|
|
TransferCustomerInput,
|
|
UpdateCustomerGroupInput,
|
|
UpdateCustomerGroupMembersInput,
|
|
UpdateCustomerInput,
|
|
UpdateIndustryInput,
|
|
} from '@lingxi/shared';
|
|
import {
|
|
keepPreviousData,
|
|
useMutation,
|
|
type UseMutationResult,
|
|
useQuery,
|
|
useQueryClient,
|
|
type UseQueryResult,
|
|
} from '@tanstack/react-query';
|
|
|
|
import {
|
|
applyCustomerField,
|
|
approveCustomerField,
|
|
createCustomer,
|
|
createCustomerGroup,
|
|
createIndustry,
|
|
deleteCustomerGroup,
|
|
deleteIndustry,
|
|
fetchCustomerDetail,
|
|
fetchCustomerFieldList,
|
|
fetchCustomerFields,
|
|
fetchCustomerGroupDetail,
|
|
fetchCustomerGroupList,
|
|
fetchCustomerList,
|
|
fetchIndustryDetail,
|
|
fetchIndustryList,
|
|
setCustomerGroups,
|
|
setCustomerStatus,
|
|
setIndustryStatus,
|
|
transferCustomer,
|
|
updateCustomer,
|
|
updateCustomerGroup,
|
|
updateCustomerGroupMembers,
|
|
updateIndustry,
|
|
} from '@/api/customers';
|
|
import { queryKeys } from '@/api/query-keys';
|
|
|
|
export function useIndustriesQuery(
|
|
query: Partial<IndustryListQuery> = {},
|
|
): UseQueryResult<{ items: IndustryTreeItem[]; next_cursor: string | null }> {
|
|
return useQuery({
|
|
queryKey: queryKeys.industryList(query),
|
|
queryFn: () => fetchIndustryList(query),
|
|
staleTime: 10_000,
|
|
});
|
|
}
|
|
|
|
export function useIndustryDetailQuery(id: string | null): UseQueryResult<IndustryDetail> {
|
|
return useQuery({
|
|
queryKey: queryKeys.industry(id ?? ''),
|
|
queryFn: () => fetchIndustryDetail(id ?? ''),
|
|
enabled: id !== null && id !== '',
|
|
});
|
|
}
|
|
|
|
export function useCustomersQuery(
|
|
query: Partial<CustomerListQuery> = {},
|
|
): UseQueryResult<{ items: CustomerSummary[]; next_cursor: string | null }> {
|
|
return useQuery({
|
|
queryKey: queryKeys.customerList(query),
|
|
queryFn: () => fetchCustomerList(query),
|
|
placeholderData: keepPreviousData,
|
|
staleTime: 10_000,
|
|
});
|
|
}
|
|
|
|
export function useCustomerDetailQuery(id: string | null): UseQueryResult<CustomerDetail> {
|
|
return useQuery({
|
|
queryKey: queryKeys.customer(id ?? ''),
|
|
queryFn: () => fetchCustomerDetail(id ?? ''),
|
|
enabled: id !== null && id !== '',
|
|
});
|
|
}
|
|
|
|
export function useCustomerGroupsQuery(): UseQueryResult<{
|
|
items: CustomerGroupSummary[];
|
|
next_cursor: string | null;
|
|
}> {
|
|
const query = { limit: 100 };
|
|
return useQuery({
|
|
queryKey: queryKeys.customerGroupList(query),
|
|
queryFn: () => fetchCustomerGroupList(query),
|
|
staleTime: 10_000,
|
|
});
|
|
}
|
|
|
|
export function useCustomerGroupDetailQuery(
|
|
id: string | null,
|
|
): UseQueryResult<CustomerGroupDetail> {
|
|
return useQuery({
|
|
queryKey: queryKeys.customerGroup(id ?? ''),
|
|
queryFn: () => fetchCustomerGroupDetail(id ?? ''),
|
|
enabled: id !== null && id !== '',
|
|
});
|
|
}
|
|
|
|
export function useCustomerFieldsQuery(
|
|
query: Partial<CustomerFieldListQuery> = {},
|
|
): UseQueryResult<{ items: CustomerFieldListItem[]; next_cursor: string | null }> {
|
|
return useQuery({
|
|
queryKey: queryKeys.customerFields(query),
|
|
queryFn: () => fetchCustomerFieldList(query),
|
|
placeholderData: keepPreviousData,
|
|
staleTime: 10_000,
|
|
});
|
|
}
|
|
|
|
export function useCustomerFieldsOfQuery(
|
|
customerId: string | null,
|
|
): UseQueryResult<CustomerField[]> {
|
|
return useQuery({
|
|
queryKey: queryKeys.customerFieldsOf(customerId ?? ''),
|
|
queryFn: () => fetchCustomerFields(customerId ?? ''),
|
|
enabled: customerId !== null && customerId !== '',
|
|
});
|
|
}
|
|
|
|
function useInvalidateClient(): () => Promise<void> {
|
|
const queryClient = useQueryClient();
|
|
return async () => {
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.industries });
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.customers });
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.customerGroups });
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.customerFieldsRoot });
|
|
};
|
|
}
|
|
|
|
export function useCreateIndustryMutation(): UseMutationResult<
|
|
IndustryDetail,
|
|
Error,
|
|
CreateIndustryInput
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: createIndustry,
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateIndustryMutation(): UseMutationResult<
|
|
IndustryDetail,
|
|
Error,
|
|
{ id: string; input: UpdateIndustryInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => updateIndustry(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useSetIndustryStatusMutation(): UseMutationResult<
|
|
IndustryDetail,
|
|
Error,
|
|
{ id: string; input: SetIndustryStatusInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => setIndustryStatus(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteIndustryMutation(): UseMutationResult<void, Error, string> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: deleteIndustry,
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCreateCustomerMutation(): UseMutationResult<
|
|
CustomerDetail,
|
|
Error,
|
|
CreateCustomerInput
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: createCustomer,
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateCustomerMutation(): UseMutationResult<
|
|
CustomerDetail,
|
|
Error,
|
|
{ id: string; input: UpdateCustomerInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => updateCustomer(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useSetCustomerStatusMutation(): UseMutationResult<
|
|
CustomerDetail,
|
|
Error,
|
|
{ id: string; input: SetCustomerStatusInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => setCustomerStatus(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useTransferCustomerMutation(): UseMutationResult<
|
|
CustomerDetail,
|
|
Error,
|
|
{ id: string; input: TransferCustomerInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => transferCustomer(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useSetCustomerGroupsMutation(): UseMutationResult<
|
|
CustomerDetail,
|
|
Error,
|
|
{ id: string; input: SetCustomerGroupsInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => setCustomerGroups(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCreateCustomerGroupMutation(): UseMutationResult<
|
|
CustomerGroupDetail,
|
|
Error,
|
|
CreateCustomerGroupInput
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: createCustomerGroup,
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateCustomerGroupMutation(): UseMutationResult<
|
|
CustomerGroupDetail,
|
|
Error,
|
|
{ id: string; input: UpdateCustomerGroupInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => updateCustomerGroup(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateCustomerGroupMembersMutation(): UseMutationResult<
|
|
CustomerGroupDetail,
|
|
Error,
|
|
{ id: string; input: UpdateCustomerGroupMembersInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => updateCustomerGroupMembers(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteCustomerGroupMutation(): UseMutationResult<void, Error, string> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: deleteCustomerGroup,
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useApplyCustomerFieldMutation(): UseMutationResult<
|
|
CustomerField,
|
|
Error,
|
|
{ customerId: string; input: ApplyCustomerFieldInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ customerId, input }) => applyCustomerField(customerId, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useApproveCustomerFieldMutation(): UseMutationResult<
|
|
CustomerField,
|
|
Error,
|
|
{ id: string; input: ApproveCustomerFieldInput }
|
|
> {
|
|
const invalidate = useInvalidateClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, input }) => approveCustomerField(id, input),
|
|
onSuccess: async () => {
|
|
await invalidate();
|
|
},
|
|
});
|
|
}
|