From f5011edde63a204e81c84463bb5e37ece862e06b Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Tue, 14 Jul 2026 11:12:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BB=93=E5=82=A8=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=9A=E6=93=8D=E4=BD=9C=E5=88=97=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E3=80=81=E7=A1=AE=E8=AE=A4=E6=94=B6=E8=B4=A7=E4=B8=8E=E6=8D=A2?= =?UTF-8?q?=E5=BA=93=E4=BD=8D=E5=AF=B9=E6=8E=A5=E3=80=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=BA=93=E4=BD=8D=E7=AE=A1=E7=90=86=E9=A1=B5=EF=BC=8C=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=AB=AF=E6=94=AF=E6=8C=81=E6=8C=89=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AD=9B=E9=80=89=EF=BC=88=E4=B8=8D=E9=80=89=E5=88=99=E6=9F=A5?= =?UTF-8?q?=E5=85=A8=E9=83=A8=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- nginx-default.conf | 3 + nginx-docker.conf | 1 + src/composables/useWarehouseAdminCustomer.ts | 94 ++++ src/constants/pageButtonCatalog.ts | 12 + src/locale/en-US.ts | 40 ++ src/locale/zh-CN.ts | 40 ++ src/router/routes/newModules/warehouse.ts | 15 + src/styles/warehouse-list-page.less | 34 ++ src/utils/warehouseActionRenderer.ts | 28 ++ src/views/warehouse/binLocation/index.vue | 449 +++++++++++++++++++ src/views/warehouse/inboundNotice/index.vue | 266 ++++++++++- src/views/warehouse/inventory/index.vue | 126 +++++- src/views/warehouse/parcel/index.vue | 64 ++- src/views/warehouse/sku/index.vue | 40 +- vite.config.ts | 8 +- 15 files changed, 1167 insertions(+), 53 deletions(-) create mode 100644 src/composables/useWarehouseAdminCustomer.ts create mode 100644 src/utils/warehouseActionRenderer.ts create mode 100644 src/views/warehouse/binLocation/index.vue diff --git a/nginx-default.conf b/nginx-default.conf index 87b5653..b6de830 100644 --- a/nginx-default.conf +++ b/nginx-default.conf @@ -137,6 +137,9 @@ server { location = /warehouse/sku { try_files /index.html =200; } + location = /warehouse/binLocation { + try_files /index.html =200; + } location ^~ /warehouse/inboundNotice { try_files /index.html =200; } diff --git a/nginx-docker.conf b/nginx-docker.conf index 11b68e3..f55b230 100644 --- a/nginx-docker.conf +++ b/nginx-docker.conf @@ -52,6 +52,7 @@ server { location = /warehouse/parcel { try_files /index.html =200; } location = /warehouse/inventory { try_files /index.html =200; } location = /warehouse/sku { try_files /index.html =200; } + location = /warehouse/binLocation { try_files /index.html =200; } location ^~ /warehouse/inboundNotice { try_files /index.html =200; } location ^~ /channel/salesProducts { try_files /index.html =200; } diff --git a/src/composables/useWarehouseAdminCustomer.ts b/src/composables/useWarehouseAdminCustomer.ts new file mode 100644 index 0000000..117f186 --- /dev/null +++ b/src/composables/useWarehouseAdminCustomer.ts @@ -0,0 +1,94 @@ +import { computed, onMounted, ref } from 'vue' +import { useI18n } from 'vue-i18n' +import { CustomerService } from '@/api/customer' +import { extractCustomerId, mapCustomerRow } from '@/api/customer/resolve' +import { $baseMessage } from '@/utils' +import { useRbacUserStore } from '@/store/modules/rbac-user' + +/** + * 仓储页管理端客户上下文: + * - 列表/汇总:未选客户 → 不传 customerId → 查全部 + * - 写入:管理端须显式选择客户 + * 客户门户登录时由后端会话强制归属,前端不传 customerId。 + */ +export function useWarehouseAdminCustomer() { + const { t } = useI18n() + const rbacUserStore = useRbacUserStore() + + const isAdmin = computed(() => !rbacUserStore.isCustomerPortal()) + /** undefined = 不筛选(查全部);仅 >0 才作为筛选条件 */ + const customerId = ref() + const customerOptions = ref>([]) + const customerLoading = ref(false) + + function resolveSelectedCustomerId(): number | undefined { + const raw = customerId.value as number | string | null | undefined + if (raw == null || raw === '') return undefined + const id = Number(raw) + return Number.isFinite(id) && id > 0 ? id : undefined + } + + async function loadCustomers() { + if (!isAdmin.value) { + customerOptions.value = [] + return + } + customerLoading.value = true + try { + const { items } = await CustomerService.getList({ page: 1, pageSize: 500 }) + customerOptions.value = (items || []) + .map((row) => mapCustomerRow(row as Record)) + .map((c) => { + const id = extractCustomerId(c as Record) + if (!id) return null + const name = c.customerName || c.customerCode || c.phone || `#${id}` + const code = c.customerCode ? ` (${c.customerCode})` : '' + return { label: `${name}${code}`, value: id } + }) + .filter((x): x is { label: string; value: number } => !!x) + } catch { + customerOptions.value = [] + } finally { + customerLoading.value = false + } + } + + /** + * 追加到 list/save 请求体。 + * 管理端未选客户时显式去掉 customerId,保证「不选 = 查全部」。 + */ + function withCustomerId>(body: T): T & { customerId?: number } { + const next = { ...body } as T & { customerId?: number } + delete next.customerId + if (!isAdmin.value) return next + const id = resolveSelectedCustomerId() + if (id != null) next.customerId = id + return next + } + + /** 管理端写操作前校验已选客户;客户门户返回 undefined 由后端会话解析 */ + function ensureMutationCustomerId(): number | undefined { + if (!isAdmin.value) return undefined + const id = resolveSelectedCustomerId() + if (id == null) { + $baseMessage(t('warehouse.common.selectCustomerRequired'), 'warning') + return undefined + } + return id + } + + onMounted(() => { + loadCustomers() + }) + + return { + isAdmin, + customerId, + customerOptions, + customerLoading, + loadCustomers, + withCustomerId, + ensureMutationCustomerId, + resolveSelectedCustomerId, + } +} diff --git a/src/constants/pageButtonCatalog.ts b/src/constants/pageButtonCatalog.ts index fdc57e6..f88c980 100644 --- a/src/constants/pageButtonCatalog.ts +++ b/src/constants/pageButtonCatalog.ts @@ -117,6 +117,8 @@ export const PERMISSION_ACTION_LABELS: Record = { AddAnnouncement: '新增公告', AddDict: '新增字典', AddWarehouse: '新增仓库', + Relocate: '换库位', + ConfirmReceive: '确认收货', AddConfig: '新增配置', Debug: '调试', Rollback: '回滚到此版本', @@ -254,6 +256,11 @@ export const PAGE_BUTTON_PARENT_ALIASES: Record = { 'Api.Test.Query': 'Api.ApiTest', 'Channel.LabelDesign': 'Channel.LabelDesign', 'Order.BatchJobs.Query': 'Order.BatchJobs', + 'Warehouse.Sku.Query': 'Warehouse.Sku', + 'Warehouse.Inventory.Query': 'Warehouse.Inventory', + 'Warehouse.Parcel.Query': 'Warehouse.Parcel', + 'Warehouse.InboundNotice.Query': 'Warehouse.InboundNotice', + 'Warehouse.BinLocation.Query': 'Warehouse.BinLocation', }; type BtnInput = { code: string; name: string }; @@ -510,8 +517,13 @@ const CORE_GROUPS: PageButtonGroup[] = [ B('Warehouse.InboundNotice', [ { code: 'Query', name: '查询' }, { code: 'Save', name: '保存' }, + { code: 'ConfirmReceive', name: '确认收货' }, { code: 'Delete', name: '删除' }, ]), + B('Warehouse.BinLocation', [ + { code: 'Query', name: '查询' }, + { code: 'Save', name: '保存' }, + ]), // ── 渠道 ── B('Channel.SalesProducts', [ diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index b1ff871..d3751c8 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -90,6 +90,7 @@ export default { 'menu.warehouse.inventory': 'My Inventory', 'menu.warehouse.sku': 'My SKU', 'menu.warehouse.inboundNotice': 'Inbound Notice', + 'menu.warehouse.binLocation': 'Bin Locations', 'warehouse.common.search': 'Search', 'warehouse.common.add': 'Add', 'warehouse.common.edit': 'Edit', @@ -99,6 +100,9 @@ export default { 'warehouse.common.confirm': 'Confirm', 'warehouse.common.detail': 'Detail', 'warehouse.common.warehouse': 'Warehouse', + 'warehouse.common.customer': 'Customer', + 'warehouse.common.selectCustomer': 'All customers', + 'warehouse.common.selectCustomerRequired': 'Select a customer before create/update', 'warehouse.common.yes': 'Yes', 'warehouse.common.no': 'No', 'warehouse.sku.table.dimensions': 'Size(cm)', @@ -152,12 +156,40 @@ export default { 'warehouse.inventory.form.targetBin': 'Target Bin', 'warehouse.inventory.form.qty': 'Qty', 'warehouse.inventory.form.selectBin': 'Select bin', + 'warehouse.inventory.form.selectSourceBin': 'Select source bin with stock', + 'warehouse.inventory.form.selectTargetBin': 'Select target bin', 'warehouse.inventory.message.loadFailed': 'Failed to load inventory', 'warehouse.inventory.message.binDetailFailed': 'Failed to load bin detail', 'warehouse.inventory.message.relocateRequired': 'Fill relocate fields', 'warehouse.inventory.message.relocateSuccess': 'Relocated', 'warehouse.inventory.message.relocateFailed': 'Relocate failed', 'warehouse.inventory.message.sameBin': 'Source and target bin must differ', + 'warehouse.inventory.message.qtyExceed': 'Qty cannot exceed source bin stock', + 'warehouse.binLocation.table.binCode': 'Bin Code', + 'warehouse.binLocation.table.binName': 'Bin Name', + 'warehouse.binLocation.table.zoneCode': 'Zone', + 'warehouse.binLocation.table.isEnabled': 'Enabled', + 'warehouse.binLocation.table.remark': 'Remark', + 'warehouse.binLocation.search.warehouse': 'Search warehouse', + 'warehouse.binLocation.search.keyword': 'Code / Name', + 'warehouse.binLocation.filter.isEnabled': 'Enabled', + 'warehouse.binLocation.actions.refreshWarehouse': 'Refresh warehouses', + 'warehouse.binLocation.actions.add': 'Add Bin', + 'warehouse.binLocation.loading.warehouse': 'Loading warehouses...', + 'warehouse.binLocation.emptySelect': 'Select a warehouse on the left', + 'warehouse.binLocation.modal.add': 'Add Bin Location', + 'warehouse.binLocation.modal.edit': 'Edit Bin Location', + 'warehouse.binLocation.form.binCode': 'Bin Code', + 'warehouse.binLocation.form.binName': 'Bin Name', + 'warehouse.binLocation.form.zoneCode': 'Zone Code', + 'warehouse.binLocation.form.isEnabled': 'Enabled', + 'warehouse.binLocation.form.remark': 'Remark', + 'warehouse.binLocation.message.warehouseLoadFailed': 'Failed to load warehouses', + 'warehouse.binLocation.message.loadFailed': 'Failed to load bins', + 'warehouse.binLocation.message.selectWarehouse': 'Select a warehouse first', + 'warehouse.binLocation.message.required': 'Bin code is required', + 'warehouse.binLocation.message.saveSuccess': 'Saved', + 'warehouse.binLocation.message.saveFailed': 'Save failed', 'warehouse.parcel.status.all': 'All', 'warehouse.parcel.status.inTransit': 'In Transit', 'warehouse.parcel.status.arrived': 'Arrived', @@ -228,7 +260,11 @@ export default { 'warehouse.inbound.line.skuCode': 'SKU', 'warehouse.inbound.line.plannedQty': 'Planned', 'warehouse.inbound.line.receivedQty': 'Received', + 'warehouse.inbound.line.binLocation': 'Putaway Bin', + 'warehouse.inbound.form.selectBin': 'Select bin', 'warehouse.inbound.actions.delete': 'Delete', + 'warehouse.inbound.actions.confirmReceive': 'Confirm Receive', + 'warehouse.inbound.modal.confirmReceive': 'Confirm Receive & Putaway', 'warehouse.inbound.message.loadFailed': 'Failed to load inbound list', 'warehouse.inbound.message.saveSuccess': 'Saved', 'warehouse.inbound.message.saveFailed': 'Save failed', @@ -236,6 +272,10 @@ export default { 'warehouse.inbound.message.required': 'Select warehouse', 'warehouse.inbound.message.boxRequired': 'Add SKU lines to at least one box', 'warehouse.inbound.message.detailFailed': 'Failed to load detail', + 'warehouse.inbound.message.receiveBinRequired': 'Bin is required when received qty > 0', + 'warehouse.inbound.message.receiveQtyInvalid': 'Received qty cannot be negative', + 'warehouse.inbound.message.receiveSuccess': 'Receive succeeded', + 'warehouse.inbound.message.receiveFailed': 'Receive failed', 'order.platformStore.platform.manual': 'Manual', 'order.platformStore.table.siteCountry': 'Site Country', 'order.platformStore.table.autoFulfill': 'Auto Fulfill', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 8c1117f..0717bf7 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -90,6 +90,7 @@ export default { 'menu.warehouse.inventory': '我的库存', 'menu.warehouse.sku': '我的 SKU', 'menu.warehouse.inboundNotice': '入库预报', + 'menu.warehouse.binLocation': '库位管理', 'warehouse.common.search': '查询', 'warehouse.common.add': '新增', 'warehouse.common.edit': '编辑', @@ -99,6 +100,9 @@ export default { 'warehouse.common.confirm': '确定', 'warehouse.common.detail': '详情', 'warehouse.common.warehouse': '仓库', + 'warehouse.common.customer': '客户', + 'warehouse.common.selectCustomer': '全部客户', + 'warehouse.common.selectCustomerRequired': '新增/修改请先选择客户', 'warehouse.common.yes': '是', 'warehouse.common.no': '否', 'warehouse.sku.table.dimensions': '尺寸(cm)', @@ -152,12 +156,40 @@ export default { 'warehouse.inventory.form.targetBin': '目标库位', 'warehouse.inventory.form.qty': '数量', 'warehouse.inventory.form.selectBin': '请选择库位', + 'warehouse.inventory.form.selectSourceBin': '请选择有库存的源库位', + 'warehouse.inventory.form.selectTargetBin': '请选择目标库位', 'warehouse.inventory.message.loadFailed': '加载库存失败', 'warehouse.inventory.message.binDetailFailed': '加载库位明细失败', 'warehouse.inventory.message.relocateRequired': '请填写换库位信息', 'warehouse.inventory.message.relocateSuccess': '换库位成功', 'warehouse.inventory.message.relocateFailed': '换库位失败', 'warehouse.inventory.message.sameBin': '源库位与目标库位不能相同', + 'warehouse.inventory.message.qtyExceed': '数量不能超过源库位库存', + 'warehouse.binLocation.table.binCode': '库位编码', + 'warehouse.binLocation.table.binName': '库位名称', + 'warehouse.binLocation.table.zoneCode': '库区', + 'warehouse.binLocation.table.isEnabled': '启用', + 'warehouse.binLocation.table.remark': '备注', + 'warehouse.binLocation.search.warehouse': '搜索仓库', + 'warehouse.binLocation.search.keyword': '编码/名称', + 'warehouse.binLocation.filter.isEnabled': '启用状态', + 'warehouse.binLocation.actions.refreshWarehouse': '刷新仓库', + 'warehouse.binLocation.actions.add': '新增库位', + 'warehouse.binLocation.loading.warehouse': '正在加载仓库...', + 'warehouse.binLocation.emptySelect': '请选择左侧仓库', + 'warehouse.binLocation.modal.add': '新增库位', + 'warehouse.binLocation.modal.edit': '编辑库位', + 'warehouse.binLocation.form.binCode': '库位编码', + 'warehouse.binLocation.form.binName': '库位名称', + 'warehouse.binLocation.form.zoneCode': '库区编码', + 'warehouse.binLocation.form.isEnabled': '启用', + 'warehouse.binLocation.form.remark': '备注', + 'warehouse.binLocation.message.warehouseLoadFailed': '加载仓库失败', + 'warehouse.binLocation.message.loadFailed': '加载库位失败', + 'warehouse.binLocation.message.selectWarehouse': '请先选择仓库', + 'warehouse.binLocation.message.required': '请填写库位编码', + 'warehouse.binLocation.message.saveSuccess': '保存成功', + 'warehouse.binLocation.message.saveFailed': '保存失败', 'warehouse.parcel.status.all': '全部', 'warehouse.parcel.status.inTransit': '在途', 'warehouse.parcel.status.arrived': '已到仓', @@ -228,7 +260,11 @@ export default { 'warehouse.inbound.line.skuCode': 'SKU', 'warehouse.inbound.line.plannedQty': '计划', 'warehouse.inbound.line.receivedQty': '实收', + 'warehouse.inbound.line.binLocation': '上架库位', + 'warehouse.inbound.form.selectBin': '请选择库位', 'warehouse.inbound.actions.delete': '删除预报', + 'warehouse.inbound.actions.confirmReceive': '确认收货', + 'warehouse.inbound.modal.confirmReceive': '确认收货上架', 'warehouse.inbound.message.loadFailed': '加载入库预报失败', 'warehouse.inbound.message.saveSuccess': '保存成功', 'warehouse.inbound.message.saveFailed': '保存失败', @@ -236,6 +272,10 @@ export default { 'warehouse.inbound.message.required': '请选择仓库', 'warehouse.inbound.message.boxRequired': '请至少为一个箱子选择 SKU', 'warehouse.inbound.message.detailFailed': '加载详情失败', + 'warehouse.inbound.message.receiveBinRequired': '有实收数量时必须选择上架库位', + 'warehouse.inbound.message.receiveQtyInvalid': '实收数量不能为负', + 'warehouse.inbound.message.receiveSuccess': '收货成功', + 'warehouse.inbound.message.receiveFailed': '收货失败', 'order.platformStore.platform.manual': '手工店铺', 'order.platformStore.table.siteCountry': '站点国家', 'order.platformStore.table.autoFulfill': '自动发货', diff --git a/src/router/routes/newModules/warehouse.ts b/src/router/routes/newModules/warehouse.ts index 5890ac5..5ea6a8a 100644 --- a/src/router/routes/newModules/warehouse.ts +++ b/src/router/routes/newModules/warehouse.ts @@ -20,6 +20,7 @@ const WAREHOUSE: AppRouteRecordRaw = { locale: 'menu.warehouse.parcel', requiresAuth: true, permission: 'Warehouse.Parcel.Query', + order: 1, }, }, { @@ -30,6 +31,7 @@ const WAREHOUSE: AppRouteRecordRaw = { locale: 'menu.warehouse.inventory', requiresAuth: true, permission: 'Warehouse.Inventory.Query', + order: 2, }, }, { @@ -40,6 +42,7 @@ const WAREHOUSE: AppRouteRecordRaw = { locale: 'menu.warehouse.sku', requiresAuth: true, permission: 'Warehouse.Sku.Query', + order: 3, }, }, { @@ -50,6 +53,18 @@ const WAREHOUSE: AppRouteRecordRaw = { locale: 'menu.warehouse.inboundNotice', requiresAuth: true, permission: 'Warehouse.InboundNotice.Query', + order: 4, + }, + }, + { + path: 'binLocation', + name: 'WarehouseBinLocation', + component: () => import('@/views/warehouse/binLocation/index.vue'), + meta: { + locale: 'menu.warehouse.binLocation', + requiresAuth: true, + permission: 'Warehouse.BinLocation.Query', + order: 5, }, }, ], diff --git a/src/styles/warehouse-list-page.less b/src/styles/warehouse-list-page.less index 31eaba2..f569ecb 100644 --- a/src/styles/warehouse-list-page.less +++ b/src/styles/warehouse-list-page.less @@ -71,3 +71,37 @@ margin-left: 4px; color: var(--app-primary-color); } + +.warehouse-list-page-card .list-wrap :deep(.wh-action-group) { + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 6px; + width: 100%; +} + +.warehouse-list-page-card .list-wrap :deep(.wh-action-btn) { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + cursor: pointer; + color: var(--color-text-3); + transition: color 0.2s ease, background-color 0.2s ease; + user-select: none; +} + +.warehouse-list-page-card .list-wrap :deep(.wh-action-btn--view:hover), +.warehouse-list-page-card .list-wrap :deep(.wh-action-btn--edit:hover), +.warehouse-list-page-card .list-wrap :deep(.wh-action-btn--relocate:hover), +.warehouse-list-page-card .list-wrap :deep(.wh-action-btn--receive:hover) { + color: var(--app-primary-color); + background: var(--app-primary-color-light, #e6f7f7); +} + +.warehouse-list-page-card .list-wrap :deep(.wh-action-btn--delete:hover) { + color: rgb(var(--danger-6)); + background: rgb(var(--danger-1)); +} diff --git a/src/utils/warehouseActionRenderer.ts b/src/utils/warehouseActionRenderer.ts new file mode 100644 index 0000000..1bcbf59 --- /dev/null +++ b/src/utils/warehouseActionRenderer.ts @@ -0,0 +1,28 @@ +/** Handsontable 仓储列表操作列:图标按钮 HTML(配合 data-action 点击) */ + +const ICON_STYLE = 'width:17px;height:17px;display:block;pointer-events:none' + +export const warehouseActionIcons = { + view: ``, + edit: ``, + delete: ``, + relocate: ``, + receive: ``, +} as const + +export type WarehouseActionVariant = keyof typeof warehouseActionIcons + +export function renderWarehouseActionBtn( + action: string, + title: string, + variant: WarehouseActionVariant = action as WarehouseActionVariant, +): string { + const icon = warehouseActionIcons[variant] || warehouseActionIcons.view + const safeTitle = String(title).replace(/"/g, '"') + return `${icon}` +} + +export function wrapWarehouseActions(parts: string[]): string { + if (!parts.length) return '' + return `
${parts.join('')}
` +} diff --git a/src/views/warehouse/binLocation/index.vue b/src/views/warehouse/binLocation/index.vue new file mode 100644 index 0000000..414b78e --- /dev/null +++ b/src/views/warehouse/binLocation/index.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/src/views/warehouse/inboundNotice/index.vue b/src/views/warehouse/inboundNotice/index.vue index 9aa6e94..4e233d1 100644 --- a/src/views/warehouse/inboundNotice/index.vue +++ b/src/views/warehouse/inboundNotice/index.vue @@ -8,14 +8,16 @@ import ModalPro from '@/components/ModalPro/index.vue' import useModal from '@/components/ModalPro/useModal' import { useTableContainerHeight } from '@/composables/useTableContainerHeight' import { + confirmWarehouseInboundReceive, deleteWarehouseInbound, + getWarehouseBinLocationList, getWarehouseInboundDetail, getWarehouseInboundList, getWarehouseInboundSummary, + getWarehouseSkuList, saveWarehouseInbound, } from '@/api/warehouse' import { getWarehouseList } from '@/api/system/warehouse' -import { getWarehouseSkuList } from '@/api/warehouse' import { $baseMessage } from '@/utils' import { hasPermission } from '@/utils/rbac-permission' import { @@ -25,6 +27,8 @@ import { pickField, resolveWarehouseLabel, } from '@/utils/warehouseListMapper' +import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer' +import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer' defineOptions({ name: 'WarehouseInboundNotice' }) @@ -45,8 +49,31 @@ interface InboundBoxForm { lines: InboundLineForm[] } +interface ReceiveLineForm { + lineId: number + skuCode: string + plannedQty: number + receivedQty: number + binLocationId?: number +} + +interface ReceiveBoxForm { + boxId: number + markBoxNo: string + boxSeqNo: string + lines: ReceiveLineForm[] +} + const { t } = useI18n() const { tableContainerRef, tableHeight } = useTableContainerHeight() +const { + isAdmin, + customerId: adminCustomerId, + customerOptions, + withCustomerId, + ensureMutationCustomerId, + resolveSelectedCustomerId, +} = useWarehouseAdminCustomer() const receiptStatus = ref() const summary = ref({ forecasted: 0, received: 0, completed: 0 }) @@ -74,7 +101,12 @@ const detailVisible = ref(false) const detailData = ref(null) const [registerFormModal, { openModal, closeModal }] = useModal() as any +const [registerReceiveModal, { openModal: openReceiveModal, closeModal: closeReceiveModal }] = useModal() as any const saving = ref(false) +const receiveSaving = ref(false) +const receiveNotice = ref(null) +const receiveBoxes = ref([]) +const receiveBinOptions = ref>([]) const formModel = reactive({ id: 0, warehouseId: undefined as number | undefined, @@ -89,6 +121,11 @@ const modalWarehouseName = computed(() => { return wh?.label || '' }) +const receiveWarehouseName = computed(() => { + const whId = receiveNotice.value?.warehouseId + return resolveWarehouseLabel(whId, warehouseNameMap.value) +}) + const boxCount = computed(() => boxRows.value.length) const totalSkuCount = computed(() => boxRows.value.reduce((sum, box) => sum + box.lines.filter((l) => l.skuId).length, 0), @@ -207,7 +244,7 @@ async function loadWarehouses() { } async function loadSkus() { - const { items } = await getWarehouseSkuList({ page: 1, pageSize: 500 }).catch(() => ({ items: [] })) + const { items } = await getWarehouseSkuList(withCustomerId({ page: 1, pageSize: 500 })).catch(() => ({ items: [] })) skuOptions.value = (items || []).map((s: any) => ({ label: `${s.skuCode || s.SkuCode} - ${s.skuName || s.SkuName}`, value: Number(s.id || s.Id), @@ -217,7 +254,9 @@ async function loadSkus() { async function loadSummary() { try { - const data = await getWarehouseInboundSummary({ warehouseId: warehouseId.value }) + const data = await getWarehouseInboundSummary(withCustomerId({ + warehouseId: warehouseId.value, + })) summary.value = { forecasted: data?.forecasted ?? data?.Forecasted ?? 0, received: data?.received ?? data?.Received ?? 0, @@ -231,13 +270,13 @@ async function loadSummary() { async function loadList() { listLoading.value = true try { - const { items, total } = await getWarehouseInboundList({ + const { items, total } = await getWarehouseInboundList(withCustomerId({ page: currentPage.value, pageSize: pageSize.value, receiptStatus: receiptStatus.value, keyword: keyword.value || undefined, warehouseId: warehouseId.value, - }) + })) tableRows.value = (items || []).map((item: Record) => mapRow(item)) totalCount.value = Number(total || 0) } catch (e: any) { @@ -265,15 +304,129 @@ function handleSearch() { async function showDetail(row: any) { try { - detailData.value = await getWarehouseInboundDetail(row._raw?.id || row.id) + detailData.value = await getWarehouseInboundDetail( + row._raw?.id || row.id, + isAdmin.value ? resolveSelectedCustomerId() : undefined, + ) detailVisible.value = true } catch (e: any) { $baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error') } } +async function loadReceiveBinOptions(whId?: number) { + if (!whId) { + receiveBinOptions.value = [] + return + } + try { + const { items } = await getWarehouseBinLocationList({ warehouseId: whId, isEnabled: true }) + receiveBinOptions.value = (items || []).map((b: any) => ({ + label: `${b.binCode || b.BinCode}${b.binName || b.BinName ? ` - ${b.binName || b.BinName}` : ''}`, + value: Number(b.id || b.Id), + })) + } catch { + receiveBinOptions.value = [] + } +} + +async function openConfirmReceive(row: any) { + if ( + !( + hasPermission('Warehouse.InboundNotice.ConfirmReceive') + || hasPermission('Warehouse.InboundNotice.Save') + ) + || row._status === 'completed' + ) { + return + } + if (isAdmin.value && ensureMutationCustomerId() == null) return + try { + const detail = await getWarehouseInboundDetail( + row._raw?.id || row.id, + isAdmin.value ? resolveSelectedCustomerId() : undefined, + ) + const normalized = normalizeListRow(detail || {}) + const whId = pickField(normalized, 'warehouseId', 'WarehouseId') + const boxesRaw = pickField(normalized, 'boxes', 'Boxes') + const boxes = Array.isArray(boxesRaw) ? boxesRaw : [] + receiveNotice.value = { + id: pickField(normalized, 'id', 'Id'), + noticeNo: pickField(normalized, 'noticeNo', 'NoticeNo') ?? row.noticeNo, + warehouseId: whId, + } + receiveBoxes.value = boxes.map((box: any) => { + const b = normalizeListRow(box) + const linesRaw = pickField(b, 'lines', 'Lines') + const lines = Array.isArray(linesRaw) ? linesRaw : [] + return { + boxId: Number(pickField(b, 'id', 'Id') || 0), + markBoxNo: String(pickField(b, 'markBoxNo', 'MarkBoxNo') ?? ''), + boxSeqNo: String(pickField(b, 'boxSeqNo', 'BoxSeqNo') ?? ''), + lines: lines.map((line: any) => { + const l = normalizeListRow(line) + const planned = Number(pickField(l, 'plannedQty', 'PlannedQty') ?? 0) + const received = pickField(l, 'receivedQty', 'ReceivedQty') + return { + lineId: Number(pickField(l, 'id', 'Id') || 0), + skuCode: String(pickField(l, 'skuCode', 'SkuCode') ?? '-'), + plannedQty: planned, + receivedQty: received != null ? Number(received) : planned, + binLocationId: pickField(l, 'binLocationId', 'BinLocationId') || undefined, + } + }), + } + }) + await loadReceiveBinOptions(whId) + openReceiveModal(true) + } catch (e: any) { + $baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error') + } +} + +async function submitConfirmReceive() { + const notice = receiveNotice.value + if (!notice?.id) return + if (isAdmin.value && ensureMutationCustomerId() == null) return + for (const box of receiveBoxes.value) { + for (const line of box.lines) { + if (line.receivedQty > 0 && !line.binLocationId) { + $baseMessage(t('warehouse.inbound.message.receiveBinRequired'), 'warning') + return + } + if (line.receivedQty < 0) { + $baseMessage(t('warehouse.inbound.message.receiveQtyInvalid'), 'warning') + return + } + } + } + receiveSaving.value = true + try { + await confirmWarehouseInboundReceive(withCustomerId({ + inboundNoticeId: notice.id, + boxes: receiveBoxes.value.map((box) => ({ + boxId: box.boxId, + lines: box.lines.map((line) => ({ + lineId: line.lineId, + receivedQty: Number(line.receivedQty) || 0, + binLocationId: line.receivedQty > 0 ? Number(line.binLocationId) : 0, + })), + })), + })) + $baseMessage(t('warehouse.inbound.message.receiveSuccess'), 'success') + closeReceiveModal() + await loadSummary() + await loadList() + } catch (e: any) { + $baseMessage(e?.message || t('warehouse.inbound.message.receiveFailed'), 'error') + } finally { + receiveSaving.value = false + } +} + function handleAdd() { if (!hasPermission('Warehouse.InboundNotice.Save')) return + if (isAdmin.value && ensureMutationCustomerId() == null) return formModel.id = 0 formModel.warehouseId = warehouseId.value formModel.markReference = '' @@ -288,6 +441,7 @@ async function handleSave() { $baseMessage(t('warehouse.inbound.message.required'), 'warning') return } + if (isAdmin.value && ensureMutationCustomerId() == null) return const boxes = boxRows.value .map((box) => ({ boxSeqNo: box.boxSeqNo, @@ -313,14 +467,14 @@ async function handleSave() { saving.value = true try { - await saveWarehouseInbound({ + await saveWarehouseInbound(withCustomerId({ id: formModel.id, warehouseId: formModel.warehouseId, markReference: formModel.markReference, inboundType: formModel.inboundType, remark: formModel.remark, boxes, - }) + })) $baseMessage(t('warehouse.inbound.message.saveSuccess'), 'success') closeModal() await loadSummary() @@ -334,6 +488,7 @@ async function handleSave() { function handleDelete(row: any) { if (!hasPermission('Warehouse.InboundNotice.Delete')) return + if (isAdmin.value && ensureMutationCustomerId() == null) return Modal.confirm({ title: t('warehouse.inbound.actions.delete'), content: row.noticeNo, @@ -347,15 +502,23 @@ function handleDelete(row: any) { } function actionRenderer(row: any) { - const parts = [``] - if (hasPermission('Warehouse.InboundNotice.Delete') && row._status === 'forecasted') { - parts.push(``) + const parts = [renderWarehouseActionBtn('detail', t('warehouse.common.detail'), 'view')] + if ( + (hasPermission('Warehouse.InboundNotice.ConfirmReceive') + || hasPermission('Warehouse.InboundNotice.Save')) + && row._status !== 'completed' + ) { + parts.push(renderWarehouseActionBtn('receive', t('warehouse.inbound.actions.confirmReceive'))) } - return parts.join('') + if (hasPermission('Warehouse.InboundNotice.Delete') && row._status === 'forecasted') { + parts.push(renderWarehouseActionBtn('delete', t('warehouse.common.delete'))) + } + return wrapWarehouseActions(parts) } function handleActionClick(row: any, _i: number, action?: string) { if (action === 'detail') showDetail(row) + if (action === 'receive') openConfirmReceive(row) if (action === 'delete') handleDelete(row) } @@ -385,6 +548,16 @@ onMounted(async () => {
+ { :loading="listLoading" :stretch-columns="true" show-action-column + :action-column-width="120" :action-renderer="actionRenderer" @action-click="handleActionClick" /> @@ -433,11 +607,11 @@ onMounted(async () => {