feat(client): A3 客户与行业目录(两级行业、客户组、字段审批)
背景: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;真实浏览器六页验证通过。
This commit is contained in:
1 parent
8626996bb1
commit
140dc386c1
54 files changed
+9018
-147
No files matched your search
@@ -0,0 +1,353 @@
|
||||
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();
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user