From 09b6f4fbb03573faf31de11e3cf8811bc2ef7a4b Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 7 Mar 2026 17:44:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(P16C-4,P16C-6):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E4=B8=BB=E6=95=B0=E6=8D=AE=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E4=B8=8E=E7=9F=A9=E9=98=B5=E6=94=B6=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/masterData.ts | 79 ++++ .../components/master-data/SupplierTab.tsx | 360 ++++++++++++++++++ client/src/pages/MasterDataPage.tsx | 13 +- docs/feature-matrix.md | 14 +- .../2026-03-07_17-14-39-p16c-l5-suppliers.csv | 6 +- scripts/validate-feature-matrix.py | 1 + 6 files changed, 465 insertions(+), 8 deletions(-) create mode 100644 client/src/components/master-data/SupplierTab.tsx diff --git a/client/src/api/masterData.ts b/client/src/api/masterData.ts index fc8a6491..0c2834aa 100644 --- a/client/src/api/masterData.ts +++ b/client/src/api/masterData.ts @@ -94,6 +94,27 @@ export type HolidayCalendarDayItem = { updatedAt: string | null; }; +export type SupplierItem = { + id: string; + code: string; + name: string; + shortName: string | null; + taxNo: string | null; + contactName: string | null; + contactPhone: string | null; + paymentTerms: string | null; + legalEntityId: string | null; + enabled: boolean; + archivedAt: string | null; + createdAt: string | null; + updatedAt: string | null; + legalEntity: { + id: string; + code: string; + name: string; + } | null; +}; + export type MasterDataListResponse = { includeArchived: boolean; items: T[]; @@ -345,3 +366,61 @@ export async function replaceHolidayCalendarDays( }, ); } + +export async function fetchSuppliers(params: { + includeArchived?: boolean; + enabled?: boolean; + legalEntityId?: string; + q?: string; +} = {}) { + return apiRequestData>( + `/api/master-data/suppliers${toQueryString(params as Record)}`, + ); +} + +export async function createSupplier(payload: { + code: string; + name: string; + shortName?: string | null; + taxNo?: string | null; + contactName?: string | null; + contactPhone?: string | null; + paymentTerms?: string | null; + legalEntityId?: string | null; + enabled?: boolean; +}) { + return apiRequestData<{ supplier: SupplierItem }>("/api/master-data/suppliers", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function patchSupplier( + supplierId: string, + payload: Partial<{ + code: string; + name: string; + shortName: string | null; + taxNo: string | null; + contactName: string | null; + contactPhone: string | null; + paymentTerms: string | null; + legalEntityId: string | null; + enabled: boolean; + }>, +) { + return apiRequestData<{ supplier: SupplierItem }>(`/api/master-data/suppliers/${supplierId}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +export async function archiveSupplier(supplierId: string) { + return apiRequestData<{ supplier: SupplierItem; outcome: string }>( + `/api/master-data/suppliers/${supplierId}/archive`, + { + method: "POST", + body: JSON.stringify({}), + }, + ); +} diff --git a/client/src/components/master-data/SupplierTab.tsx b/client/src/components/master-data/SupplierTab.tsx new file mode 100644 index 00000000..3ee113e7 --- /dev/null +++ b/client/src/components/master-data/SupplierTab.tsx @@ -0,0 +1,360 @@ +import { useMemo, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { mapApiErrorMessage } from "../../api/errorMessages"; +import { + archiveSupplier, + createSupplier, + fetchSuppliers, + type LegalEntityItem, + type SupplierItem, +} from "../../api/masterData"; +import { isApiError } from "../../api/types"; +import { ConfirmDialog } from "../common/ConfirmDialog"; +import { StatusBadge } from "../common/StatusBadge"; +import { TableSkeleton } from "../common/TableSkeleton"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; +import { Textarea } from "../ui/textarea"; + +type SupplierTabProps = { + canManage: boolean; + includeArchived: boolean; + legalEntities: LegalEntityItem[]; +}; + +function resolveError(error: unknown) { + if (isApiError(error)) { + return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; + } + if (error instanceof Error) { + return error.message; + } + return "操作失败,请稍后重试。"; +} + +function toEnabledFilter(value: "" | "true" | "false") { + if (value === "") { + return undefined; + } + return value === "true"; +} + +function renderEnabledBadge(item: { archivedAt: string | null; enabled: boolean }) { + if (item.archivedAt) { + return ; + } + if (item.enabled) { + return ; + } + return ; +} + +function renderLegalEntityLabel(item: SupplierItem) { + if (!item.legalEntity) { + return 全局; + } + return `${item.legalEntity.code} / ${item.legalEntity.name}`; +} + +export function SupplierTab({ canManage, includeArchived, legalEntities }: SupplierTabProps) { + const queryClient = useQueryClient(); + const [error, setError] = useState(""); + const [enabledFilter, setEnabledFilter] = useState<"" | "true" | "false">(""); + const [legalEntityFilter, setLegalEntityFilter] = useState(""); + const [keyword, setKeyword] = useState(""); + + const [supplierCode, setSupplierCode] = useState(""); + const [supplierName, setSupplierName] = useState(""); + const [supplierShortName, setSupplierShortName] = useState(""); + const [supplierTaxNo, setSupplierTaxNo] = useState(""); + const [supplierContactName, setSupplierContactName] = useState(""); + const [supplierContactPhone, setSupplierContactPhone] = useState(""); + const [supplierPaymentTerms, setSupplierPaymentTerms] = useState(""); + const [supplierLegalEntityId, setSupplierLegalEntityId] = useState(""); + const [confirmArchiveId, setConfirmArchiveId] = useState(null); + + const suppliersQuery = useQuery({ + queryKey: ["master-data", "suppliers", includeArchived, enabledFilter, legalEntityFilter, keyword], + queryFn: () => + fetchSuppliers({ + includeArchived, + enabled: toEnabledFilter(enabledFilter), + legalEntityId: legalEntityFilter || undefined, + q: keyword.trim() || undefined, + }), + }); + + const suppliers = suppliersQuery.data?.items ?? []; + const activeLegalEntities = useMemo( + () => legalEntities.filter((item) => !item.archivedAt), + [legalEntities], + ); + + const invalidateSuppliers = async () => { + await queryClient.invalidateQueries({ queryKey: ["master-data", "suppliers"] }); + }; + + const createMutation = useMutation({ + mutationFn: async () => + createSupplier({ + code: supplierCode, + name: supplierName, + shortName: supplierShortName || null, + taxNo: supplierTaxNo || null, + contactName: supplierContactName || null, + contactPhone: supplierContactPhone || null, + paymentTerms: supplierPaymentTerms || null, + legalEntityId: supplierLegalEntityId || null, + }), + onSuccess: async () => { + await invalidateSuppliers(); + setSupplierCode(""); + setSupplierName(""); + setSupplierShortName(""); + setSupplierTaxNo(""); + setSupplierContactName(""); + setSupplierContactPhone(""); + setSupplierPaymentTerms(""); + setSupplierLegalEntityId(""); + setError(""); + toast.success("供应商已创建"); + }, + onError: (mutationError) => { + const message = resolveError(mutationError); + setError(message); + toast.error(message); + }, + }); + + const archiveMutation = useMutation({ + mutationFn: async (supplierId: string) => archiveSupplier(supplierId), + onSuccess: async () => { + await invalidateSuppliers(); + setConfirmArchiveId(null); + setError(""); + toast.success("供应商已归档"); + }, + onError: (mutationError) => { + const message = resolveError(mutationError); + setConfirmArchiveId(null); + setError(message); + toast.error(message); + }, + }); + + return ( +
+ {error ? ( +

{error}

+ ) : null} + +
+
+ + + setKeyword(event.target.value)} + placeholder="搜索编码 / 名称 / 税号 / 联系人" + /> +
+
+ + {canManage ? ( +
+

新建供应商

+
+
+ + setSupplierCode(event.target.value)} + placeholder="VENDOR_001" + /> +
+
+ + setSupplierName(event.target.value)} + placeholder="海音达演示供应商" + /> +
+
+ + setSupplierShortName(event.target.value)} + placeholder="演示供应商" + /> +
+
+ + setSupplierTaxNo(event.target.value)} + placeholder="9144..." + /> +
+
+ + setSupplierContactName(event.target.value)} + placeholder="张采购" + /> +
+
+ + setSupplierContactPhone(event.target.value)} + placeholder="0755-12345678" + /> +
+
+ +