diff --git a/src/pages/Inventory/Import/index.tsx b/src/pages/Inventory/Import/index.tsx index d05981a..a3d9b11 100644 --- a/src/pages/Inventory/Import/index.tsx +++ b/src/pages/Inventory/Import/index.tsx @@ -5,6 +5,55 @@ import { Upload, Card, message, Button, Alert, Space } from 'antd'; const { Dragger } = Upload; +const templateHeaders = [ + '产品名称', + '规格型号', + '颜色', + '色号', + '产品码', + '销售价', + '批次号', + '纱线配比', + '经线克重(g/m)', + '纬线克重(g/m)', + '生产工艺', + '备注', +]; + +const templateSample = [ + '示例布料', + '120cm', + '本色', + '01', + '', + '0', + '', + '{"warp":[],"weft":[]}', + '0', + '0', + '无', + '', +]; + +const escapeHtml = (value: string) => + value.replace(/&/g, '&').replace(//g, '>'); + +const downloadTemplate = () => { + const rows = [templateHeaders, templateSample] + .map((row) => `${row.map((cell) => `${escapeHtml(cell)}`).join('')}`) + .join(''); + const html = `${rows}
`; + const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = '产品导入模板.xls'; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); +}; + const ImportPage: React.FC = () => { const token = localStorage.getItem('token'); @@ -14,15 +63,15 @@ const ImportPage: React.FC = () => { - + { if (info.file.status === 'done') { @@ -39,7 +88,7 @@ const ImportPage: React.FC = () => { >

点击或拖拽文件到此处上传

-

支持 .xlsx、.xls、.csv 格式

+

支持 .xlsx、.xls 格式

diff --git a/src/pages/Inventory/Products/index.test.tsx b/src/pages/Inventory/Products/index.test.tsx index 7faf4c3..c30774b 100644 --- a/src/pages/Inventory/Products/index.test.tsx +++ b/src/pages/Inventory/Products/index.test.tsx @@ -7,7 +7,12 @@ vi.mock('@/services/api'); import { smokeTest } from '../../../../test/smoke'; import Page from './index'; -import { createProduct, getProductSummary } from '@/services/api'; +import { createProduct, getProductSummary, listYarns } from '@/services/api'; + +const clickDialogSubmit = async (user: ReturnType, dialog: HTMLElement) => { + const submitBtn = within(dialog).getByRole('button', { name: /确\s*定|确\s*认|提交|OK/i }); + await user.click(submitBtn); +}; describe('Inventory / Products page', () => { beforeEach(() => { @@ -56,19 +61,111 @@ describe('Inventory / Products page', () => { const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ }); await user.type(nameInput, '测试产品'); - // Click the submit/OK button – find the non-close button in the dialog footer - const buttons = within(dialog).getAllByRole('button'); - const submitBtn = buttons.find((b) => { - const text = b.textContent?.trim() ?? ''; - return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text); - }); - if (submitBtn) await user.click(submitBtn); + await clickDialogSubmit(user, dialog); await waitFor(() => { expect(vi.mocked(createProduct)).toHaveBeenCalled(); }); }); + it('renders yarn ratio tables and blocks weight calculation until both sides are complete', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + await waitFor(() => screen.getByRole('dialog')); + + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getAllByText('纱线名称/纱线颜色')).toHaveLength(2); + expect(within(dialog).getAllByText('使用配比(合计 10)')).toHaveLength(2); + expect(within(dialog).getAllByRole('button', { name: '根据纱线配比计算' })).toHaveLength(1); + + await user.click(within(dialog).getByRole('button', { name: '根据纱线配比计算' })); + expect(await within(dialog).findByText('请先添加经线和纬线纱线,并确保两边配比合计均为 10')).toBeInTheDocument(); + }); + + it('submits selected yarn ratio rows as initial batch yarnRatio', async () => { + vi.mocked(listYarns).mockResolvedValue({ + code: 0, + msg: 'ok', + data: { + total: 1, + list: [ + { yarnId: 'yarn-1', yarnName: 'A纱', color: '红', weightGM: '20', supplierId: 'supplier-1' }, + ], + }, + } as any); + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + await waitFor(() => screen.getByRole('dialog')); + + const dialog = screen.getByRole('dialog'); + await user.type(within(dialog).getByRole('textbox', { name: /产品名称/ }), '测试产品'); + await waitFor(() => expect(vi.mocked(listYarns)).toHaveBeenCalled()); + + await user.click(within(dialog).getByRole('combobox', { name: '添加经线纱线' })); + await user.click(await screen.findByText('A纱 / 红')); + await clickDialogSubmit(user, dialog); + + await waitFor(() => { + expect(vi.mocked(createProduct)).toHaveBeenCalled(); + }); + const payload = vi.mocked(createProduct).mock.calls.at(-1)?.[0] as any; + const yarnRatio = JSON.parse(payload.initialBatch.yarnRatio); + expect(yarnRatio.warp).toEqual([ + { yarn_id: 'yarn-1', yarn_name: 'A纱', color: '红', ratio: 10 }, + ]); + expect(yarnRatio.weft).toEqual([]); + }); + + it('rejects product creation when an existing yarn side does not add up to ten', async () => { + vi.mocked(listYarns).mockResolvedValue({ + code: 0, + msg: 'ok', + data: { + total: 1, + list: [ + { yarnId: 'yarn-1', yarnName: 'A纱', color: '红', weightGM: '20', supplierId: 'supplier-1' }, + ], + }, + } as any); + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + await waitFor(() => screen.getByRole('dialog')); + + const dialog = screen.getByRole('dialog'); + await user.type(within(dialog).getByRole('textbox', { name: /产品名称/ }), '测试产品'); + await waitFor(() => expect(vi.mocked(listYarns)).toHaveBeenCalled()); + await user.click(within(dialog).getByRole('combobox', { name: '添加经线纱线' })); + await user.click(await screen.findByText('A纱 / 红')); + + const ratioInput = within(dialog).getByRole('spinbutton', { name: '经线A纱使用配比' }); + await user.click(ratioInput); + await user.keyboard('{Control>}a{/Control}9'); + await clickDialogSubmit(user, dialog); + + await waitFor(() => { + expect(screen.getAllByText('经线配比合计需为 10,当前为 9').length).toBeGreaterThan(0); + }); + expect(vi.mocked(createProduct)).not.toHaveBeenCalled(); + }); + it('renders Excel export button', async () => { render(); await act(async () => { @@ -133,12 +230,7 @@ describe('Inventory / Products page', () => { const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ }); await user.type(nameInput, '新布料'); - const buttons = within(dialog).getAllByRole('button'); - const submitBtn = buttons.find((b) => { - const text = b.textContent?.trim() ?? ''; - return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text); - }); - if (submitBtn) await user.click(submitBtn); + await clickDialogSubmit(user, dialog); await waitFor(() => { expect(vi.mocked(createProduct)).toHaveBeenCalled(); diff --git a/src/pages/Inventory/Products/index.tsx b/src/pages/Inventory/Products/index.tsx index 41a5eea..0a61216 100644 --- a/src/pages/Inventory/Products/index.tsx +++ b/src/pages/Inventory/Products/index.tsx @@ -1,6 +1,6 @@ import { PlusOutlined, UploadOutlined, DownloadOutlined, ArrowLeftOutlined } from '@ant-design/icons'; -import { ActionType, ModalForm, ProColumns, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components'; -import { Button, Card, Descriptions, Drawer, Input, InputNumber, List, message, Popconfirm, Space, Tag } from 'antd'; +import { ActionType, ModalForm, ProColumns, ProFormSelect, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components'; +import { Alert, Button, Card, Descriptions, Drawer, Form, Input, InputNumber, List, message, Popconfirm, Segmented, Select, Space, Table, Tag } from 'antd'; import { useRef, useState, useCallback } from 'react'; import { getProductSummary, @@ -9,14 +9,47 @@ import { createProduct, updateProduct, deleteProduct, + exportProducts, savePans, + listYarns, + createYarn, + listSuppliers, + listProductBatches, + createProductBatch, + updateProductBatch, + deleteProductBatch, } from '@/services/api'; -import type { ProductSummaryItem, ColorDetailItem, ProductTree, PanInput } from '@/types/api'; +import type { ProductSummaryItem, ColorDetailItem, ProductTree, PanInput, ProductBatchInfo, YarnInfo } from '@/types/api'; type ViewLevel = 'summary' | 'colors' | 'tree'; +type YarnSide = 'warp' | 'weft'; + +interface YarnRatioRow { + key: string; + yarnId: string; + yarnName: string; + yarnColor: string; + weightGM: string; + ratio: number; +} + +const CREATE_YARN_OPTION = '__create_yarn__'; +const ratioSideLabels: Record = { + warp: '经线', + weft: '纬线', +}; +const prominentErrorTextStyle = { color: '#ff1f1f', fontWeight: 600 }; + +const getRatioTotal = (rows: YarnRatioRow[]) => rows.reduce((sum, row) => sum + Number(row.ratio || 0), 0); + +const formatWeight = (value: number) => { + const fixed = value.toFixed(4); + return fixed.replace(/\.?0+$/, ''); +}; const ProductsPage: React.FC = () => { const summaryRef = useRef(); + const [createForm] = Form.useForm(); const [viewLevel, setViewLevel] = useState('summary'); const [selectedProductName, setSelectedProductName] = useState(''); const [selectedProductId, setSelectedProductId] = useState(''); @@ -26,20 +59,306 @@ const ProductsPage: React.FC = () => { const [editTreeDrawer, setEditTreeDrawer] = useState(false); const [currentColor, setCurrentColor] = useState(null); const [panInputs, setPanInputs] = useState([]); + const [batches, setBatches] = useState([]); + const [batchOpen, setBatchOpen] = useState(false); + const [currentBatch, setCurrentBatch] = useState(null); + const [yarnOpen, setYarnOpen] = useState(false); + const [yarnCreateTarget, setYarnCreateTarget] = useState(null); + const [yarnOptions, setYarnOptions] = useState([]); + const [warpYarns, setWarpYarns] = useState([]); + const [weftYarns, setWeftYarns] = useState([]); + const [ratioSubmitError, setRatioSubmitError] = useState(''); + const [weightCalcError, setWeightCalcError] = useState(''); + const [unit, setUnit] = useState<'cm' | 'in'>('cm'); + + const formatCmSpec = (spec?: string) => { + if (!spec || unit === 'cm') return spec; + return spec.replace(/(\d+(?:\.\d+)?)\s*cm/gi, (_, n) => `${(Number(n) / 2.54).toFixed(2)}in`); + }; + + const handleExport = async () => { + try { + const data = await exportProducts(); + const blob = data instanceof Blob ? data : new Blob([data as BlobPart]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `products-${Date.now()}.xlsx`; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + } catch { + message.error('导出失败'); + } + }; const loadTree = useCallback(async (productId: string) => { const res = await getProductTree(productId); if (res?.code === 0) { + const batchRes = await listProductBatches(productId); + setBatches(batchRes?.data?.list || []); setTreeData(res.data); setSelectedProductId(productId); setViewLevel('tree'); } }, []); + const loadYarnOptions = useCallback(async (keyWords?: string) => { + const res = await listYarns({ page: 1, pageSize: 50, yarnName: keyWords, status: 1 }); + if (res?.code === 0) { + setYarnOptions(res.data?.list || []); + } + }, []); + + const resetCreateProductState = useCallback(() => { + createForm.resetFields(); + setWarpYarns([]); + setWeftYarns([]); + setRatioSubmitError(''); + setWeightCalcError(''); + setYarnCreateTarget(null); + }, [createForm]); + + const handleCreateOpenChange = useCallback((open: boolean) => { + setCreateOpen(open); + if (open) { + resetCreateProductState(); + loadYarnOptions(); + } else { + resetCreateProductState(); + } + }, [loadYarnOptions, resetCreateProductState]); + + const getYarnRows = useCallback((side: YarnSide) => ( + side === 'warp' ? warpYarns : weftYarns + ), [warpYarns, weftYarns]); + + const setYarnRows = useCallback((side: YarnSide, updater: (rows: YarnRatioRow[]) => YarnRatioRow[]) => { + const wrappedUpdater = (rows: YarnRatioRow[]) => updater(rows); + if (side === 'warp') { + setWarpYarns(wrappedUpdater); + return; + } + setWeftYarns(wrappedUpdater); + }, []); + + const clearRatioFeedback = useCallback(() => { + setRatioSubmitError(''); + setWeightCalcError(''); + }, []); + + const appendYarnRatioRow = useCallback((side: YarnSide, yarn: YarnInfo) => { + setYarnRows(side, (rows) => { + if (rows.some((row) => row.yarnId === yarn.yarnId)) { + return rows; + } + const remaining = 10 - getRatioTotal(rows); + const ratio = remaining > 0 ? Math.min(10, remaining) : 1; + return [ + ...rows, + { + key: `${side}-${yarn.yarnId}-${Date.now()}`, + yarnId: yarn.yarnId, + yarnName: yarn.yarnName, + yarnColor: yarn.color || '未设置颜色', + weightGM: yarn.weightGM, + ratio, + }, + ]; + }); + clearRatioFeedback(); + }, [clearRatioFeedback, setYarnRows]); + + const handleSelectYarn = (side: YarnSide, value: string) => { + if (value === CREATE_YARN_OPTION) { + setYarnCreateTarget(side); + setYarnOpen(true); + return; + } + const yarn = yarnOptions.find((item) => item.yarnId === value); + if (yarn) { + appendYarnRatioRow(side, yarn); + } + }; + + const updateYarnRatio = (side: YarnSide, key: string, value: number | null) => { + const nextRatio = Math.max(1, Math.min(10, Math.round(Number(value || 1)))); + setYarnRows(side, (rows) => rows.map((row) => ( + row.key === key ? { ...row, ratio: nextRatio } : row + ))); + clearRatioFeedback(); + }; + + const removeYarnRatioRow = (side: YarnSide, key: string) => { + setYarnRows(side, (rows) => rows.filter((row) => row.key !== key)); + clearRatioFeedback(); + }; + + const getRatioError = (side: YarnSide, rows: YarnRatioRow[]) => { + if (rows.length === 0) { + return ''; + } + if (rows.some((row) => !row.yarnId)) { + return `${ratioSideLabels[side]}纱线不能为空`; + } + if (rows.some((row) => !Number.isInteger(row.ratio) || row.ratio < 1 || row.ratio > 10)) { + return `${ratioSideLabels[side]}使用配比必须为 1-10 的整数`; + } + const total = getRatioTotal(rows); + if (total !== 10) { + return `${ratioSideLabels[side]}配比合计需为 10,当前为 ${total}`; + } + return ''; + }; + + const getRatioErrors = () => ( + [ + getRatioError('warp', warpYarns), + getRatioError('weft', weftYarns), + ].filter(Boolean) + ); + + const serializeYarnRatio = () => { + if (warpYarns.length === 0 && weftYarns.length === 0) { + return ''; + } + return JSON.stringify({ + warp: warpYarns.map((row) => ({ + yarn_id: row.yarnId, + yarn_name: row.yarnName, + color: row.yarnColor, + ratio: row.ratio, + })), + weft: weftYarns.map((row) => ({ + yarn_id: row.yarnId, + yarn_name: row.yarnName, + color: row.yarnColor, + ratio: row.ratio, + })), + }); + }; + + const calculateSideWeight = (rows: YarnRatioRow[]) => rows.reduce((sum, row) => { + const weight = Number(row.weightGM || 0); + return sum + (Number.isFinite(weight) ? weight * row.ratio / 10 : 0); + }, 0); + + const calculateBatchWeight = () => { + const ratioErrors = getRatioErrors(); + if (warpYarns.length === 0 || weftYarns.length === 0) { + setWeightCalcError('请先添加经线和纬线纱线,并确保两边配比合计均为 10'); + return; + } + if (ratioErrors.length > 0) { + setWeightCalcError(ratioErrors.join(';')); + return; + } + const initialBatch = createForm.getFieldValue('initialBatch') || {}; + createForm.setFieldsValue({ + initialBatch: { + ...initialBatch, + warpWeightGM: formatWeight(calculateSideWeight(warpYarns)), + weftWeightGM: formatWeight(calculateSideWeight(weftYarns)), + }, + }); + setWeightCalcError(''); + message.success('已根据纱线配比计算克重'); + }; + + const renderYarnRatioTable = (side: YarnSide) => { + const rows = getYarnRows(side); + const label = ratioSideLabels[side]; + const error = getRatioError(side, rows); + const selectedYarnIds = new Set(rows.map((row) => row.yarnId)); + const options = [ + { label: `+ 创建纱线`, value: CREATE_YARN_OPTION }, + ...yarnOptions + .filter((yarn) => !selectedYarnIds.has(yarn.yarnId)) + .map((yarn) => ({ + label: `${yarn.yarnName} / ${yarn.color || '未设置颜色'}`, + value: yarn.yarnId, + })), + ]; + const total = getRatioTotal(rows); + + return ( +
+ + {label} + ({ label: batch.batchNo, value: batch.batchId }))} + onChange={(value) => { + const next = [...panInputs]; + next[pi] = { ...next[pi], batchId: value || undefined }; + setPanInputs(next); + }} +/> {pan.boltLengths?.map((len, bi) => ( diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index be8dcfe..365c343 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { history, useModel, request } from '@umijs/max'; import { Button, Form, Input, message, Select } from 'antd'; import { LockOutlined, UserOutlined, BankOutlined } from '@ant-design/icons'; @@ -6,6 +6,7 @@ import { getUserInfo, login } from '@/services/api'; import type { LoginParams } from '@/types/api'; const USER_CACHE_KEY = 'muyu_user_state'; +const DEFAULT_TENANT_OPTION = { label: '默认租户(t_default_001)', value: 't_default_001' }; const LoginPage: React.FC = () => { const { setInitialState } = useModel('@@initialState'); @@ -14,32 +15,41 @@ const LoginPage: React.FC = () => { const [searching, setSearching] = useState(false); const debounceRef = useRef>(); + const loadTenantOptions = useCallback(async (keyword = '') => { + setSearching(true); + try { + const res = await request('/api/v1/public/tenants/search', { + method: 'GET', + params: { keyword, limit: 20 }, + }); + if (res?.code === 0 && res.data?.list) { + const options = res.data.list.map((t: { id: string; name: string }) => ({ + label: `${t.name}(${t.id})`, + value: t.id, + })); + setTenantOptions(options.length ? options : [DEFAULT_TENANT_OPTION]); + } else { + setTenantOptions([DEFAULT_TENANT_OPTION]); + message.error('获取公司列表失败,请稍后重试'); + } + } catch { + setTenantOptions([DEFAULT_TENANT_OPTION]); + message.error('获取公司列表失败,请检查网络连接'); + } finally { + setSearching(false); + } + }, []); + + useEffect(() => { + loadTenantOptions(); + }, [loadTenantOptions]); + const handleTenantSearch = useCallback((keyword: string) => { if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(async () => { - setSearching(true); - try { - const res = await request('/api/v1/public/tenants/search', { - method: 'GET', - params: { keyword, limit: 20 }, - }); - if (res?.code === 0 && res.data?.list) { - setTenantOptions( - res.data.list.map((t: { id: string; name: string }) => ({ - label: `${t.name}(${t.id})`, - value: t.id, - })), - ); - } else { - message.error('获取公司列表失败,请稍后重试'); - } - } catch { - message.error('获取公司列表失败,请检查网络连接'); - } finally { - setSearching(false); - } + debounceRef.current = setTimeout(() => { + loadTenantOptions(keyword); }, 300); - }, []); + }, [loadTenantOptions]); const handleSubmit = async (values: LoginParams) => { setLoading(true); diff --git a/src/services/__mocks__/api.ts b/src/services/__mocks__/api.ts index 5a3d6c6..73d5343 100644 --- a/src/services/__mocks__/api.ts +++ b/src/services/__mocks__/api.ts @@ -60,6 +60,14 @@ export const exportProducts = vi.fn(ok); export const getProductSummary = vi.fn(ok); export const getColorDetail = vi.fn(ok); export const getProductTree = vi.fn(ok); +export const listYarns = vi.fn(ok); +export const createYarn = vi.fn(ok); +export const updateYarn = vi.fn(ok); +export const deleteYarn = vi.fn(ok); +export const listProductBatches = vi.fn(ok); +export const createProductBatch = vi.fn(ok); +export const updateProductBatch = vi.fn(ok); +export const deleteProductBatch = vi.fn(ok); export const listPans = vi.fn(ok); export const savePans = vi.fn(ok); export const listBolts = vi.fn(ok); diff --git a/src/services/api.ts b/src/services/api.ts index 9c7702c..316d5e8 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -15,6 +15,8 @@ import type { PanInfo, PanInput, Product, + ProductBatchInfo, + ProductBatchInput, ProductionPlanInfo, ProductSummaryItem, ProductTree, @@ -36,6 +38,7 @@ import type { Supplier, SystemConfig, User, + YarnInfo, } from '@/types/api'; // ── Auth ────────────────────────────────────────── @@ -118,13 +121,13 @@ export const updateConfig = (key: string, value: string) => // ── Products ────────────────────────────────────── -export const listProducts = (params: PaginationParams & { productName?: string; spec?: string; color?: string }) => +export const listProducts = (params: PaginationParams & { productName?: string; spec?: string; color?: string; colorNo?: string; productCode?: string }) => request>>('/api/v1/inventory/products', { method: 'GET', params }); export const getProduct = (id: string) => request>(`/api/v1/inventory/products/${id}`, { method: 'GET' }); -export const createProduct = (data: Partial & { pans?: PanInput[] }) => +export const createProduct = (data: Partial & { pans?: PanInput[]; initialBatch?: ProductBatchInput }) => request('/api/v1/inventory/products', { method: 'POST', data }); export const updateProduct = (id: string, data: Partial) => @@ -147,6 +150,30 @@ export const getColorDetail = (productName: string) => export const getProductTree = (id: string) => request>(`/api/v1/inventory/products/${id}/tree`, { method: 'GET' }); +export const listYarns = (params?: PaginationParams & { yarnName?: string; supplierId?: string; status?: number }) => + request>>('/api/v1/inventory/yarns', { method: 'GET', params }); + +export const createYarn = (data: Partial) => + request>('/api/v1/inventory/yarns', { method: 'POST', data }); + +export const updateYarn = (id: string, data: Partial) => + request(`/api/v1/inventory/yarns/${id}`, { method: 'PUT', data }); + +export const deleteYarn = (id: string) => + request(`/api/v1/inventory/yarns/${id}`, { method: 'DELETE' }); + +export const listProductBatches = (productId: string) => + request>(`/api/v1/inventory/products/${productId}/batches`, { method: 'GET' }); + +export const createProductBatch = (productId: string, batch: ProductBatchInput) => + request>(`/api/v1/inventory/products/${productId}/batches`, { method: 'POST', data: { batch } }); + +export const updateProductBatch = (productId: string, batchId: string, batch: ProductBatchInput, status?: number) => + request(`/api/v1/inventory/products/${productId}/batches/${batchId}`, { method: 'PUT', data: status === undefined ? { batch } : { batch, status } }); + +export const deleteProductBatch = (productId: string, batchId: string) => + request(`/api/v1/inventory/products/${productId}/batches/${batchId}`, { method: 'DELETE' }); + // ── Pan / Bolt CRUD ─────────────────────────────── export const listPans = (productId: string) => diff --git a/src/types/api.ts b/src/types/api.ts index e93d269..fca21f2 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -40,6 +40,8 @@ export interface Product { imageUrl?: string; spec?: string; color?: string; + colorNo?: string; + productCode?: string; salesPrice: string; remark?: string; status?: number; @@ -57,6 +59,8 @@ export interface BoltInfo { export interface PanInfo { panId: string; productId: string; + batchId?: string; + batchNo?: string; name: string; position: string; sortOrder: number; @@ -69,6 +73,7 @@ export interface PanInput { name: string; position?: string; boltLengths?: string[]; + batchId?: string; } export interface ProductTree { @@ -94,6 +99,8 @@ export interface ColorDetailItem { productId: string; productName: string; color: string; + colorNo?: string; + productCode?: string; spec: string; imageUrl: string; panCount: number; @@ -101,6 +108,39 @@ export interface ColorDetailItem { totalLengthM: string; locations: string; salesPrice: string; + batchCount?: number; +} + +export interface YarnInfo { + yarnId: string; + yarnName: string; + color: string; + weightGM: string; + supplierId: string; + supplierName?: string; + dyeFactory?: string; + imageUrl?: string; + remark?: string; + status?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface ProductBatchInput { + yarnRatio?: string; + warpWeightGM?: string; + weftWeightGM?: string; + productionProcess?: string; + remark?: string; +} + +export interface ProductBatchInfo extends ProductBatchInput { + batchId: string; + productId: string; + batchNo: string; + status?: number; + createdAt?: string; + updatedAt?: string; } // ── Pan / Bolt dimension view ───────────────────── @@ -108,6 +148,8 @@ export interface ColorDetailItem { export interface PanListItem { panId: string; productId: string; + batchId?: string; + batchNo?: string; name: string; position: string; productName: string;