Files
docs/悦/PDA(海外)/assets/app.js
T
2026-09-08 14:05:30 +08:00

472 lines
160 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
THESIS: A warehouse operator should see the next physical move before the paperwork.
OWN-WORLD: A calm blue work surface, white task sheets, and status colors that read at a glance.
STORY: Sign in, unload a container, consolidate same-destination freight, then move to outbound work.
FIRST VIEWPORT: Workbench metrics lead directly to the three inbound actions; scan controls stay thumb-ready.
FORM: Mobile task board with anchored action bars, chosen to extend the domestic PDA's direct-action grammar.
*/
const LANGS=['zh','en','es'];
const I18N={
zh:{brand:'柏悦物流 PDA',sub:'海外仓移动作业平台',loginTitle:'海外仓作业登录',loginIntro:'登录后处理卸柜、打托、出库和库存查询。',username:'工号',password:'密码',login:'登录',show:'显示',hide:'隐藏',demo:'演示账号',welcome:'欢迎回来',shift:'白班 · 作业中',warehouse:'洛杉矶海外仓',profile:'个人中心',logout:'退出登录',today:'今日',unload:'待卸货集装箱',receivedToday:'今日已入库',outboundToday:'今日待出库',inbound:'入库作业',outbound:'出库作业',internal:'库内作业',plan:'入库计划',planDesc:'按计划拆柜并核对托数',receive:'拆柜收货',receiveDesc:'拍摄 3 张作业照片完成收货',pallet:'打托',palletDesc:'按计划托数装对应货物',pick:'出库拣货',pickDesc:'扫描 SKU 或箱号完成拣货',review:'复核出库',reviewDesc:'核对数量后完成复核',handover:'出库交接',handoverDesc:'确认车辆和货物交接',inventory:'库存查询',inventoryDesc:'查询 SKU、箱号和托盘',stocktake:'库存盘点',transfer:'移库作业',relabel:'换标作业',exception:'异常处理',more:'更多作业',demoOnly:'当前版本仅展示入口',task:'待处理任务',empty:'暂无待处理任务',choose:'选择',details:'详情',status:'状态',customer:'客户',destination:'目的地',destinationCode:'目的地代码',destinationAddress:'目的地地址',ticket:'票号',planNo:'入库计划',container:'集装箱',boxes:'箱',expected:'预计',received:'已收',arrived:'预计到仓',startReceive:'开始拆柜',scanBox:'扫描或输入箱号',scan:'扫码',normal:'正常',short:'短少',damaged:'破损',hold:'待处理',receiveProgress:'收货进度',receivedList:'已收箱明细',goPallet:'进入打托',newPallet:'新建托盘',currentPallet:'当前托盘',palletNo:'托盘号',items:'明细',pieces:'件数',addCargo:'扫描箱号或 SKU 加入托盘',remove:'移除',submitPallet:'提交打托',sameDest:'同目的地可跨票合托',lockedDest:'已锁定目的地',mergeOk:'已加入托盘',mergeBlocked:'该货物目的地不一致,不能合托',duplicate:'该箱号已收货',notFound:'未找到匹配货物',emptyPallet:'托盘还没有货物,不能提交',submitted:'打托已完成',trace:'明细追溯',back:'返回',next:'下一步',complete:'完成',search:'查询',clear:'清空',sku:'SKU',boxNo:'箱号',qty:'数量',operator:'操作员',role:'角色',shiftLabel:'班次',language:'语言',version:'版本',selectPlan:'请选择入库计划',scanContainer:'扫描柜号或输入柜号',unloadTask:'待卸柜任务',pickTask:'待出库任务',startPick:'开始拣货',pickProgress:'拣货进度',picked:'已拣',partial:'部分',skipped:'跳拣',pending:'待处理',confirmPick:'确认拣出',partialPick:'部分拣货',skipPick:'跳拣',undo:'撤销',submitPick:'提交拣货完成',reviewScan:'扫描出库单或 SKU',confirmReview:'完成复核',carrier:'承运商',driver:'司机',vehicle:'车辆',handoverDone:'确认交接',queryHint:'输入 SKU、箱号或托盘号',noResult:'没有找到匹配库存',inventoryStatus:'库存状态',inStock:'在库',inTransit:'在途',newPalletConfirm:'当前托盘尚未提交,确认新建托盘吗?',exitConfirm:'当前作业有未提交内容,确认退出吗?',yes:'确认',cancel:'取消',success:'操作成功',loginError:'工号或密码错误',required:'请输入内容',scanSuccess:'扫码成功',unknownScan:'未找到匹配编码',taskDone:'任务已完成',demoAccount:'演示账号:W001 / 123456',containerMeta:'柜号 {container} · {boxes} 箱',planMeta:'{customer} · {destinationCode}',palletMeta:'{count} 票货物 · {pieces} 件',inventoryMeta:'{customer} · {destinationCode}',unavailable:'暂未开放'},
en:{brand:'Baiyue PDA',sub:'Overseas warehouse operations',loginTitle:'Overseas warehouse sign in',loginIntro:'Unload containers, consolidate pallets, process outbound work and search inventory.',username:'Worker ID',password:'Password',login:'Sign in',show:'Show',hide:'Hide',demo:'Demo account',welcome:'Welcome back',shift:'Day shift · Active',warehouse:'Los Angeles Overseas WH',profile:'Profile',logout:'Sign out',today:'Today',unload:'Containers to unload',receivedToday:'Received today',outboundToday:'Outbound due',inbound:'Inbound',outbound:'Outbound',internal:'Warehouse',plan:'Inbound plans',planDesc:'Unload and reconcile by plan',receive:'Unload & receive',receiveDesc:'Capture 3 operation photos to receive',pallet:'Palletize',palletDesc:'Load freight by planned pallet count',pick:'Outbound picking',pickDesc:'Scan SKU or carton to pick',review:'Outbound review',reviewDesc:'Verify quantity before release',handover:'Outbound handover',handoverDesc:'Confirm vehicle and freight',inventory:'Inventory search',inventoryDesc:'Search SKU, carton or pallet',more:'More operations',demoOnly:'Demo entry only in this version',task:'Open tasks',empty:'No open tasks',choose:'Open',details:'Details',status:'Status',customer:'Customer',destination:'Destination',destinationCode:'Destination code',destinationAddress:'Destination address',ticket:'Ticket',planNo:'Inbound plan',container:'Container',boxes:'Cartons',expected:'Expected',received:'Received',arrived:'ETA',startReceive:'Start unload',scanBox:'Scan or enter carton ID',scan:'Scan',normal:'Normal',short:'Short',damaged:'Damaged',hold:'Hold',receiveProgress:'Receiving progress',receivedList:'Received cartons',goPallet:'Continue to palletize',newPallet:'New pallet',currentPallet:'Current pallet',palletNo:'Pallet ID',items:'Lines',pieces:'Pieces',addCargo:'Scan carton or SKU to add',remove:'Remove',submitPallet:'Submit pallet',sameDest:'Same destination can merge across tickets',lockedDest:'Destination locked',mergeOk:'Added to pallet',mergeBlocked:'Destination mismatch. Cannot merge.',duplicate:'This carton was already received',notFound:'No matching freight found',emptyPallet:'Add freight before submitting the pallet',submitted:'Pallet completed',trace:'Traceable lines',back:'Back',next:'Next',complete:'Complete',search:'Search',clear:'Clear',sku:'SKU',boxNo:'Carton ID',qty:'Qty',operator:'Operator',role:'Role',shiftLabel:'Shift',language:'Language',version:'Version',selectPlan:'Select an inbound plan',scanContainer:'Scan or enter container ID',unloadTask:'Containers to unload',pickTask:'Outbound tasks',startPick:'Start picking',pickProgress:'Picking progress',picked:'Picked',partial:'Partial',skipped:'Skipped',pending:'Pending',confirmPick:'Confirm pick',partialPick:'Partial pick',skipPick:'Skip',undo:'Undo',submitPick:'Submit picking',reviewScan:'Scan order or SKU',confirmReview:'Complete review',carrier:'Carrier',driver:'Driver',vehicle:'Vehicle',handoverDone:'Confirm handover',queryHint:'Enter SKU, carton or pallet ID',noResult:'No matching inventory',inventoryStatus:'Inventory status',inStock:'In stock',inTransit:'In transit',newPalletConfirm:'This pallet is not submitted. Start a new pallet?',exitConfirm:'There are unsaved changes. Exit anyway?',yes:'Confirm',cancel:'Cancel',success:'Action completed',loginError:'Invalid worker ID or password',required:'Enter a value',scanSuccess:'Scan accepted',unknownScan:'No matching code',taskDone:'Task completed',demoAccount:'Demo: W001 / 123456',containerMeta:'{container} · {boxes} cartons',planMeta:'{customer} · {destinationCode}',palletMeta:'{count} tickets · {pieces} pcs',inventoryMeta:'{customer} · {destinationCode}',unavailable:'Not available'},
es:{brand:'PDA Baiyue',sub:'Operaciones de almacén en el extranjero',loginTitle:'Acceso al almacén',loginIntro:'Descarga contenedores, consolida pallets, procesa salidas y consulta inventario.',username:'ID de trabajador',password:'Contraseña',login:'Entrar',show:'Mostrar',hide:'Ocultar',demo:'Cuenta demo',welcome:'Bienvenido',shift:'Turno de día · Activo',warehouse:'Almacén Los Ángeles',profile:'Perfil',logout:'Salir',today:'Hoy',unload:'Contenedores por descargar',receivedToday:'Recibidos hoy',outboundToday:'Salidas pendientes',inbound:'Entrada',outbound:'Salida',internal:'Almacén',plan:'Planes de entrada',planDesc:'Descarga y verifica por plan',receive:'Descarga y recepción',receiveDesc:'Captura 3 fotos para recibir',pallet:'Paletizar',palletDesc:'Carga según los pallets del plan',pick:'Picking de salida',pickDesc:'Escanea SKU o caja',review:'Revisión de salida',reviewDesc:'Verifica cantidades antes de liberar',handover:'Entrega de salida',handoverDesc:'Confirma vehículo y carga',inventory:'Buscar inventario',inventoryDesc:'Busca SKU, caja o pallet',more:'Más operaciones',demoOnly:'Solo demostración en esta versión',task:'Tareas abiertas',empty:'No hay tareas abiertas',choose:'Abrir',details:'Detalles',status:'Estado',customer:'Cliente',destination:'Destino',destinationCode:'Código de destino',destinationAddress:'Dirección de destino',ticket:'Referencia',planNo:'Plan de entrada',container:'Contenedor',boxes:'Cajas',expected:'Previsto',received:'Recibido',arrived:'ETA',startReceive:'Iniciar descarga',scanBox:'Escanea o escribe la caja',scan:'Escanear',normal:'Normal',short:'Faltante',damaged:'Dañada',hold:'En espera',receiveProgress:'Progreso de recepción',receivedList:'Cajas recibidas',goPallet:'Continuar a paletizar',newPallet:'Nuevo pallet',currentPallet:'Pallet actual',palletNo:'ID de pallet',items:'Líneas',pieces:'Unidades',addCargo:'Escanea caja o SKU para añadir',remove:'Quitar',submitPallet:'Enviar pallet',sameDest:'Mismo destino permite consolidar referencias',lockedDest:'Destino bloqueado',mergeOk:'Añadido al pallet',mergeBlocked:'Destino distinto. No se puede consolidar.',duplicate:'Esta caja ya fue recibida',notFound:'No se encontró la carga',emptyPallet:'Añade carga antes de enviar el pallet',submitted:'Pallet completado',trace:'Trazabilidad',back:'Volver',next:'Siguiente',complete:'Completar',search:'Buscar',clear:'Limpiar',sku:'SKU',boxNo:'ID de caja',qty:'Cantidad',operator:'Operador',role:'Rol',shiftLabel:'Turno',language:'Idioma',version:'Versión',selectPlan:'Selecciona un plan de entrada',scanContainer:'Escanea o escribe el contenedor',unloadTask:'Contenedores por descargar',pickTask:'Tareas de salida',startPick:'Iniciar picking',pickProgress:'Progreso de picking',picked:'Recogido',partial:'Parcial',skipped:'Omitido',pending:'Pendiente',confirmPick:'Confirmar picking',partialPick:'Picking parcial',skipPick:'Omitir',undo:'Deshacer',submitPick:'Enviar picking',reviewScan:'Escanea orden o SKU',confirmReview:'Completar revisión',carrier:'Transportista',driver:'Conductor',vehicle:'Vehículo',handoverDone:'Confirmar entrega',queryHint:'Escribe SKU, caja o pallet',noResult:'No hay inventario coincidente',inventoryStatus:'Estado de inventario',inStock:'En stock',inTransit:'En tránsito',newPalletConfirm:'El pallet no está enviado. ¿Crear otro?',exitConfirm:'Hay cambios sin enviar. ¿Salir?',yes:'Confirmar',cancel:'Cancelar',success:'Operación completada',loginError:'ID o contraseña incorrectos',required:'Introduce un valor',scanSuccess:'Escaneo aceptado',unknownScan:'Código no encontrado',taskDone:'Tarea completada',demoAccount:'Demo: W001 / 123456',containerMeta:'{container} · {boxes} cajas',planMeta:'{customer} · {destinationCode}',palletMeta:'{count} referencias · {pieces} uds.',inventoryMeta:'{customer} · {destinationCode}',unavailable:'No disponible'}
};
const users={W001:{pass:'123456',name:'张伟',role:'仓管员',warehouse:'洛杉矶海外仓'},W002:{pass:'123456',name:'李敏',role:'仓库组长',warehouse:'洛杉矶海外仓'},W003:{pass:'123456',name:'王磊',role:'收货员',warehouse:'新泽西海外仓'}};
const plans=[
{id:'INB-260819-018',customer:'杭州积乐贸易',destinationCode:'ONT8',destinationWarehouses:['ONT8','LAX9'],destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',tickets:'TK-88401 / TK-88402',inboundCode:'FBA-ONT8-260819-018',tractorDriver:'Robert Chen',tractorPlate:'CA 8TR-221',expectedPallets:18,receivedPallets:5,eta:'2026-08-19 10:30',status:'待卸货',container:'D2222'},
{id:'INB-260819-021',customer:'深圳璟智供应链',destinationCode:'ONT8',destinationWarehouses:['ONT8','LAX9','SNA6'],destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',tickets:'TK-88416',inboundCode:'FBA-ONT8-260819-021',tractorDriver:'Luis Garcia',tractorPlate:'CA 7LG-333',expectedPallets:12,receivedPallets:0,eta:'2026-08-19 13:00',status:'待卸货',container:'D3333'},
{id:'INB-260819-026',customer:'宁波远航电商',destinationCode:'LGB8',destinationWarehouses:['LGB8'],destinationAddress:'1568 N Linden Ave, Rialto, CA',tickets:'TK-88429',inboundCode:'FBA-LGB8-260819-026',tractorDriver:'Kevin Wu',tractorPlate:'CA 5KW-444',expectedPallets:8,receivedPallets:0,eta:'2026-08-18 16:20',status:'待打托',container:'D4444'}
];
plans.forEach(p=>{p.expectedBoxes=p.expectedPallets;p.receivedBoxes=p.receivedPallets});
const cargo=[
{id:'C-001',planNo:'INB-260819-018',ticketNo:'TK-88401',customer:'杭州积乐贸易',container:'D2222',boxNo:'BX-88401-01',sku:'YQ-CASE-01',name:'Phone case set',qty:120,receivedQty:120,destinationCode:'ONT8',destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',received:true},
{id:'C-002',planNo:'INB-260819-018',ticketNo:'TK-88402',customer:'杭州积乐贸易',container:'D2222',boxNo:'BX-88402-01',sku:'YQ-CABLE-02',name:'USB-C cable',qty:80,receivedQty:80,destinationCode:'ONT8',destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',received:true},
{id:'C-003',planNo:'INB-260819-021',ticketNo:'TK-88416',customer:'深圳璟智供应链',container:'D3333',boxNo:'BX-88416-01',sku:'JZ-LAMP-03',name:'Desk lamp',qty:60,receivedQty:0,destinationCode:'ONT8',destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',received:false},
{id:'C-004',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-01',sku:'NH-BAG-04',name:'Travel bag',qty:32,receivedQty:32,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true},
{id:'C-005',planNo:'INB-260819-021',ticketNo:'TK-88416',customer:'深圳璟智供应链',container:'D3333',boxNo:'BX-88416-02',sku:'JZ-HOLDER-05',name:'Tablet holder',qty:40,receivedQty:0,destinationCode:'ONT8',destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',received:false},
{id:'C-006',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-02',sku:'NH-BAG-06',name:'Packing pouch',qty:48,receivedQty:48,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true}
,{id:'C-007',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-03',sku:'NH-BAG-07',name:'Travel bag accessory',qty:36,receivedQty:36,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true}
,{id:'C-008',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-04',sku:'NH-BAG-08',name:'Packing strap',qty:28,receivedQty:28,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true}
,{id:'C-009',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-05',sku:'NH-BAG-09',name:'Luggage tag',qty:24,receivedQty:24,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true}
,{id:'C-010',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-06',sku:'NH-BAG-10',name:'Carry pouch',qty:42,receivedQty:42,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true}
,{id:'C-011',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-07',sku:'NH-BAG-11',name:'Shoe bag',qty:30,receivedQty:30,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true}
,{id:'C-012',planNo:'INB-260819-026',ticketNo:'TK-88429',customer:'宁波远航电商',container:'D4444',boxNo:'BX-88429-08',sku:'NH-BAG-12',name:'Foldable tote',qty:38,receivedQty:38,destinationCode:'LGB8',destinationAddress:'1568 N Linden Ave, Rialto, CA',received:true}
,{id:'C-013',planNo:'INB-260819-021',ticketNo:'TK-88416',customer:'深圳璟智供应链',container:'D3333',boxNo:'BX-88416-03',sku:'JZ-MOUSE-07',name:'Wireless mouse',qty:90,receivedQty:90,destinationCode:'ONT8',destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',received:true}
];
const pickTasks=[{id:'OUT-260819-004',inboundCode:'FBA-ONT8-260819-004',plateNo:'CA 8KX-217',driver:'Michael Chen',outboundTime:'2026-08-20 09:30',destination:'ONT8',customer:'杭州积乐贸易',boxes:6,palletCount:3,totalWeightLb:203.0,totalPallets:3,orderInfo:'SO-260819-004 / AMZ-PO-88401',items:[{sku:'YQ-CASE-01',qty:45,picked:0},{sku:'YQ-CABLE-02',qty:20,picked:0}]},{id:'OUT-260819-006',inboundCode:'FBA-LGB8-260819-006',plateNo:'CA 6MP-908',driver:'David Lee',outboundTime:'2026-08-20 13:00',destination:'LGB8',customer:'宁波远航电商',boxes:3,palletCount:2,totalWeightLb:99.9,totalPallets:2,orderInfo:'SO-260819-006 / AMZ-PO-88412',items:[{sku:'NH-BAG-04',qty:12,picked:0},{sku:'NH-BAG-06',qty:18,picked:0}]}];
const cartonWeights=[18.5,12.2,9.8,14.6,7.4,16.1,13.8,5.6,3.2,11.7,4.8,10.9,8.3];
cargo.forEach((item,index)=>{item.weight=cartonWeights[index]||0});
const inventory=[...cargo.map((x,i)=>({sku:x.sku,name:x.name,customer:x.customer,boxNo:x.boxNo,palletNo:i<2?'D2222-TC-001':'—',destinationCode:x.destinationCode,qty:x.receivedQty,status:x.receivedQty?'在库':'在途'})),{sku:'JZ-MOUSE-07',name:'Wireless mouse',customer:'深圳璟智供应链',boxNo:'BX-88416-03',palletNo:'—',destinationCode:'ONT8',qty:90,status:'在途'}];
const packingCartons=[
{sku:'YQ-CASE-01',boxNo:'BX-PACK-001',unitsPerCarton:120,cartons:1},
{sku:'YQ-CABLE-02',boxNo:'BX-PACK-002',unitsPerCarton:80,cartons:1},
{sku:'YQ-HOLDER-03',boxNo:'BX-PACK-003',unitsPerCarton:60,cartons:1}
];
const demoPalletizedPallets=[{id:'D2222-TC-001',mode:'palletize',planId:'INB-260819-018',targetPallets:18,container:'D2222',destinationCode:'ONT8',destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',items:[{...cargo[0],palletNo:'D2222-TC-001'},{...cargo[1],palletNo:'D2222-TC-001'}]}];
cargo[0].palletNo='D2222-TC-001';cargo[1].palletNo='D2222-TC-001';
const demoConsolidatedPallets=[{id:'D2222-TC-019',mode:'consolidate',planId:null,targetPallets:null,container:'D2222',destinationCode:'ONT8',destinationAddress:'24300 Nandina Ave, Moreno Valley, CA',items:[{...cargo[0],isMerged:false,mergeFlag:'否'},{...cargo[1],isMerged:true,mergeFlag:'是'}]}];
const WORK_LABELS={zh:{inboundPallet:'入库打托',inboundPalletDesc:'按入库计划托数完成打托',pallet:'入库打托',palletDesc:'按计划托数装对应货物',palletList:'托盘列表',palletTotal:'托盘数',palletBoxSummary:'托盘数/箱数',palletListDesc:'选择托盘进入详情',pendingPallet:'待创建托盘',cartonCount:'箱数',boxMark:'箱唛',productName:'产品名称',palletStatus:'托盘状态',idle:'闲置',occupied:'占用',viewPalletDetails:'查看明细',directEnter:'直接进入',selectPalletLabel:'选择托盘标',palletLabel:'托盘标',scanBoxSku:'扫描箱唛/SKU',scanPalletLabel:'扫描托盘标',unknownPalletScan:'未找到匹配托盘',palletCompleted:'已完成',palletPending:'未完成',palletizeAction:'卸货打托',planDesc:'按入库计划选择打托任务',mergeOnly:'合托',mergeDesc:'将不足一托的箱子合成一托',startPallet:'开始打托',palletProgress:'打托进度',palletTargetReached:'已达到计划托数,不能继续提交'},en:{inboundPallet:'Inbound palletizing',inboundPalletDesc:'Palletize against the inbound plan',pallet:'Inbound palletizing',palletDesc:'Load freight by planned pallet count',palletList:'Pallet list',palletTotal:'Pallets',palletBoxSummary:'Pallets/Cartons',palletListDesc:'Select a pallet to view details',pendingPallet:'Pallet to create',cartonCount:'Cartons',boxMark:'Carton mark',productName:'Product name',palletStatus:'Pallet status',idle:'Idle',occupied:'Occupied',viewPalletDetails:'View pallet details',directEnter:'Enter directly',selectPalletLabel:'Select pallet label',palletLabel:'Pallet label',scanBoxSku:'Scan carton mark/SKU',scanPalletLabel:'Scan pallet label',unknownPalletScan:'No matching pallet found',palletCompleted:'Completed',palletPending:'Incomplete',palletizeAction:'Palletize',planDesc:'Choose an inbound plan to palletize',mergeOnly:'Consolidate',mergeDesc:'Combine cartons that do not fill a pallet',startPallet:'Start palletizing',palletProgress:'Palletizing progress',palletTargetReached:'Planned pallet count reached'},es:{inboundPallet:'Paletizado de entrada',inboundPalletDesc:'Paletiza según el plan de entrada',pallet:'Paletizado de entrada',palletDesc:'Carga según los pallets del plan',palletList:'Lista de pallets',palletTotal:'Pallets',palletBoxSummary:'Pallets/Cajas',palletListDesc:'Selecciona un pallet para ver detalles',pendingPallet:'Pallet por crear',cartonCount:'Cajas',boxMark:'Marca de caja',productName:'Nombre del producto',palletStatus:'Estado del pallet',idle:'Libre',occupied:'Ocupado',viewPalletDetails:'Ver detalles de pallets',directEnter:'Entrar directamente',selectPalletLabel:'Seleccionar etiqueta de pallet',palletLabel:'Etiqueta de pallet',scanBoxSku:'Escanear marca de caja/SKU',scanPalletLabel:'Escanear etiqueta de pallet',unknownPalletScan:'No se encontró el pallet',palletCompleted:'Completado',palletPending:'Incompleto',palletizeAction:'Paletizar',planDesc:'Elige un plan de entrada para paletizar',mergeOnly:'Consolidar',mergeDesc:'Combina cajas que no llenan un pallet',startPallet:'Iniciar paletizado',palletProgress:'Progreso de paletizado',palletTargetReached:'Se alcanzó el total de pallets del plan'}};
const expressOrders=[
{id:'PO-260819-001',type:'purchase',customerCode:'HZJL',boxMark:'BX-EXP-001',sku:'YQ-CASE-01',cartons:4,pieces:120,carrier:'FedEx',trackingNo:'7845 2210 9931',signedIn:false},
{id:'PO-260819-002',type:'purchase',customerCode:'SZJZ',boxMark:'BX-EXP-002',sku:'JZ-LAMP-03',cartons:2,pieces:60,carrier:'UPS',trackingNo:'1Z84A72E0391',signedIn:false},
{id:'RT-260819-001',type:'return',customerCode:'NBHY',boxMark:'BX-RET-001',sku:'NH-BAG-04',cartons:1,pieces:32,carrier:'USPS',trackingNo:'9400 1118 8821',signedIn:false},
{id:'RT-260819-002',type:'return',customerCode:'HZJL',boxMark:'BX-RET-002',sku:'YQ-CABLE-02',cartons:2,pieces:80,carrier:'DHL',trackingNo:'JD0146000068',signedIn:false}
];
const state={lang:localStorage.getItem('overseas-pda-lang')||'zh',page:'login',user:null,error:'',selectedPlanId:null,receiveScan:[],receiveStatus:{},unloadPhotos:{},pallet:null,palletMode:'palletize',palletOrderType:'fba',fbaPalletLabels:[],dropshipPalletLabel:'',palletTypeMessage:null,fbaPalletMessage:null,dropshipScanMessage:null,pallets:[...demoPalletizedPallets,...demoConsolidatedPallets].map(pallet=>({...pallet,items:pallet.items.map(item=>({...item}))})),palletMessage:null,palletScanQty:'1',palletTopMessage:null,palletPhotos:{},photoPalletId:'',pickTaskId:null,pickMessage:null,reviewScanned:[],handoverDone:false,handoverPhotos:{},inventoryQuery:'',packingMessage:null,packingCustomerCode:'',packingCustomerError:'',packingInboundDone:false,relabelMessage:null,relabelDoneOrders:{},selectedRelabelId:null,relabelPhotos:{},relabelMode:'box',relabelScans:{box:{},sku:{}},exceptionRefValue:'',exceptionMessage:null,expressOrderType:'purchase',selectedExpressOrderId:null,expressScan:null,expressMessage:null,expressWaybillPhotos:{}};
const EXTRA_LABELS={zh:{planDesc:'按计划核对托数并打托',startPallet:'开始打托',mergeOnly:'合托',receiveDesc:'拍摄 3 张作业照片完成收货',receiveListTitle:'待卸货集装箱',receiveListDesc:'选择集装箱后拍摄 3 张照片完成收货',pendingContainers:'个待卸货集装箱',startUnload:'开始拆柜',stocktake:'库存盘点',transfer:'移库作业',relabel:'换标作业',exception:'异常处理',palletCount:'托数',palletQty:'托盘数量',linkedOrder:'关联订单',licensePlate:'车牌照片',containerSeal:'集装箱封签',doorOpen:'开柜照片',unloading:'卸货照片',unloadComplete:'卸货完成照片',takePhoto:'拍照',retake:'重拍',photoProgress:'拍照进度',confirmUnload:'确认拆柜收货',photosRequired:'请完成全部 3 张照片',handoverPhotosRequired:'请完成全部 7 张交接照片',mergeFlag:'是否合托',yesMerge:'是',noMerge:'否',licensePlatePhoto:'车牌照片',vehicleCabPhoto:'车厢照片',driverLicense:'司机驾照',loadingSealPhoto:'装货封门照片',lockSealPhoto:'锁门 SEAL 照片',bolDriverSigned:'BOL(含司机签字)',amazonAppointment:'亚马逊预约单',handoverPhotos:'交接照片(7张)',relabelTitle:'换标作业',relabelHint:'订单列表中提交换标完成',newLabel:'新标签 / 新 SKU',submitRelabel:'换标完成',relabelDone:'换标已完成',relabelStatus:'换标状态',relabelPhotoRequired:'请先完成换标拍照',exceptionTitle:'异常处理',exceptionType:'异常类型',exceptionRef:'关联尾程订单号 / 入仓码 / 托盘号',exceptionDesc:'异常描述',submitException:'提交异常',exceptionDone:'异常已记录',selectException:'请选择异常类型',queryHint:'输入托盘号或产品名称',inventoryDesc:'查询产品名称或托盘号'},en:{planDesc:'Verify pallet count and start palletizing',startPallet:'Start palletizing',mergeOnly:'Consolidate',receiveDesc:'Capture 3 operation photos to receive',receiveListTitle:'Containers to unload',receiveListDesc:'Choose a container, then capture 3 photos to receive it',pendingContainers:'containers to unload',startUnload:'Start unload',stocktake:'Stocktake',transfer:'Stock transfer',relabel:'Relabeling',exception:'Exceptions',palletCount:'Pallets',palletQty:'Pallet quantity',linkedOrder:'Linked order',licensePlate:'License plate photo',containerSeal:'Container seal',doorOpen:'Door open',unloading:'Unloading',unloadComplete:'Unload complete',takePhoto:'Take photo',retake:'Retake',photoProgress:'Photo progress',confirmUnload:'Confirm unloading',photosRequired:'Complete all 3 photos',handoverPhotosRequired:'Complete all 7 handover photos',mergeFlag:'Merged pallet',yesMerge:'Yes',noMerge:'No',licensePlatePhoto:'License plate photo',vehicleCabPhoto:'Vehicle cab photo',driverLicense:'Driver license',loadingSealPhoto:'Loading door photo',lockSealPhoto:'Lock SEAL photo',bolDriverSigned:'BOL with driver signature',amazonAppointment:'Amazon appointment',handoverPhotos:'Handover photos (7)',relabelTitle:'Relabeling',relabelHint:'Submit completion from the order list',newLabel:'New label / new SKU',submitRelabel:'Relabel complete',relabelDone:'Relabel completed',relabelStatus:'Relabel status',relabelPhotoRequired:'Take a relabel photo before completing',exceptionTitle:'Exception handling',exceptionType:'Exception type',exceptionRef:'Outbound order / inbound code / pallet ID',exceptionDesc:'Exception description',submitException:'Log exception',exceptionDone:'Exception recorded',selectException:'Select an exception type',queryHint:'Enter pallet ID or product name',inventoryDesc:'Search product name or pallet ID'},es:{planDesc:'Verifica pallets e inicia paletizado',startPallet:'Iniciar paletizado',mergeOnly:'Consolidar',receiveDesc:'Captura 3 fotos para recibir',receiveListTitle:'Contenedores por descargar',receiveListDesc:'Elige un contenedor y captura 3 fotos para recibirlo',pendingContainers:'contenedores por descargar',startUnload:'Iniciar descarga',stocktake:'Inventario físico',transfer:'Traslado interno',relabel:'Reetiquetado',exception:'Excepciones',palletCount:'Pallets',palletQty:'Cantidad de pallets',linkedOrder:'Pedido relacionado',licensePlate:'Matrícula',containerSeal:'Sello del contenedor',doorOpen:'Apertura',unloading:'Descarga',unloadComplete:'Descarga terminada',takePhoto:'Tomar foto',retake:'Repetir',photoProgress:'Progreso de fotos',confirmUnload:'Confirmar descarga',photosRequired:'Completa las 3 fotos',handoverPhotosRequired:'Completa las 7 fotos de entrega',mergeFlag:'Pallet consolidado',yesMerge:'Sí',noMerge:'No',licensePlatePhoto:'Foto de matrícula',vehicleCabPhoto:'Foto de cabina',driverLicense:'Licencia del conductor',loadingSealPhoto:'Puerta de carga',lockSealPhoto:'Foto del SEAL',bolDriverSigned:'BOL firmado por conductor',amazonAppointment:'Cita de Amazon',handoverPhotos:'Fotos de entrega (7)',relabelTitle:'Reetiquetado',relabelHint:'Envía la finalización desde la lista de pedidos',newLabel:'Nueva etiqueta / SKU',submitRelabel:'Reetiquetado completo',relabelDone:'Reetiquetado completado',relabelStatus:'Estado del reetiquetado',relabelPhotoRequired:'Toma una foto del reetiquetado antes de completar',exceptionTitle:'Gestión de excepciones',exceptionType:'Tipo de excepción',exceptionRef:'Orden de salida / código de entrada / ID de pallet',exceptionDesc:'Descripción de la excepción',submitException:'Registrar excepción',exceptionDone:'Excepción registrada',selectException:'Selecciona un tipo',queryHint:'Escribe ID de pallet o nombre del producto',inventoryDesc:'Busca nombre del producto o ID de pallet'}};
Object.assign(EXTRA_LABELS.zh,{expressSignIn:'快递签收',expressSignInDesc:'签收采购或退货快递订单',purchase:'采购',return:'退货',customerCode:'客户简码',cartonPiece:'箱数/件数',trackingNo:'快递号',expressSign:'签收',expressSigned:'已签收'});
Object.assign(EXTRA_LABELS.en,{expressSignIn:'Express receiving',expressSignInDesc:'Receive purchase or return parcels',purchase:'Purchase',return:'Return',customerCode:'Customer code',cartonPiece:'Cartons/Pieces',trackingNo:'Tracking no.',expressSign:'Sign in',expressSigned:'Received'});
Object.assign(EXTRA_LABELS.es,{expressSignIn:'Recepción de paquetería',expressSignInDesc:'Recibe paquetes de compras o devoluciones',purchase:'Compra',return:'Devolución',customerCode:'Código de cliente',cartonPiece:'Cajas/Unidades',trackingNo:'N.º de seguimiento',expressSign:'Recibir',expressSigned:'Recibido'});
Object.assign(EXTRA_LABELS.zh,{expressScanHint:'扫描箱唛或SKU',cartonQuantity:'箱数',pieceQuantity:'个数',photoInfo:'照片信息',waybill:'面单',expressComplete:'签收完成',expressScanRequired:'请先扫描箱唛或SKU',expressPhotoRequired:'请拍摄面单照片',expressQuantityRequired:'请输入有效数量'});
Object.assign(EXTRA_LABELS.en,{expressScanHint:'Scan carton mark or SKU',cartonQuantity:'Carton count',pieceQuantity:'Unit count',photoInfo:'Photo information',waybill:'Waybill',expressComplete:'Complete receiving',expressScanRequired:'Scan a carton mark or SKU first',expressPhotoRequired:'Take a waybill photo',expressQuantityRequired:'Enter a valid quantity'});
Object.assign(EXTRA_LABELS.es,{expressScanHint:'Escanea marca de caja o SKU',cartonQuantity:'Cantidad de cajas',pieceQuantity:'Cantidad de unidades',photoInfo:'Información fotográfica',waybill:'Etiqueta de envío',expressComplete:'Completar recepción',expressScanRequired:'Escanea una caja o SKU primero',expressPhotoRequired:'Toma una foto de la etiqueta',expressQuantityRequired:'Introduce una cantidad válida'});
const PICK_LABELS={zh:{outboundOrderNo:'出库单号',inboundCode:'入仓码',plateNo:'车牌号',outboundTime:'出库时间',totalPalletBoxes:'总托数 / 总箱数',totalBoxes:'总箱数',totalWeight:'总重量',orderList:'订单列表',tractorDriver:'拖车司机',tractorPlate:'拖车车牌',destinationWarehouse:'目的仓编码'},en:{outboundOrderNo:'Outbound order',inboundCode:'Inbound code',plateNo:'Plate number',outboundTime:'Outbound time',totalPalletBoxes:'Total pallets / cartons',totalBoxes:'Total cartons',totalWeight:'Total weight',orderList:'Order list',tractorDriver:'Tractor driver',tractorPlate:'Tractor plate',destinationWarehouse:'Destination warehouse'},es:{outboundOrderNo:'Orden de salida',inboundCode:'Código de entrada',plateNo:'Matrícula',outboundTime:'Hora de salida',totalPalletBoxes:'Pallets / cajas totales',totalBoxes:'Total de cajas',totalWeight:'Peso total',orderList:'Lista de pedidos',tractorDriver:'Conductor del tractor',tractorPlate:'Matrícula del tractor',destinationWarehouse:'Almacén de destino'}};
Object.assign(EXTRA_LABELS.zh,{packing:'拣货装箱',packingHint:'扫描箱号或 SKU 完成拣货装箱',packingScanHint:'扫描箱号或 SKU',customerInfo:'客户信息',customerCode:'客户简码',customerCodeHint:'请输入客户简码',customerCodeRequired:'请输入客户简码',cartonDetails:'箱子详情',unitsPerCarton:'每箱数量',backToOrderList:'返回订单列表',confirmInbound:'确认入库',packingInboundDone:'拣货装箱已入库'});
Object.assign(EXTRA_LABELS.en,{packing:'Pick & pack',packingHint:'Scan a carton or SKU to pick and pack',packingScanHint:'Scan carton ID or SKU',customerInfo:'Customer information',customerCode:'Customer code',customerCodeHint:'Enter customer code',customerCodeRequired:'Enter a customer code',cartonDetails:'Carton details',unitsPerCarton:'Units per carton',backToOrderList:'Back to order list',confirmInbound:'Confirm inbound',packingInboundDone:'Pick and pack received'});
Object.assign(EXTRA_LABELS.es,{packing:'Picking y embalaje',packingHint:'Escanea una caja o SKU para preparar',packingScanHint:'Escanea caja o SKU',customerInfo:'Información del cliente',customerCode:'Código de cliente',customerCodeHint:'Introduce el código del cliente',customerCodeRequired:'Introduce el código del cliente',cartonDetails:'Detalles de cajas',unitsPerCarton:'Unidades por caja',backToOrderList:'Volver a pedidos',confirmInbound:'Confirmar entrada',packingInboundDone:'Picking y embalaje recibido'});
state.pickDetailsCollapsed=false;
const SECTION_FILES={
home:'index.html',
receive:'receive.html',
pallet:'pallet.html',
'express-signin':'express-signin.html',
consolidation:'consolidation.html',
pick:'pick.html',
handover:'handover.html',
packing:'packing.html',
relabel:'relabel.html',
exception:'exception.html'
};
const SECTION_LANDING_PAGES={home:'workbench',receive:'receive-list',pallet:'plans','express-signin':'express-signin',consolidation:'pallet-list',pick:'pick',handover:'handover',packing:'packing',relabel:'relabel',exception:'exception'};
const SECTION_PAGE_GROUPS={
home:['login','workbench','profile','inventory'],
receive:['receive-list','receive'],
pallet:['plans','pallet-list','pallet','photo-detail'],
'express-signin':['express-signin','express-signin-detail'],
consolidation:['pallet-list','pallet','photo-detail'],
pick:['pick','review'],
handover:['handover'],
packing:['packing'],
relabel:['relabel','relabel-photo','relabel-box','relabel-sku'],
exception:['exception']
};
const APP_SESSION_KEY='overseas-pda-session';
function currentSection(){return document.body?.dataset.section||'home';}
function currentHtmlFile(){const file=window.location?.pathname?.split('/').pop();return file||'index.html';}
function sectionForPage(page){
if(['pallet-list','pallet','photo-detail'].includes(page))return state.palletMode==='consolidate'?'consolidation':'pallet';
return Object.keys(SECTION_PAGE_GROUPS).find(section=>SECTION_PAGE_GROUPS[section].includes(page))||'home';
}
function fileForPage(page){return SECTION_FILES[sectionForPage(page)]||SECTION_FILES.home;}
function persistAppSession(){
localStorage.setItem(APP_SESSION_KEY,JSON.stringify({state,plans,cargo,pickTasks,expressOrders}));
}
function restoreAppSession(){
const raw=localStorage.getItem(APP_SESSION_KEY);
if(!raw)return;
try{
const saved=JSON.parse(raw);
if(saved.state&&typeof saved.state==='object')Object.assign(state,saved.state);
if(Array.isArray(saved.plans))plans.splice(0,plans.length,...saved.plans);
if(Array.isArray(saved.cargo))cargo.splice(0,cargo.length,...saved.cargo);
if(Array.isArray(saved.pickTasks))pickTasks.splice(0,pickTasks.length,...saved.pickTasks);
if(Array.isArray(saved.expressOrders))expressOrders.splice(0,expressOrders.length,...saved.expressOrders);
}catch(error){localStorage.removeItem?.(APP_SESSION_KEY);}
}
function initializeSectionPage(){
const section=currentSection();
if(!state.user){state.page='login';return;}
if(!SECTION_PAGE_GROUPS[section]?.includes(state.page))state.page=SECTION_LANDING_PAGES[section]||'workbench';
if(section==='consolidation')state.palletMode='consolidate';
if(section==='pallet'&&state.palletMode==='consolidate')state.palletMode='palletize';
}
function redirectForCurrentPage(){
if(state.page==='login')return false;
const target=fileForPage(state.page);
if(currentHtmlFile()===target)return false;
persistAppSession();
window.location.href=target;
return true;
}
function t(key,vars={}){let value=WORK_LABELS[state.lang]?.[key]||EXTRA_LABELS[state.lang]?.[key]||PICK_LABELS[state.lang]?.[key]||(I18N[state.lang]&&I18N[state.lang][key])||WORK_LABELS.zh[key]||EXTRA_LABELS.zh[key]||PICK_LABELS.zh[key]||I18N.zh[key]||key;return String(value).replace(/\{(\w+)\}/g,(_,k)=>vars[k]??'');}
function esc(value){return String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
const ICON_PATHS={
pallet:'<path class="icon-fill" d="m4.5 8.5 7.5-4 7.5 4-7.5 4-7.5-4Z"/><path d="m4.5 8.5 7.5-4 7.5 4-7.5 4-7.5-4Z"/><path d="M4.5 8.5v7l7.5 4 7.5-4v-7M12 12.5v7M3.5 21h17"/>',
unload:'<path class="icon-fill" d="M3.5 4.5h17v11h-17z"/><rect x="3.5" y="4.5" width="17" height="11" rx="2"/><path d="M8 4.5v11m8-11v11M6.5 8h3M14.5 8h3M12 14v7m-3-3 3 3 3-3"/>',
merge:'<rect class="icon-fill" x="7.5" y="15.5" width="9" height="5" rx="1.25"/><rect x="3" y="3.5" width="6.5" height="5" rx="1.25"/><rect x="14.5" y="3.5" width="6.5" height="5" rx="1.25"/><rect x="7.5" y="15.5" width="9" height="5" rx="1.25"/><path d="M6.25 8.5v1.75A2.75 2.75 0 0 0 9 13h6a2.75 2.75 0 0 0 2.75-2.75V8.5M12 13v2.5"/>',
pick:'<path class="icon-fill" d="M5 7.5h13l-1.5 8h-10z"/><path d="M3 4h1.5L6.5 16h10l1.5-8.5H5.1M8.5 20h.01M15 20h.01"/><path d="m9.5 11.5 1.6 1.6 3.4-3.4"/>',
review:'<rect x="5" y="3" width="14" height="18" rx="2"/><path d="M9 3.5h6"/><path d="m8.5 12 2 2 5-5"/><path d="M8.5 8h2"/>',
handover:'<path class="icon-fill" d="M3 6h11v10H3zM14 9h4l3 3v4h-7z"/><path d="M3 6h11v10H3V6Zm11 3h4l3 3v4h-7V9ZM6.5 19h.01M17.5 19h.01"/><path d="m6.5 11 1.5 1.5 3-3"/>',
inventory:'<path class="icon-fill" d="m3.5 6.5 6.5-3 6.5 3-6.5 3-6.5-3Z"/><path d="m3.5 6.5 6.5-3 6.5 3-6.5 3-6.5-3Zm0 0v7l6.5 3 3-1.4M10 9.5v7"/><circle cx="16.5" cy="16.5" r="3.5"/><path d="m19.25 19.25 2.25 2.25"/>',
packing:'<path class="icon-fill" d="m4 7 8-4 8 4-8 4-8-4Z"/><path d="m4 7 8-4 8 4-8 4-8-4Zm0 0v10l8 4 8-4V7M12 11v10M8 5l8 4"/>',
relabel:'<path class="icon-fill" d="m3.5 6.5 7.5-3 9.5 9.5-7.5 7.5L3.5 11z"/><path d="m3.5 6.5 7.5-3 9.5 9.5-7.5 7.5L3.5 11V6.5Z"/><circle cx="8" cy="8" r="1.25"/><path d="M12.5 9.5h4m0 0-1.5-1.5m1.5 1.5L15 11m-3.5 4h4m-4 0 1.5-1.5M11.5 15l1.5 1.5"/>',
exception:'<path class="icon-fill" d="M12 3 21 20H3L12 3Z"/><path d="M12 3 21 20H3L12 3Z"/><path d="M12 9v4.5M12 17h.01"/>',
more:'<rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/><rect x="4" y="14" width="6" height="6" rx="1"/><rect x="14" y="14" width="6" height="6" rx="1"/>',
scan:'<path d="M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3"/><path d="M7 9h10v6H7z"/><path d="M10 9v6M13 9v6"/>',
arrowLeft:'<path d="M19 12H5"/><path d="m11 6-6 6 6 6"/>',
camera:'<path d="M4 7h3l1.5-2h7L17 7h3v12H4V7Z"/><circle cx="12" cy="13" r="3.5"/>',
check:'<path d="m5 12 4 4L19 6"/>'
};
ICON_PATHS.express='<path class="icon-fill" d="M4 7h16v10H4z"/><path d="M4 7h16v10H4V7Zm0 0 8 5 8-5M8 4h8"/>';
function icon(name,label=''){const path=ICON_PATHS[name]||ICON_PATHS.more;return `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"${label?` aria-label="${esc(label)}"`:''}>${path}</svg>`;}
function setLang(lang){if(!LANGS.includes(lang))return;state.lang=lang;localStorage.setItem('overseas-pda-lang',lang);render();}
function langButtons(){return `<div class="lang-pill" role="group" aria-label="${t('language')}">${[['zh','中'],['en','EN'],['es','ES']].map(([v,l])=>`<button type="button" class="${state.lang===v?'active':''}" aria-pressed="${state.lang===v}" onclick="setLang('${v}')">${l}</button>`).join('')}</div>`;}
function header(title,sub='',back='workbench'){return `<header class="topbar"><div class="topbar-row"><div class="page-heading">${back?`<button type="button" class="back-btn" onclick="navigate('${back}')" aria-label="${t('back')}">${icon('arrowLeft')}</button>`:''}<div><h1>${esc(compactPageTitle(title))}</h1>${sub?`<p>${esc(sub)}</p>`:''}</div></div><div class="top-actions">${langButtons()}<button type="button" class="avatar" onclick="navigate('profile')" aria-label="${t('profile')}">${esc((state.user?.name||'张').slice(0,1))}</button></div></div></header>`;}
function layout(body,title='',sub='',back='workbench'){const resolvedBack=state.page==='pallet'?(state.palletMode==='consolidate'?'pallet-list':'plans'):back;return `${state.page==='login'?'':header(title,sub,resolvedBack)}<main class="content ${state.page==='login'?'narrow':''}">${body}</main>`;}
function loginView(){return `<div class="login-wrap"><section class="login-card"><div class="brand"><div class="brand-mark">P</div><div><div class="brand-title">${t('brand')}</div><div class="brand-sub">${t('sub')}</div></div></div><div style="display:flex;justify-content:flex-end;margin-top:-16px;margin-bottom:14px">${langButtons()}</div><h1>${t('loginTitle')}</h1><p class="intro">${t('loginIntro')}</p><div class="field"><label for="login-user">${t('username')}</label><input id="login-user" autocomplete="username" placeholder="W001" aria-describedby="login-error"></div><div class="field"><label for="login-pass">${t('password')}</label><div class="password-wrap"><input id="login-pass" type="password" autocomplete="current-password" placeholder="123456" aria-describedby="login-error"><button onclick="togglePassword()" type="button" id="password-toggle">${t('show')}</button></div></div><div class="login-error" id="login-error" role="alert" aria-live="polite">${esc(state.error)}</div><button type="button" class="login-btn" onclick="login()">${t('login')}</button><div class="demo-row"><button type="button" onclick="quickLogin('W001')">W001 · ${esc(users.W001.name)}</button><button type="button" onclick="quickLogin('W002')">W002 · ${esc(users.W002.name)}</button></div><div class="demo-tip">${t('demoAccount')}</div></section></div>`;}
function togglePassword(){const input=document.getElementById('login-pass');const button=document.getElementById('password-toggle');if(!input||!button)return;const show=input.type==='password';input.type=show?'text':'password';button.textContent=t(show?'hide':'show');}
function login(){const user=(document.getElementById('login-user')?.value||'').trim();const pass=document.getElementById('login-pass')?.value||'';if(!user||!pass){state.error=t('required');render();return}if(!users[user]||users[user].pass!==pass){state.error=t('loginError');render();return}state.user={id:user,...users[user]};state.error='';state.page=SECTION_LANDING_PAGES[currentSection()]||'workbench';render();toast(t('success'),'good');}
function quickLogin(id){state.user={id,...users[id]};state.error='';state.page=SECTION_LANDING_PAGES[currentSection()]||'workbench';render();toast(t('success'),'good');}
function logout(){state.user=null;state.page='login';state.pallet=null;localStorage.removeItem?.(APP_SESSION_KEY);if(currentHtmlFile()!==SECTION_FILES.home){window.location.href=SECTION_FILES.home;return}render();}
function navigate(page){if(page==='pallet'&&state.palletMode!=='consolidate')state.palletMode='palletize';if(state.page==='pallet'&&state.pallet&&state.pallet.items.length&&page==='workbench'){confirmExit();return}state.page=page;render();}
function confirmExit(){showModal(t('exitConfirm'),()=>{state.pallet=null;state.page='workbench';closeModal();render();});}
function workbenchView(){const unload=plans.filter(p=>p.status==='待卸货').length;const received=plans.reduce((sum,p)=>sum+p.receivedBoxes,0);const outbound=pickTasks.filter(t=>!t.done).length;return layout(`<section class="welcome"><div><div class="welcome-kicker">${t('welcome')}</div><div class="welcome-name">${esc(state.user?.name||'')}</div><div class="welcome-meta">${esc(state.user?.role||'')} · ${esc(state.user?.warehouse||t('warehouse'))}</div></div><div class="shift">${t('shift')}</div></section><section class="metrics"><div class="metric warn"><div class="metric-label">${t('unload')}</div><div class="metric-value">${unload}</div></div><div class="metric good"><div class="metric-label">${t('receivedToday')}</div><div class="metric-value">${received}</div></div><div class="metric blue"><div class="metric-label">${t('outboundToday')}</div><div class="metric-value">${outbound}</div></div></section><div class="section-title"><h2>${t('inbound')}</h2><span>${t('task')}</span></div><div class="entry-grid inbound-entry-grid"><button class="entry receive-entry" onclick="navigate('receive-list')"><div class="entry-icon">${icon('unload')}</div><h3>${t('receive')}</h3><p>${t('receiveDesc')}</p><span class="badge">${unload}</span></button><button class="entry pallet-entry" onclick="navigate('plans')"><div class="entry-icon">${icon('pallet')}</div><h3>${t('inboundPallet')}</h3><p>${t('inboundPalletDesc')}</p><span class="badge">${plans.filter(p=>p.status==='待打托').length}</span></button><button class="entry express-entry" onclick="navigate('express-signin')"><div class="entry-icon">${icon('express')}</div><h3>${t('expressSignIn')}</h3><p>${t('expressSignInDesc')}</p></button></div><div class="section-title"><h2>${t('outbound')}</h2><span>${t('pickTask')}</span></div><div class="entry-grid outbound-entry-grid"><button class="entry merge-entry" onclick="startConsolidation()"><div class="entry-icon">${icon('merge')}</div><h3>${t('mergeOnly')}</h3><p>${t('mergeDesc')}</p></button><button class="entry pick-entry" onclick="navigate('pick')"><div class="entry-icon">${icon('pick')}</div><h3>${t('pick')}</h3><p>${t('pickDesc')}</p><span class="badge">${outbound}</span></button><button class="entry handover-entry" onclick="navigate('handover')"><div class="entry-icon">${icon('handover')}</div><h3>${t('handover')}</h3><p>${t('handoverDesc')}</p></button></div><div class="section-title"><h2>${t('internal')}</h2><span>${t('inventory')}</span></div><div class="entry-grid"></div><div class="task-strip"><div class="section-title" style="margin-top:0"><h2>${t('task')}</h2><span>${t('unloadTask')}</span></div>${plans.filter(p=>p.status==='待卸货').slice(0,2).map(p=>`<div class="task-row"><span class="task-dot"></span><div class="task-main"><strong>${esc(p.container)}</strong><p>${esc(p.customer)} · ${esc(p.destinationCode)} · ${p.expectedPallets} ${t('palletCount')}</p></div><button onclick="openPlan('${p.id}')">${t('choose')}</button></div>`).join('')||`<div class="empty">${t('empty')}</div>`}</div>`,t('brand'),t('sub'),null);}
function openPlan(id){state.selectedPlanId=id;state.page='receive';render();}
function receiveListView(){const pending=plans.filter(p=>p.status==='待卸货');return layout(`<div class="toolbar"><span class="subtle">${pending.length} ${t('pendingContainers')}</span></div><div class="list">${pending.map(p=>`<article class="list-card"><div class="list-top"><div><div class="list-title">${esc(p.container)}</div><div class="list-meta">${esc(p.id)}</div></div><span class="status wait">${t('unload')}</span></div><div class="list-grid"><div><span>${t('destinationWarehouse')}</span><strong>${(p.destinationWarehouses||[p.destinationCode]).slice(0,3).map(esc).join(' / ')}</strong></div><div><span>${t('receivePalletBoxes')}</span><strong>${p.expectedPallets} ${t('palletUnit')} / ${p.expectedBoxes} ${t('boxes')}</strong></div><div><span>${t('arrived')}</span><strong>${esc(p.eta.slice(5))}</strong></div><div><span>${t('tractorDriver')}</span><strong>${esc(p.tractorDriver)}</strong></div><div><span>${t('tractorPlate')}</span><strong>${esc(p.tractorPlate)}</strong></div></div><div class="card-actions"><button class="btn primary" onclick="openPlan('${p.id}')">${t('startUnload')}</button></div></article>`).join('')||`<div class="empty">${t('empty')}</div>`}</div>`,t('receiveListTitle'),t('receiveListDesc'),'workbench');}
function plansView(){return layout(`<div class="toolbar"><span class="subtle">${plans.length} ${t('plan')}</span></div><div class="list">${plans.map(p=>`<article class="list-card"><div class="list-top"><div><div class="list-title">${esc(p.id)}</div><div class="list-meta">${esc(p.customer)} · ${esc(p.container)}</div></div><span class="status ${p.status==='待打托'?'blue':'wait'}">${esc(p.status==='待打托'?t('pallet'):t('unload'))}</span></div><div class="list-grid"><div><span>${t('destinationCode')}</span><strong>${esc(p.destinationCode)}</strong></div><div><span>${t('boxes')}</span><strong>${p.receivedBoxes} / ${p.expectedBoxes}</strong></div><div><span>${t('arrived')}</span><strong>${esc(p.eta.slice(5))}</strong></div></div><div class="list-meta">${t('destinationAddress')}: ${esc(p.destinationAddress)}<br>${t('ticket')}: ${esc(p.tickets)}</div><div class="card-actions"><button class="btn primary" onclick="openPlan('${p.id}')">${t('startReceive')}</button></div></article>`).join('')}</div>`,t('plan'),t('planDesc'),'workbench');}
function selectedPlan(){return plans.find(p=>p.id===state.selectedPlanId)||plans[0];}
function resetPalletTypeInputs(){state.fbaPalletLabels=[];state.dropshipPalletLabel='';state.palletScanQty='1';state.palletTypeMessage=null;state.fbaPalletMessage=null;state.dropshipScanMessage=null;}
function setPalletOrderType(type){if(!['fba','overseas','dropship'].includes(type))return;state.palletOrderType=type;state.palletTypeMessage=null;state.fbaPalletMessage=null;state.dropshipScanMessage=null;render();}
function palletOrderTabs(){return `<div class="pallet-order-tabs" role="tablist" aria-label="${t('palletOrderType')}"><button type="button" role="tab" aria-selected="${state.palletOrderType==='fba'}" class="${state.palletOrderType==='fba'?'active':''}" onclick="setPalletOrderType('fba')">${t('fbaWarehouse')}</button><button type="button" role="tab" aria-selected="${state.palletOrderType==='overseas'}" class="${state.palletOrderType==='overseas'?'active':''}" onclick="setPalletOrderType('overseas')">${t('overseasWarehouse')}</button><button type="button" role="tab" aria-selected="${state.palletOrderType==='dropship'}" class="${state.palletOrderType==='dropship'?'active':''}" onclick="setPalletOrderType('dropship')">${t('oneClickDropship')}</button></div>`;}
const UNLOAD_PHOTO_STEPS=['licensePlate','containerSeal','doorOpen','other'];
const UNLOAD_REQUIRED_PHOTO_STEPS=['licensePlate','containerSeal','doorOpen'];
function startPalletWork(planId){const plan=plans.find(x=>x.id===planId)||plans[0];state.selectedPlanId=plan.id;state.palletMode='palletize';const nextIndex=Array.from({length:plan.expectedPallets},(_,index)=>index).find(index=>!state.pallets.some(x=>x.id===plannedPalletNo(plan,index)));if(nextIndex===undefined){startPlanPallet(plan.id);return}selectPallet(plannedPalletNo(plan,nextIndex),plan.id);}
function plansPalletView(){const pallets=plans.flatMap(plan=>Array.from({length:plan.expectedPallets},(_,index)=>{const id=plannedPalletNo(plan,index);const saved=state.pallets.find(item=>item.mode==='palletize'&&item.id===id);return {id,plan,saved,items:saved?.items||[],destinationCode:saved?.destinationCode||plan.destinationCode}}));const totalBoxes=pallets.length*5;return layout(`<div class="scan-box pallet-top-scan"><input id="pallet-top-scan" placeholder="${t('scanBoxSku')}" onkeydown="if(event.key==='Enter')handlePalletTopScan()"><button onclick="handlePalletTopScan()" aria-label="${t('scan')}">${icon('scan')}</button></div>${state.palletTopMessage?`<div class="scan-result ${state.palletTopMessage.type}">${esc(state.palletTopMessage.text)}</div>`:''}<div class="toolbar pallet-list-toolbar"><span class="subtle">${t('palletBoxSummary')}:${pallets.length}/${totalBoxes}</span></div><div class="table-wrap"><table class="table pallet-list-table"><thead><tr><th>${t('palletLabel')}</th><th>${t('destinationWarehouseShort')}</th><th>${t('cartonCount')}</th><th>${t('palletStatus')}</th></tr></thead><tbody>${pallets.map(pallet=>{const completed=Boolean(pallet.saved?.persisted||pallet.items.length);return `<tr><td>${esc(pallet.id)}</td><td>${esc(pallet.destinationCode||'—')}</td><td>${pallet.items.length}</td><td><span class="status ${completed?'good':'wait'}">${completed?t('palletCompleted'):t('palletPending')}</span></td></tr>`}).join('')}</tbody></table></div>`,t('inboundPallet'),t('inboundPalletDesc'),'workbench');}
function handlePalletTopScan(){const input=document.getElementById('pallet-top-scan');const value=(input?.value||'').trim();if(!value){state.palletTopMessage=null;state.palletMode='palletize';startPallet();return}const item=cargo.find(c=>c.boxNo===value||c.sku===value);if(!item||!item.received){state.palletTopMessage={type:'error',text:t('notFound')};render();return}const existing=state.pallets.find(pallet=>pallet.mode==='palletize'&&pallet.items?.some(current=>current.id===item.id));if(existing){state.palletTopMessage=null;state.palletMode='palletize';selectPallet(existing.id,existing.planId);return}state.palletTopMessage=null;state.palletMode='palletize';state.selectedPlanId=item.planNo;startPallet(item.id);}
function receivePhotoView(){const p=selectedPlan();const photos=state.unloadPhotos[p.id]||{};const completed=UNLOAD_PHOTO_STEPS.filter(k=>photos[k]).length;const total=UNLOAD_PHOTO_STEPS.length;const requiredComplete=UNLOAD_REQUIRED_PHOTO_STEPS.every(key=>photos[key]);return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('receive')} · ${esc(p.container)}</h2><p>${esc(p.id)} · ${esc(p.customer)}</p></div><span class="status ${completed===total?'good':'wait'}">${completed} / ${total}</span></div><div class="chips" style="margin-top:12px"><span class="chip blue">${t('destinationCode')}: ${esc(p.destinationCode)}</span><span class="chip">${t('receivePalletBoxes')}: ${p.expectedPallets} ${t('palletUnit')} / ${p.expectedBoxes} ${t('boxes')}</span></div><div class="progress"><span style="width:${completed/total*100}%"></span></div><div class="progress-line"><span>${t('photoProgress')}</span><strong>${completed} / ${total}</strong></div></section><section class="work-card"><div class="photo-grid">${UNLOAD_PHOTO_STEPS.map(key=>`<div class="photo-step ${photos[key]?'done':''}"><div class="photo-preview">${photos[key]?.startsWith('data:')?`<img src="${photos[key]}" alt="${t(key)}">`:photos[key]?icon('check'):icon('camera')}</div><strong>${t(key)}${UNLOAD_REQUIRED_PHOTO_STEPS.includes(key)?' *':''}</strong><label>${photos[key]?t('retake'):t('takePhoto')}<input type="file" accept="image/*" capture="environment" onchange="handleUnloadPhoto('${key}',this)"></label></div>`).join('')}</div></section>${!requiredComplete?`<div class="scan-result error">${t('photosRequired')}</div>`:''}<div class="bottom-bar"><button class="btn primary" onclick="completeUnload()" ${requiredComplete?'':'disabled'}>${t('confirmUnload')}</button></div>`,t('receive'),`${esc(p.id)} · ${esc(p.destinationCode)}`,'receive-list');}
function handleUnloadPhoto(key,input){const file=input.files&&input.files[0];if(!file)return;const reader=new FileReader();reader.onload=()=>{const p=selectedPlan();state.unloadPhotos[p.id]??={};state.unloadPhotos[p.id][key]=reader.result;render()};reader.readAsDataURL(file);}
function completeUnload(){const p=selectedPlan();const photos=state.unloadPhotos[p.id]||{};if(UNLOAD_REQUIRED_PHOTO_STEPS.some(key=>!photos[key])){toast(t('photosRequired'),'bad');return}p.receivedPallets=0;p.receivedBoxes=p.expectedPallets;p.status='待打托';cargo.filter(c=>c.planNo===p.id).forEach(c=>{c.received=true;c.receivedQty=c.qty});state.selectedPlanId=p.id;state.palletMode='palletize';state.pallet=null;state.palletMessage=null;state.page='plans';toast(t('taskDone'),'good');render();}
function receiveView(){const p=selectedPlan();const received=cargo.filter(c=>c.planNo===p.id&&c.received);const pct=Math.min(100,Math.round((p.receivedBoxes/p.expectedBoxes)*100));return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('receive')} · ${esc(p.id)}</h2><p>${esc(p.customer)} · ${esc(p.container)}</p></div><span class="status ${p.status==='待打托'?'blue':'wait'}">${esc(p.status)}</span></div><div class="chips" style="margin-top:12px"><span class="chip blue">${t('destinationCode')}: ${esc(p.destinationCode)}</span><span class="chip">${p.expectedBoxes} ${t('boxes')}</span></div><div class="progress"><span style="width:${pct}%"></span></div><div class="progress-line"><span>${t('receiveProgress')}</span><strong>${p.receivedBoxes} / ${p.expectedBoxes} ${t('boxes')}</strong></div><div class="scan-box"><input id="receive-scan" placeholder="${t('scanBox')}" onkeydown="if(event.key==='Enter')handleReceiveScan()"><button onclick="handleReceiveScan()" aria-label="${t('scan')}">${icon('scan')}</button></div>${state.receiveMessage?`<div class="scan-result ${state.receiveMessage.type}">${esc(state.receiveMessage.text)}</div>`:''}</section><section class="work-card"><div class="work-head"><div><h2>${t('receivedList')}</h2><p>${t('containerMeta',{container:p.container,boxes:received.length})}</p></div><span class="status good">${received.length}</span></div>${received.length?`<div class="list" style="margin-top:12px">${received.map(c=>`<div class="task-row"><span class="task-dot" style="background:var(--green)"></span><div class="task-main"><strong>${esc(c.boxNo)}</strong><p>${esc(c.sku)} · ${c.receivedQty} ${t('pieces')} · ${esc(c.ticketNo)}</p></div><span class="chip ${state.receiveStatus[c.id]==='damaged'?'red':'green'}">${t(state.receiveStatus[c.id]||'normal')}</span></div>`).join('')}</div>`:`<div class="empty">${t('empty')}</div>`}</section><div class="bottom-bar"><button class="btn primary" onclick="navigate('pallet')">${t('goPallet')}</button></div>`,t('receive'),`${esc(p.id)} · ${esc(p.destinationCode)}`,'plans');}
function handleReceiveScan(){const input=document.getElementById('receive-scan');const value=(input?.value||'').trim();if(!value){state.receiveMessage={type:'error',text:t('required')};render();return}const p=selectedPlan();const item=cargo.find(c=>c.planNo===p.id&&(c.boxNo===value||c.container===value));if(!item){state.receiveMessage={type:'error',text:t('notFound')};render();return}if(item.received){state.receiveMessage={type:'error',text:t('duplicate')};render();return}item.received=true;item.receivedQty=item.qty;p.receivedBoxes=Math.min(p.expectedBoxes,p.receivedBoxes+1);p.status=p.receivedBoxes>=p.expectedBoxes?'待打托':'待卸货';state.receiveMessage={type:'good',text:`${t('scanSuccess')}: ${item.boxNo}`};render();}
function setExpressOrderType(type){if(!['purchase','return'].includes(type))return;state.expressOrderType=type;render();}
function selectedExpressOrder(){return expressOrders.find(item=>item.id===state.selectedExpressOrderId)||null;}
function openExpressSignIn(orderId){const order=expressOrders.find(item=>item.id===orderId);if(!order||order.signedIn)return;state.selectedExpressOrderId=order.id;state.expressOrderType=order.type;state.expressScan=null;state.expressMessage=null;state.page='express-signin-detail';render();}
function handleExpressScan(){const order=selectedExpressOrder();const input=document.getElementById('express-scan');const value=(input?.value||'').trim();if(!order||!value){state.expressScan=null;state.expressMessage={type:'error',text:t('required')};render();return}if(value===order.boxMark){state.expressScan={kind:'carton',value,quantity:order.cartons};state.expressMessage={type:'good',text:`${t('scanSuccess')}: ${value}`};render();return}if(value.toUpperCase()===order.sku.toUpperCase()){state.expressScan={kind:'sku',value:order.sku,quantity:order.pieces};state.expressMessage={type:'good',text:`${t('scanSuccess')}: ${order.sku}`};render();return}state.expressScan=null;state.expressMessage={type:'error',text:t('unknownScan')};render();}
function setExpressQuantity(value){if(!state.expressScan)return;state.expressScan.quantity=value;state.expressMessage=null;const button=document.getElementById('express-complete');if(button)button.disabled=!(Number.isInteger(Number(value))&&Number(value)>0&&state.expressWaybillPhotos[state.selectedExpressOrderId]);}
function handleExpressWaybillPhoto(input){const order=selectedExpressOrder();const file=input.files&&input.files[0];if(!order||!file)return;const reader=new FileReader();reader.onload=()=>{state.expressWaybillPhotos[order.id]=reader.result;state.expressMessage=null;render()};reader.onerror=()=>toast(t('expressPhotoRequired'),'bad');reader.readAsDataURL(file);}
function completeExpressSignIn(){const order=selectedExpressOrder();if(!order)return;if(!state.expressScan){state.expressMessage={type:'error',text:t('expressScanRequired')};render();return}const quantity=Number(state.expressScan.quantity);if(!Number.isInteger(quantity)||quantity<1){state.expressMessage={type:'error',text:t('expressQuantityRequired')};render();return}if(!state.expressWaybillPhotos[order.id]){state.expressMessage={type:'error',text:t('expressPhotoRequired')};render();return}order.signedIn=true;order.receivedQuantity=quantity;order.receivedBy=state.expressScan.kind;state.page='express-signin';state.selectedExpressOrderId=null;state.expressScan=null;state.expressMessage=null;toast(t('expressSigned'),'good');render();}
function expressSignInView(){const type=state.expressOrderType||'purchase';const orders=expressOrders.filter(item=>item.type===type);return layout(`<div class="relabel-mode-tabs express-tabs" role="tablist" aria-label="${t('expressSignIn')}"><button type="button" role="tab" aria-selected="${type==='purchase'}" class="${type==='purchase'?'active':''}" onclick="setExpressOrderType('purchase')">${t('purchase')}</button><button type="button" role="tab" aria-selected="${type==='return'}" class="${type==='return'?'active':''}" onclick="setExpressOrderType('return')">${t('return')}</button></div><div class="toolbar"><span class="subtle">${orders.length} ${t('orderList')}</span></div><div class="express-order-list">${orders.map(order=>`<article class="express-order-card"><div class="express-order-row"><div class="express-order-field"><span>${t('customerCode')}</span><strong>${esc(order.customerCode)}</strong></div><div class="express-order-field"><span>${t('sku')}</span><strong>${esc(order.sku)}</strong></div><div class="express-order-field"><span>${t('cartonPiece')}</span><strong>${order.cartons} ${t('boxes')} / ${order.pieces} ${t('pieces')}</strong></div></div><div class="express-order-row"><div class="express-order-field"><span>${t('carrier')}</span><strong>${esc(order.carrier)}</strong></div><div class="express-order-field"><span>${t('trackingNo')}</span><strong>${esc(order.trackingNo)}</strong></div><div class="express-order-field"><span>${t('status')}</span><strong><span class="status ${order.signedIn?'good':'wait'}">${order.signedIn?t('expressSigned'):t('pending')}</span></strong></div><div class="express-order-action"><button type="button" class="btn ${order.signedIn?'ghost':'primary'}" onclick="openExpressSignIn('${order.id}')" ${order.signedIn?'disabled':''}>${order.signedIn?t('expressSigned'):t('expressSign')}</button></div></div></article>`).join('')}</div>`,t('expressSignIn'),t('expressSignInDesc'),'workbench');}
function expressSignInDetailView(){const order=selectedExpressOrder();if(!order){state.page='express-signin';return expressSignInView()}const scan=state.expressScan;const photo=state.expressWaybillPhotos[order.id];const ready=Boolean(scan&&Number.isInteger(Number(scan.quantity))&&Number(scan.quantity)>0&&photo);return layout(`<section class="work-card express-scan-card"><div class="work-head"><div><h2>${t('expressScanHint')}</h2><p>${esc(order.id)} · ${esc(order.customerCode)}</p></div></div><div class="detail-list express-order-context"><div class="detail-row"><span>${t('carrier')}</span><strong>${esc(order.carrier)}</strong></div><div class="detail-row"><span>${t('trackingNo')}</span><strong>${esc(order.trackingNo)}</strong></div></div><div class="scan-box"><input id="express-scan" aria-label="${t('expressScanHint')}" placeholder="${t('expressScanHint')}" autocomplete="off" onkeydown="if(event.key==='Enter')handleExpressScan()"><button type="button" onclick="handleExpressScan()" aria-label="${t('scan')}">${icon('scan')}</button></div>${scan?`<div class="field express-quantity-field"><label for="express-quantity">${scan.kind==='carton'?t('cartonQuantity'):t('pieceQuantity')}</label><input id="express-quantity" type="number" min="1" step="1" inputmode="numeric" value="${esc(scan.quantity)}" oninput="setExpressQuantity(this.value)"></div>`:''}${state.expressMessage?`<div class="scan-result ${state.expressMessage.type}" role="${state.expressMessage.type==='error'?'alert':'status'}" style="margin-top:12px">${esc(state.expressMessage.text)}</div>`:''}</section><section class="work-card"><div class="work-head"><div><h2>${t('photoInfo')}</h2><p>${t('waybill')}</p></div><span class="status ${photo?'good':'wait'}">${photo?'1 / 1':'0 / 1'}</span></div><div class="express-photo-row">${photo?`<div class="express-waybill-preview"><img src="${photo}" alt="${t('waybill')}"></div>`:''}<label class="express-photo-add" aria-label="${t('takePhoto')}"><span aria-hidden="true">+</span><input id="express-waybill-photo-input" type="file" accept="image/*" capture="environment" onchange="handleExpressWaybillPhoto(this)"></label></div></section><div class="bottom-bar"><button id="express-complete" type="button" class="btn primary" onclick="completeExpressSignIn()" ${ready?'':'disabled'}>${t('expressComplete')}</button></div>`,t('expressSignIn'),`${esc(order.carrier)} · ${esc(order.trackingNo)}`,'express-signin');}
function canMergePallet(pallet,cargo){
return !pallet.destinationCode || ((!cargo.destinationCode || pallet.destinationCode===cargo.destinationCode) && (!pallet.destinationAddress || pallet.destinationAddress===cargo.destinationAddress));
}
function startPlanPallet(planId){state.selectedPlanId=planId;state.palletMode='palletize';state.pallet=null;state.palletMessage=null;resetPalletTypeInputs();state.page='pallet-list';render();}
function startConsolidation(){state.palletMode='consolidate';state.pallet=null;state.palletMessage=null;state.photoPalletId='';resetPalletTypeInputs();state.page='pallet-list';render();}
function plannedPalletNo(plan,index){return `${plan.container}-TC-${String(index+1).padStart(3,'0')}`;}
function palletWeight(pallet){return pallet.items.reduce((sum,item)=>sum+Number(item.weight||0),0);}
function newGeneratedPalletLabel(){const plan=selectedPlan();const used=state.pallets.filter(pallet=>pallet.mode==='palletize'&&pallet.planId===plan.id).map(pallet=>pallet.id);const nextIndex=Array.from({length:plan.expectedPallets},(_,index)=>index).find(index=>!used.includes(plannedPalletNo(plan,index)));if(nextIndex===undefined){toast(t('palletTargetReached'),'bad');return}selectPallet(plannedPalletNo(plan,nextIndex),plan.id);}
function palletListView(){const plan=selectedPlan();const created=state.pallets.filter(pallet=>pallet.mode==='palletize'&&pallet.planId===plan.id&&pallet.items?.length>0);const progress=`${created.length} / ${plan.expectedPallets} ${t('palletCount')}`;return layout(`<div class="toolbar pallet-list-toolbar"><span class="subtle">${t('palletProgress')}: ${progress}</span></div>${created.length?`<div class="list">${created.map(pallet=>`<article class="list-card"><div class="list-top"><div><div class="list-title">${esc(pallet.id)}</div><div class="list-meta">${esc(plan.id)} · ${esc(pallet.container||plan.container)}</div></div></div><div class="list-grid"><div><span>${t('destination')}</span><strong>${esc(pallet.destinationCode||plan.destinationCode)}</strong></div><div><span>${t('cartonCount')}</span><strong>${pallet.items.length}</strong></div><div><span>${t('weight')}</span><strong>${palletWeight(pallet).toFixed(1)} kg</strong></div></div><div class="card-actions"><button class="btn ghost" onclick="openPalletPhotos('${pallet.id}','${plan.id}')">${t('photoDetails')}</button><button class="btn primary" onclick="selectPallet('${pallet.id}','${plan.id}')">${t('directEnter')}</button></div></article>`).join('')}</div>`:`<div class="empty">${t('empty')}</div>`}<div class="bottom-bar pallet-list-footer"><div><button class="btn secondary" onclick="newGeneratedPalletLabel()">${t('generatePalletLabel')}</button><button class="btn primary" onclick="startPallet()">${t('palletizeFooter')}</button></div></div>`,t('palletList'),`${plan.id} · ${t('palletListDesc')}`,'plans');}
function selectPallet(palletId,planId){const saved=state.pallets.find(x=>x.id===palletId);if(planId)state.selectedPlanId=planId;const consolidating=state.palletMode==='consolidate';resetPalletTypeInputs();if(saved?.orderType)state.palletOrderType=saved.orderType;state.pallet=saved?{...JSON.parse(JSON.stringify(saved)),persisted:true}:{id:palletId,mode:consolidating?'consolidate':'palletize',planId:consolidating?null:state.selectedPlanId,targetPallets:consolidating?null:selectedPlan().expectedPallets,destinationCode:'',destinationAddress:'',items:[]};if(saved?.selectedPalletLabels)state.fbaPalletLabels=[...saved.selectedPalletLabels];if(saved?.dropshipPalletLabel)state.dropshipPalletLabel=saved.dropshipPalletLabel;state.palletMessage=null;state.page='pallet';render();}
function overseasPalletDetails(){return `<section class="work-card pallet-items-card"><div class="work-head"><div><h2>${t('items')}</h2><p>${t('trace')}</p></div></div>${state.pallet.items.length?`<div class="table-wrap"><table class="table"><thead><tr><th>${t('boxNo')}</th><th>${t('sku')}</th><th>${t('customer')}</th><th>${t('planNo')}</th><th>${t('qty')}</th><th></th></tr></thead><tbody>${state.pallet.items.map((item,index)=>`<tr><td>${esc(item.boxNo)}</td><td>${esc(item.sku)}</td><td>${esc(item.customer)}</td><td>${esc(item.planNo)}</td><td>${item.qty}</td><td><button class="btn danger" style="min-height:44px;padding:0 8px" onclick="removePalletItem(${index})">${t('remove')}</button></td></tr>`).join('')}</tbody></table></div>`:`<div class="empty">${t('emptyPallet')}</div>`}</section>`;}
function toggleFbaPalletLabel(palletId){const occupied=state.pallets.some(pallet=>pallet.id===palletId);if(state.pallet?.persisted||occupied)return;const selected=state.fbaPalletLabels.includes(palletId);state.fbaPalletLabels=selected?state.fbaPalletLabels.filter(id=>id!==palletId):[...state.fbaPalletLabels,palletId];state.palletTypeMessage=null;state.fbaPalletMessage=null;render();}
function fbaPalletLabelSelector(plan){const labels=Array.from({length:plan.expectedPallets},(_,index)=>plannedPalletNo(plan,index));const readonly=Boolean(state.pallet?.persisted);return `<section class="work-card fba-pallet-selector"><div class="work-head"><div><h2>${t('selectPalletLabels')}</h2><p>${t('palletLabel')} · ${t('palletStatus')}</p></div><span class="status blue">${state.fbaPalletLabels.length}</span></div><div class="fba-pallet-options">${labels.map(palletId=>{const selected=state.fbaPalletLabels.includes(palletId);const occupied=state.pallets.some(pallet=>pallet.id===palletId);const disabled=readonly||occupied;return `<button type="button" class="fba-pallet-choice ${selected?'selected':''}" aria-pressed="${selected}" ${disabled?'disabled aria-disabled="true"':''} onclick="toggleFbaPalletLabel('${esc(palletId)}')">${esc(palletId)}${selected?' ✓':''}</button>`}).join('')}</div>${state.fbaPalletMessage?`<div class="scan-result ${state.fbaPalletMessage.type}">${esc(state.fbaPalletMessage.text)}</div>`:''}</section>`;}
function palletView(){
const p=selectedPlan();
const consolidating=state.palletMode==='consolidate';
const done=consolidating?0:state.pallets.filter(x=>x.mode==='palletize'&&x.planId===p.id).length;
const targetReached=!consolidating&&done>=p.expectedPallets;
const candidates=cargo.filter(c=>c.received&&!c.palletNo&&(consolidating||c.planNo===p.id));
const title=consolidating?t('mergeOnly'):t('pallet');
const description=consolidating?t('mergeDesc'):t('palletDesc');
if(!state.pallet){return layout(`<section class="work-card"><div class="work-head"><div><h2>${title}</h2><p>${description}</p></div><span class="status ${targetReached?'good':'blue'}">${consolidating?candidates.length:`${done} / ${p.expectedPallets}`}</span></div><div class="list" style="margin-top:12px">${candidates.map(c=>`<div class="task-row"><span class="task-dot"></span><div class="task-main"><strong>${esc(c.boxNo)} · ${esc(c.sku)}</strong><p>${esc(c.customer)} · ${esc(c.planNo)} · ${esc(c.destinationCode)}</p></div><button onclick="startPallet('${c.id}')" ${targetReached?'disabled':''}>${t('choose')}</button></div>`).join('')||`<div class="empty">${targetReached?t('taskDone'):t('empty')}</div>`}</div></section><div class="bottom-bar"><button class="btn primary" onclick="startPallet()" ${targetReached?'disabled':''}>${t('newPallet')}</button></div>`,title,description,consolidating?'workbench':'plans');}
const showDetails=state.palletOrderType==='overseas';
const scanHint=state.palletOrderType==='dropship'?t('scanPalletLabel'):t('scanBoxSku');
const progress=`${t('palletProgress')}: ${done} / ${p.expectedPallets} ${t('palletCount')}`;
const cargoInfo=`<section class="work-card cargo-info-card"><div class="work-head"><div><h2>${t('cargoInfo')}</h2><p>${state.pallet.destinationCode?`${t('lockedDest')}: ${esc(state.pallet.destinationCode)}`:description}</p></div><span class="status blue">${state.pallet.items.length} ${t('items')}</span></div><div class="detail-list"><div class="detail-row"><span>${t('inboundCode')}</span><strong>${esc(p.inboundCode||p.id||'—')}</strong></div><div class="detail-row"><span>${t('destinationWarehouseShort')}</span><strong>${esc(state.pallet.destinationCode||'—')}</strong></div><div class="detail-row"><span>${t('cartonCount')}</span><strong>${state.pallet.items.length}</strong></div></div></section>`;
const scanCard=`<section class="work-card pallet-type-scan"><div class="work-head"><div><h2>${esc(scanHint)}</h2></div></div><div class="scan-box"><input id="pallet-scan" placeholder="${esc(scanHint)}" oninput="updatePalletScanRemaining()" onkeydown="if(event.key==='Enter')handlePalletScan()"><button onclick="handlePalletScan()" aria-label="${t('scan')}">${icon('scan')}</button></div>${state.palletOrderType==='fba'?`<div class="fba-scan-quantity"><div class="field"><label for="pallet-scan-qty">${t('inputCartonQty')}</label><input id="pallet-scan-qty" type="number" min="1" step="1" value="${esc(state.palletScanQty||'1')}" oninput="updatePalletScanQty(this.value)"></div><div class="detail-row"><span>${t('remainingSkuCartons')}</span><strong id="pallet-scan-remaining">${palletScanRemaining('')}</strong></div></div>`:''}${state.palletMessage?`<div class="scan-result ${state.palletMessage.type}">${esc(state.palletMessage.text)}</div>`:''}${state.palletOrderType==='dropship'&&state.dropshipScanMessage?`<div class="scan-result ${state.dropshipScanMessage.type}">${esc(state.dropshipScanMessage.text)}</div>`:''}</section>`;
return layout(`${palletOrderTabs()}${scanCard}<div class="scan-result info pallet-progress-card">${progress}</div>${cargoInfo}${state.palletOrderType==='fba'?fbaPalletLabelSelector(p):''}${showDetails?overseasPalletDetails():''}<div class="bottom-bar"><div style="display:flex;gap:8px"><button class="btn ghost" style="flex:1" onclick="newPallet()">${t('newPallet')}</button><button class="btn primary" style="flex:2" onclick="submitPallet()" ${state.pallet.items.length?'':'disabled'}>${t('submitPallet')}</button></div></div>`,title,consolidating?t('sameDest'):`${p.id} · ${done} / ${p.expectedPallets} ${t('palletCount')}`,consolidating?'workbench':'plans');
}
function startPallet(itemId){const p=selectedPlan();const item=itemId?cargo.find(c=>c.id===itemId):null;const consolidating=state.palletMode==='consolidate';const done=state.pallets.filter(x=>x.mode==='palletize'&&x.planId===p.id).length;if(!consolidating&&done>=p.expectedPallets){toast(t('palletTargetReached'),'bad');return}if(item&&!consolidating&&item.planNo!==p.id){toast(t('notFound'),'bad');return}resetPalletTypeInputs();state.pallet={id:'',mode:consolidating?'consolidate':'palletize',planId:consolidating?null:p.id,targetPallets:consolidating?null:p.expectedPallets,destinationCode:item?.destinationCode||'',destinationAddress:item?.destinationAddress||'',items:[]};state.palletMessage=null;if(item)addPalletItem(item);state.page='pallet';render();}
function newPallet(){if(state.pallet?.items?.length){showModal(t('newPalletConfirm'),()=>{closeModal();startPallet();});return}startPallet();}
function palletScanCandidates(value){return cargo.filter(item=>(item.sku===value||item.boxNo===value)&&item.received&&!item.palletNo&&!state.pallet.items.some(current=>current.id===item.id)&&(state.palletMode==='consolidate'||item.planNo===state.selectedPlanId));}
function palletScanRemaining(value){return palletScanCandidates(value).length;}
function updatePalletScanQty(value){const qty=Number(value);state.palletScanQty=Number.isInteger(qty)&&qty>0?String(qty):'1';updatePalletScanRemaining();}
function openPalletScanModal(){const root=document.getElementById('modal-root');const value=(document.getElementById('pallet-scan')?.value||'').trim();root.innerHTML=`<div class="modal-backdrop"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="pallet-scan-modal-title"><h2 id="pallet-scan-modal-title">${t('addCargo')}</h2><div class="detail-row"><span>${t('sku')} / ${t('boxNo')}</span><strong>${esc(value||'—')}</strong></div><div class="field"><label for="pallet-modal-qty">${t('scanCartonQty')}</label><input id="pallet-modal-qty" type="number" min="1" step="1" value="1" onkeydown="if(event.key==='Enter')confirmPalletScanModal()"></div><div class="detail-row"><span>${t('remainingSkuCartons')}</span><strong id="pallet-modal-remaining">0</strong></div><div class="modal-actions"><button class="btn ghost" onclick="closeModal()">${t('cancel')}</button><button class="btn primary" onclick="confirmPalletScanModal()">${t('yes')}</button></div></div></div>`;updatePalletScanRemaining();requestAnimationFrame(()=>document.getElementById('pallet-modal-qty')?.focus());}
function updatePalletScanRemaining(){const value=(document.getElementById('pallet-scan')?.value||'').trim();const remaining=palletScanRemaining(value);const inline=document.getElementById('pallet-scan-remaining');if(inline)inline.textContent=remaining;const output=document.getElementById('pallet-modal-remaining');if(output)output.textContent=remaining;const qty=document.getElementById('pallet-modal-qty');if(qty)qty.max=String(Math.max(1,remaining));}
function confirmPalletScanModal(){const value=(document.getElementById('pallet-scan')?.value||'').trim();const qty=Number(document.getElementById('pallet-modal-qty')?.value);if(!value){toast(t('required'),'bad');return}if(!Number.isInteger(qty)||qty<1){toast(t('required'),'bad');return}closeModal();handlePalletScan();}
function handlePalletScan(){
const input=document.getElementById('pallet-scan');
const value=(input?.value||'').trim();
if(!value){return}
if(state.palletMode==='palletize'&&state.palletOrderType==='dropship'){
const pallet=state.pallets.find(item=>String(item.id).toLowerCase()===value.toLowerCase());
if(!pallet){state.dropshipScanMessage={type:'error',text:t('unknownPalletScan')};render();return}
state.dropshipPalletLabel=pallet.id;
if(!state.pallet.items.length){state.selectedPlanId=pallet.planId||state.selectedPlanId;state.pallet={...state.pallet,planId:pallet.planId||state.pallet.planId,destinationCode:pallet.destinationCode||'',destinationAddress:pallet.destinationAddress||'',items:JSON.parse(JSON.stringify(pallet.items||[]))}}
state.palletMessage=null;
state.dropshipScanMessage={type:'good',text:`${t('scanSuccess')}: ${pallet.id}`};
render();
return;
}
const candidates=palletScanCandidates(value);
if(!candidates.length){state.palletMessage={type:'error',text:t('notFound')};render();return}
const requestedQty=state.palletOrderType==='fba'?Math.max(1,Number(state.palletScanQty||1)):1;
const selected=candidates.slice(0,requestedQty);
if(selected.length<requestedQty){state.palletMessage={type:'error',text:t('notFound')};render();return}
const blocked=selected.find(item=>!canMergePallet(state.pallet,item));
if(blocked){state.palletMessage={type:'error',text:t('mergeBlocked')};render();return}
selected.forEach(item=>addPalletItem(item));
state.palletMessage={type:'good',text:`${t('mergeOk')}: ${selected.map(item=>item.boxNo).join(', ')}`};
state.palletScanQty='1';
render();
}
function addPalletItem(item){const isMerged=state.pallet.items.length>0;if(!state.pallet.destinationCode){state.pallet.destinationCode=item.destinationCode;state.pallet.destinationAddress=item.destinationAddress}state.pallet.items.push({...item,isMerged,mergeFlag:isMerged?t('yesMerge'):t('noMerge')});}
function removePalletItem(index){const removed=state.pallet.items.splice(index,1)[0];const source=cargo.find(x=>x.id===removed.id);if(source&&source.palletNo===state.pallet.id)source.palletNo='';if(state.pallet.persisted){const savedIndex=state.pallets.findIndex(x=>x.id===state.pallet.id);if(savedIndex>=0){if(state.pallet.items.length){const updated=JSON.parse(JSON.stringify(state.pallet));delete updated.persisted;state.pallets[savedIndex]=updated}else{state.pallets.splice(savedIndex,1);state.pallet.persisted=false}}}if(!state.pallet.items.length){state.pallet.destinationCode='';state.pallet.destinationAddress=''}state.palletMessage={type:'info',text:`${t('remove')}: ${removed.boxNo}`};render();}
function submitPallet(){
if(!state.pallet?.items.length){state.palletMessage={type:'error',text:t('emptyPallet')};render();return}
if(state.pallet.mode==='palletize'&&state.palletOrderType==='dropship'&&!state.dropshipPalletLabel){state.dropshipScanMessage={type:'error',text:t('scanPalletLabel')};render();return}
if(state.pallet.mode==='palletize'&&state.palletOrderType==='fba'&&!state.fbaPalletLabels.length){state.fbaPalletMessage={type:'error',text:t('selectAtLeastOnePallet')};render();return}
const p=selectedPlan();
const done=state.pallets.filter(x=>x.mode==='palletize'&&x.planId===p.id).length;
if(state.pallet.mode==='palletize'&&done>=p.expectedPallets){state.palletMessage={type:'error',text:t('palletTargetReached')};render();return}
const sourcePlan=state.pallet.mode==='palletize'?p:plans.find(x=>x.id===state.pallet.items[0]?.planNo);
const prefix=sourcePlan?.container||'D0000';
const sequence=state.pallets.filter(x=>x.container===prefix).length+1;
if(state.pallet.mode==='palletize'&&state.palletOrderType==='fba'){
const remaining=Math.max(0,p.expectedPallets-done);
const selected=[...new Set(state.fbaPalletLabels)].filter(id=>!state.pallets.some(pallet=>pallet.id===id)).slice(0,remaining);
if(!selected.length){state.fbaPalletMessage={type:'error',text:t('selectAtLeastOnePallet')};render();return}
const records=selected.map((id,index)=>({...JSON.parse(JSON.stringify(state.pallet)),id,container:prefix,orderType:'fba',selectedPalletLabels:[...selected],items:state.pallet.items.filter((item,itemIndex)=>itemIndex%selected.length===index),persisted:true}));
records.forEach(record=>record.items.forEach(item=>{const source=cargo.find(c=>c.id===item.id);if(source)source.palletNo=record.id}));
state.pallets.push(...records);
p.receivedPallets=Math.min(p.expectedPallets,done+records.length);
state.pallet=records[0];
const submittedLabels=records.map(record=>record.id).join('、');
const submittedItems=records.reduce((count,record)=>count+record.items.length,0);
resetPalletTypeInputs();
state.palletMessage={type:'good',text:`${t('submitted')}: ${submittedLabels}`};
showModal(`${t('submitted')}: ${submittedLabels} · ${t('trace')}: ${submittedItems} ${t('items')}`,()=>{state.pallet=null;closeModal();state.page='plans';render();});
return;
}
const no=state.pallet.id||state.pallet.selectedPalletLabels?.[0]||`${prefix}-TC-${String(sequence).padStart(3,'0')}`;
state.pallet.id=no;
state.pallet.container=prefix;
state.pallet.items.forEach(x=>{const source=cargo.find(c=>c.id===x.id);if(source)source.palletNo=no});
state.pallet.orderType=state.palletOrderType;
state.pallet.dropshipPalletLabel=state.dropshipPalletLabel;
state.pallet.persisted=true;
state.pallets.push(JSON.parse(JSON.stringify(state.pallet)));
if(state.pallet.mode==='palletize')p.receivedPallets=Math.min(p.expectedPallets,done+1);
resetPalletTypeInputs();
state.palletMessage={type:'good',text:`${t('submitted')}: ${no}`};
showModal(`${t('submitted')} ${no}<br><small>${t('trace')}: ${state.pallet.items.length} ${t('items')}</small>`,()=>{const mode=state.pallet.mode;state.pallet=null;closeModal();state.page=mode==='palletize'?'plans':'pallet-list';render();});
}
function pickView(){const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];if(!state.pickTaskId)state.pickTaskId=task.id;const done=task.items.filter(x=>x.status||x.picked>=x.qty).length;const total=task.items.length;const pickedPallets=task.done?task.palletCount:Math.min(task.palletCount,Math.floor(done/total*task.palletCount));return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('pick')} · ${esc(task.id)}</h2><p>${esc(task.customer)} · ${esc(task.destination)}</p></div><span class="status wait">${task.done?t('taskDone'):t('pending')}</span></div><div class="detail-list"><div class="detail-row"><span>${t('palletQty')}</span><strong>${pickedPallets} / ${task.palletCount} ${t('palletCount')}</strong></div><div class="detail-row"><span>${t('linkedOrder')}</span><strong>${esc(task.orderInfo)}</strong></div><div class="detail-row"><span>${t('boxes')}</span><strong>${task.boxes}</strong></div></div><div class="progress"><span style="width:${task.palletCount?Math.round(pickedPallets/task.palletCount*100):0}%"></span></div><div class="progress-line"><span>${t('pickProgress')}</span><strong>${pickedPallets} / ${task.palletCount} ${t('palletCount')}</strong></div><div class="scan-box"><input id="pick-scan" placeholder="${t('reviewScan')}" onkeydown="if(event.key==='Enter')handlePickScan()"><button onclick="handlePickScan()" aria-label="${t('scan')}">⌗</button></div>${state.pickMessage?`<div class="scan-result ${state.pickMessage.type}">${esc(state.pickMessage.text)}</div>`:''}</section><section class="work-card"><div class="list">${task.items.map((x,i)=>`<div class="list-card" style="padding:12px"><div class="list-top"><div><div class="list-title">${esc(x.sku)}</div><div class="list-meta">${x.picked} / ${x.qty} ${t('pieces')}${x.skipReason?` · ${esc(x.skipReason)}`:''}</div></div><span class="status ${x.status==='picked'?'good':x.status==='skipped'?'bad':x.status==='partial'?'blue':'wait'}">${t(x.status||'pending')}</span></div><div class="card-actions" style="justify-content:stretch;flex-wrap:wrap"><button class="btn secondary" onclick="pickOne(${i})">${t('confirmPick')}</button><button class="btn ghost" onclick="openPartialPick(${i})">${t('partialPick')}</button><button class="btn danger" onclick="skipPick(${i})">${t('skipPick')}</button>${x.status?`<button class="btn ghost" onclick="undoPick(${i})">${t('undo')}</button>`:''}</div></div>`).join('')}</div></section><div class="bottom-bar"><button class="btn primary" onclick="submitPick()" ${done===total?'':'disabled'}>${t('submitPick')}</button></div>`,t('pick'),t('pickDesc'),'workbench');}
function handlePickScan(){const value=(document.getElementById('pick-scan')?.value||'').trim();if(!value){state.pickMessage={type:'error',text:t('required')};render();return}const task=pickTasks.find(x=>x.id===state.pickTaskId);const item=task.items.find(x=>x.sku===value);if(!item){state.pickMessage={type:'error',text:t('unknownScan')};render();return}if(item.picked>=item.qty){state.pickMessage={type:'error',text:t('complete')};render();return}item.picked++;state.pickMessage={type:'good',text:`${t('scanSuccess')}: ${value}`};render();}
function pickOne(index){const task=pickTasks.find(x=>x.id===state.pickTaskId);const item=task.items[index];item.picked=item.qty;item.status='picked';delete item.skipReason;render();}
function openPartialPick(index){const task=pickTasks.find(x=>x.id===state.pickTaskId);const item=task.items[index];document.getElementById('modal-root').innerHTML=`<div class="modal-backdrop"><div class="modal"><h2>${t('partialPick')} · ${esc(item.sku)}</h2><p>${t('qty')}: 1 - ${item.qty-1}</p><div class="field"><input id="partial-qty" type="number" min="1" max="${item.qty-1}" value="${Math.max(1,Math.floor(item.qty/2))}"></div><div class="modal-actions"><button class="btn ghost" onclick="closeModal()">${t('cancel')}</button><button class="btn primary" onclick="confirmPartialPick(${index})">${t('yes')}</button></div></div></div>`;}
function confirmPartialPick(index){const task=pickTasks.find(x=>x.id===state.pickTaskId);const item=task.items[index];const qty=Number(document.getElementById('partial-qty')?.value);if(!Number.isInteger(qty)||qty<=0||qty>=item.qty){toast(`${t('qty')}: 1 - ${item.qty-1}`,'bad');return}item.picked=qty;item.status='partial';delete item.skipReason;closeModal();render();}
function skipPick(index){const task=pickTasks.find(x=>x.id===state.pickTaskId);const item=task.items[index];document.getElementById('modal-root').innerHTML=`<div class="modal-backdrop"><div class="modal"><h2>${t('skipReasonTitle')} · ${esc(item.sku)}</h2><div class="field"><label>${t('skipReasonTitle')}</label><select id="skip-reason"><option value="notFound">${t('skipReasonNotFound')}</option><option value="outOfStock">${t('skipReasonOutOfStock')}</option><option value="other">${t('skipReasonOther')}</option></select></div><div class="modal-actions"><button class="btn ghost" onclick="closeModal()">${t('cancel')}</button><button class="btn primary" onclick="confirmSkipPick(${index})">${t('yes')}</button></div></div></div>`;}
function confirmSkipPick(index){const task=pickTasks.find(x=>x.id===state.pickTaskId);const item=task.items[index];const reason=document.getElementById('skip-reason')?.value||'other';const labels={notFound:t('skipReasonNotFound'),outOfStock:t('skipReasonOutOfStock'),other:t('skipReasonOther')};item.picked=0;item.status='skipped';item.skipReason=labels[reason];closeModal();render();}
function undoPick(index){const task=pickTasks.find(x=>x.id===state.pickTaskId);const item=task.items[index];item.picked=0;delete item.status;delete item.skipReason;render();}
function submitPick(){const task=pickTasks.find(x=>x.id===state.pickTaskId);if(task.items.some(x=>!x.status&&x.picked<x.qty)){toast(t('pending'),'info');return}task.done=true;toast(t('taskDone'),'good');state.page='workbench';render();}
function reviewView(){const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];const scanned=state.reviewScanned.length;const target=30;const pct=Math.min(100,Math.round(scanned/target*100));return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('review')}</h2><p>${t('reviewDesc')}</p></div><span class="status blue">${scanned}</span></div><div class="detail-list"><div class="detail-row"><span>${t('driver')}</span><strong>${esc(task.driver)}</strong></div><div class="detail-row"><span>${t('plateNo')}</span><strong>${esc(task.plateNo)}</strong></div><div class="detail-row"><span>${t('vehicleType')}</span><strong>${t('vehicleTypeValue')}</strong></div><div class="detail-row"><span>${t('reviewPallets')}</span><strong>${t('palletReferenceValue')}</strong></div></div><div class="scan-box"><input id="review-scan" placeholder="${t('reviewScanPallet')}" onkeydown="if(event.key==='Enter')handleReviewScan()"><button onclick="handleReviewScan()" aria-label="${t('scan')}">⌗</button></div><div class="progress"><span style="width:${pct}%"></span></div><div class="progress-line"><span>${t('reviewPalletProgress')}</span><strong>${scanned} / ${target} ${t('palletCount')}</strong></div>${state.reviewScanned.length?`<div class="list">${state.reviewScanned.map(x=>`<div class="task-row"><span class="task-dot" style="background:var(--green)"></span><div class="task-main"><strong>${esc(x)}</strong><p>${t('scanSuccess')}</p></div><span class="status good">OK</span></div>`).join('')}</div>`:`<div class="empty">${t('empty')}</div>`}</section><div class="bottom-bar"><button class="btn primary" onclick="completeReview()" ${state.reviewScanned.length?'':'disabled'}>${t('confirmReview')}</button></div>`,t('review'),t('reviewDesc'),'workbench');}
function handleReviewScan(){const value=(document.getElementById('review-scan')?.value||'').trim();if(!value){toast(t('required'),'bad');return}if(!state.reviewScanned.includes(value))state.reviewScanned.push(value);render();}
function completeReview(){state.reviewScanned=[];toast(t('taskDone'),'good');state.page='workbench';render();}
const HANDOVER_PHOTO_STEPS=['licensePlatePhoto','vehicleCabPhoto','driverLicense','loadingSealPhoto','lockSealPhoto','bolDriverSigned','amazonAppointment'];
function handoverView(){const completed=HANDOVER_PHOTO_STEPS.filter(key=>state.handoverPhotos[key]).length;const totalPallets=pickTasks.reduce((sum,task)=>sum+Number(task.palletCount||0),0);const totalBoxes=pickTasks.reduce((sum,task)=>sum+Number(task.boxes||0),0);const totalWeight=pickTasks.reduce((sum,task)=>sum+Number(task.totalWeightLb||0),0);return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('handover')}</h2><p>${t('handoverDesc')}</p></div><span class="status ${state.handoverDone?'good':completed===7?'blue':'wait'}">${state.handoverDone?t('complete'):`${completed} / 7`}</span></div><div class="detail-list"><div class="detail-row"><span>${t('carrier')}</span><strong>Pacific Linehaul</strong></div><div class="detail-row"><span>${t('driver')}</span><strong>Michael Chen</strong></div><div class="detail-row"><span>${t('vehicle')}</span><strong>CA 8KX-217</strong></div><div class="detail-row"><span>${t('palletQty')}</span><strong>${totalPallets} ${t('palletCount')}</strong></div><div class="detail-row"><span>${t('totalBoxes')}</span><strong>${totalBoxes}</strong></div><div class="detail-row"><span>${t('totalWeight')}</span><strong>${totalWeight.toFixed(1)} lb</strong></div></div><div class="progress"><span style="width:${completed/7*100}%"></span></div><div class="progress-line"><span>${t('handoverPhotos')}</span><strong>${completed} / 7</strong></div></section><section class="work-card"><div class="photo-grid">${HANDOVER_PHOTO_STEPS.map(key=>`<div class="photo-step ${state.handoverPhotos[key]?'done':''}"><div class="photo-preview">${state.handoverPhotos[key]?.startsWith('data:')?`<img src="${state.handoverPhotos[key]}" alt="${t(key)}">`:state.handoverPhotos[key]?icon('check'):icon('camera')}</div><strong>${t(key)}</strong><label>${state.handoverPhotos[key]?t('retake'):t('takePhoto')}<input type="file" accept="image/*" capture="environment" onchange="handleHandoverPhoto('${key}',this)"></label></div>`).join('')}</div></section>${completed<7?`<div class="scan-result error">${t('handoverPhotosRequired')}</div>`:''}<div class="bottom-bar"><button class="btn primary" onclick="completeHandover()" ${state.handoverDone||completed<7?'disabled':''}>${t('handoverDone')}</button></div>`,t('handover'),t('handoverDesc'),'workbench');}
function handleHandoverPhoto(key,input){const file=input.files&&input.files[0];if(!file)return;const reader=new FileReader();reader.onload=()=>{state.handoverPhotos[key]=reader.result;render()};reader.readAsDataURL(file);}
function completeHandover(){if(HANDOVER_PHOTO_STEPS.some(key=>!state.handoverPhotos[key])){toast(t('handoverPhotosRequired'),'bad');return}state.handoverDone=true;toast(t('taskDone'),'good');render();}
function packingView(){return layout(`<section class="work-card packing-scan-card"><div class="scan-box"><input id="packing-scan" autocomplete="off" placeholder="${t('packingScanHint')}" aria-label="${t('packingScanHint')}" onkeydown="if(event.key==='Enter')handlePackingScan()"><button type="button" onclick="handlePackingScan()" aria-label="${t('scan')}">${icon('scan')}</button></div>${state.packingMessage?`<div class="scan-result ${state.packingMessage.type}" role="${state.packingMessage.type==='error'?'alert':'status'}"${state.packingMessage.type==='error'?'':' aria-live="polite"'}>${esc(state.packingMessage.text)}</div>`:''}</section><section class="work-card packing-customer-card"><div class="field packing-customer-field"><label for="packing-customer-code">${t('customerCode')} *</label><input id="packing-customer-code" required aria-required="true" aria-invalid="${Boolean(state.packingCustomerError)}" aria-describedby="packing-customer-error" maxlength="30" autocomplete="off" autocapitalize="characters" value="${esc(state.packingCustomerCode)}" placeholder="${t('customerCodeHint')}" oninput="state.packingCustomerCode=this.value;state.packingCustomerError='';this.setAttribute('aria-invalid','false');document.getElementById('packing-customer-error').textContent=''"></div><div id="packing-customer-error" class="packing-customer-error" role="alert">${esc(state.packingCustomerError)}</div></section><section class="work-card packing-details-card"><div class="work-head"><div><h2>${t('cartonDetails')}</h2></div></div><div class="table-wrap"><table class="table packing-details-table"><thead><tr><th>${t('sku')}</th><th>${t('unitsPerCarton')}</th><th>${t('cartonCount')}</th></tr></thead><tbody>${packingCartons.map(item=>`<tr><td>${esc(item.sku)}</td><td>${item.unitsPerCarton}</td><td>${item.cartons}</td></tr>`).join('')}</tbody></table></div></section><div class="bottom-bar"><div class="packing-actions"><button type="button" class="btn secondary" onclick="navigate('workbench')">${t('backToOrderList')}</button><button type="button" class="btn primary" onclick="confirmPackingInbound()">${t('confirmInbound')}</button></div></div>`,t('packing'),t('packingHint'),'workbench');}
function focusPackingScan(){window.requestAnimationFrame(()=>document.getElementById('packing-scan')?.focus());}
function focusPackingCustomerCode(){window.requestAnimationFrame(()=>document.getElementById('packing-customer-code')?.focus());}
function handlePackingScan(){const value=(document.getElementById('packing-scan')?.value||'').trim();if(!value){state.packingMessage={type:'error',text:t('required')};render();focusPackingScan();return}const normalized=value.toLowerCase();const item=packingCartons.find(row=>row.sku.toLowerCase()===normalized||row.boxNo.toLowerCase()===normalized);state.packingMessage=item?{type:'good',text:`${t('scanSuccess')}: ${item.sku}`}:{type:'error',text:t('unknownScan')};render();focusPackingScan();}
function confirmPackingInbound(){if(!state.packingCustomerCode.trim()){state.packingCustomerError=t('customerCodeRequired');state.packingInboundDone=false;render();focusPackingCustomerCode();return}state.packingInboundDone=true;state.packingMessage=null;state.packingCustomerCode='';state.packingCustomerError='';state.page='workbench';render();toast(t('packingInboundDone'),'good');}
function relabelView(){return layout(`<div class="toolbar"><span class="subtle">${plans.length} ${t('orderList')}</span></div><div class="list">${plans.map(p=>{const done=Boolean(state.relabelDoneOrders[p.id]);return `<article class="list-card"><div class="list-top"><div><div class="list-title">${esc(p.id)}</div><div class="list-meta">${t('container')}: ${esc(p.container)} · ${esc(p.customer)}</div></div><span class="status ${done?'good':'wait'}">${done?t('complete'):t('pending')}</span></div><div class="list-grid"><div><span>${t('inboundCode')}</span><strong>${esc(p.inboundCode||p.tickets)}</strong></div><div><span>${t('palletCount')}</span><strong>${p.expectedPallets} ${t('palletCount')}</strong></div><div><span>${t('relabelStatus')}</span><strong>${done?t('relabelDone'):t('pending')}</strong></div></div><div class="card-actions"><button class="btn ${done?'ghost':'primary'}" onclick="openRelabelPhoto('${p.id}')" ${done?'disabled':''}>${done?t('complete'):t('submitRelabel')}</button></div></article>`}).join('')}</div>`,t('relabelTitle'),t('relabelHint'),'workbench');}
function openRelabelPhoto(planId){if(!plans.some(p=>p.id===planId))return;state.selectedRelabelId=planId;state.page='relabel-photo';render();}
function selectedRelabelOrder(){return plans.find(p=>p.id===state.selectedRelabelId);}
function relabelPhotoView(){const p=selectedRelabelOrder();if(!p)return relabelView();const photo=state.relabelPhotos[p.id];return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('relabelPhotoTitle')}</h2><p>${esc(p.id)} · ${esc(p.customer)}</p></div><span class="status wait">${t('pending')}</span></div><div class="detail-list"><div class="detail-row"><span>${t('container')}</span><strong>${esc(p.container)}</strong></div><div class="detail-row"><span>${t('inboundCode')}</span><strong>${esc(p.inboundCode||p.tickets)}</strong></div><div class="detail-row"><span>${t('palletCount')}</span><strong>${p.expectedPallets} ${t('palletUnit')}</strong></div></div></section><section class="work-card"><div class="photo-step ${photo?'done':''}"><div class="photo-preview" style="height:180px">${photo?.startsWith('data:')?`<img src="${photo}" alt="${t('relabelPhotoTitle')}">`:icon('camera')}</div><strong>${t('relabelPhotoHint')}</strong><label>${photo?t('retake'):t('takePhoto')}<input type="file" accept="image/*" capture="environment" onchange="handleRelabelPhoto(this)"></label></div></section><div class="bottom-bar"><button class="btn primary" onclick="submitRelabelPhoto()" ${!photo?'disabled':''}>${t('submitRelabelPhoto')}</button></div>`,t('relabelPhotoTitle'),t('relabelPhotoHint'),'relabel');}
function handleRelabelPhoto(input){const order=selectedRelabelOrder();const file=input.files&&input.files[0];if(!order||!file)return;const reader=new FileReader();reader.onload=()=>{state.relabelPhotos[order.id]=reader.result;render()};reader.onerror=()=>toast(t('relabelPhotoRequired'),'bad');reader.readAsDataURL(file);}
function submitRelabelPhoto(){const order=selectedRelabelOrder();if(!order){state.page='relabel';render();return}if(!state.relabelPhotos[order.id]){toast(t('relabelPhotoRequired'),'bad');return}state.relabelDoneOrders[order.id]=true;state.relabelMessage={type:'good',text:`${t('relabelDone')}: ${order.id}`};state.page='relabel';toast(state.relabelMessage.text,'good');render();}
function exceptionView(){return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('exceptionTitle')}</h2></div><span class="status wait">${t('pending')}</span></div><div class="scan-box"><input id="exception-scan" placeholder="${t('scanBox')}" onkeydown="if(event.key==='Enter')handleExceptionScan()"><button onclick="handleExceptionScan()" aria-label="${t('scan')}">${icon('scan')}</button></div><div class="field"><label>${t('exceptionRef')}</label><input id="exception-ref" value="${esc(state.exceptionRefValue||'')}" placeholder="OUT-260819-004 / FBA-ONT8-260819-004 / D2222-TC-001"></div><div class="field"><label>${t('exceptionType')}</label><select id="exception-type"><option value="">${t('selectException')}</option><option value="damage">${t('damaged')}</option><option value="short">${t('short')}</option><option value="hold">${t('hold')}</option></select></div><div class="field"><label>${t('exceptionDesc')}</label><input id="exception-desc" maxlength="120" placeholder="${t('exceptionDesc')}"></div>${state.exceptionMessage?`<div class="scan-result ${state.exceptionMessage.type}">${esc(state.exceptionMessage.text)}</div>`:''}</section><div class="bottom-bar"><button class="btn primary" onclick="submitException()">${t('submitException')}</button></div>`,t('exceptionTitle'),t('exceptionDesc'),'workbench');}
function handleExceptionScan(){const value=(document.getElementById('exception-scan')?.value||'').trim();if(!value){state.exceptionMessage={type:'error',text:t('required')};render();return}state.exceptionRefValue=value;state.exceptionMessage={type:'good',text:`${t('scanSuccess')}: ${value}`};render();}
function submitException(){const type=(document.getElementById('exception-type')?.value||'').trim();const ref=(document.getElementById('exception-ref')?.value||'').trim();const desc=(document.getElementById('exception-desc')?.value||'').trim();if(!type||!ref||!desc){state.exceptionMessage={type:'error',text:t('required')};render();return}state.exceptionRefValue=ref;state.exceptionMessage={type:'good',text:`${t('exceptionDone')}: ${ref}`};render();}
function inventoryView(){const q=state.inventoryQuery.toLowerCase();const rows=inventory.filter(x=>!q||[x.palletNo,x.name].some(v=>String(v).toLowerCase().includes(q)));return layout(`<div class="search-row"><input id="inventory-search" value="${esc(state.inventoryQuery)}" aria-label="${t('inventory')}" placeholder="${t('queryHint')}" onkeydown="if(event.key==='Enter')searchInventory()"><button type="button" onclick="searchInventory()">${t('search')}</button></div>${q?`<div class="card-actions" style="margin-bottom:10px"><button type="button" class="btn ghost" onclick="state.inventoryQuery='';render()">${t('clear')}</button></div>`:''}${rows.length?`<div class="inventory-results">${rows.map(x=>`<article class="inventory-card"><div class="inventory-card-head"><div><div class="inventory-card-title">${esc(x.name)}</div><div class="inventory-card-customer">${esc(x.customer)}</div></div><span class="status ${x.status==='在库'?'good':'wait'}">${esc(x.status==='在库'?t('inStock'):t('inTransit'))}</span></div><div class="inventory-facts"><div class="inventory-fact"><span>${t('palletNo')}</span><strong>${esc(x.palletNo)}</strong></div><div class="inventory-fact qty"><span>${t('qty')}</span><strong>${x.qty}</strong></div></div></article>`).join('')}</div>`:`<div class="empty">${t('noResult')}</div>`}`,t('inventory'),t('inventoryDesc'),'workbench');}
function searchInventory(){state.inventoryQuery=(document.getElementById('inventory-search')?.value||'').trim();render();}
function profileView(){return layout(`<section class="work-card"><div class="profile-grid"><div class="profile-line"><span>${t('operator')}</span><strong>${esc(state.user?.name)}</strong></div><div class="profile-line"><span>${t('role')}</span><strong>${esc(state.user?.role)}</strong></div><div class="profile-line"><span>${t('warehouse')}</span><strong>${esc(state.user?.warehouse)}</strong></div><div class="profile-line"><span>${t('shiftLabel')}</span><strong>${t('shift')}</strong></div><div class="profile-line"><span>${t('language')}</span><div class="lang-select">${[['zh','中文'],['en','English'],['es','Español']].map(([v,l])=>`<button type="button" class="${state.lang===v?'active':''}" aria-pressed="${state.lang===v}" onclick="setLang('${v}')">${l}</button>`).join('')}</div></div><div class="profile-line"><span>${t('version')}</span><strong>V1.0.0</strong></div></div></section><div class="bottom-bar"><button type="button" class="btn danger" onclick="logout()">${t('logout')}</button></div>`,t('profile'),t('sub'),'workbench');}
let modalReturnFocus=null;
function showModal(message,onConfirm){modalReturnFocus=document.activeElement;const root=document.getElementById('modal-root');root.innerHTML=`<div class="modal-backdrop"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title"><h2 id="modal-title">${t('details')}</h2><p>${esc(message)}</p><div class="modal-actions"><button type="button" class="btn ghost" onclick="closeModal()">${t('cancel')}</button><button type="button" class="btn primary" id="modal-confirm">${t('yes')}</button></div></div></div>`;const confirm=document.getElementById('modal-confirm');confirm.onclick=onConfirm;root.onkeydown=event=>{if(event.key==='Escape')closeModal()};requestAnimationFrame(()=>confirm.focus());}
function closeModal(){const root=document.getElementById('modal-root');root.innerHTML='';root.onkeydown=null;if(modalReturnFocus?.focus)modalReturnFocus.focus();modalReturnFocus=null;}
let toastTimer;function toast(message,type='info'){const root=document.getElementById('toast-root');root.innerHTML=`<div class="toast ${type}" role="status" aria-live="polite">${esc(message)}</div>`;clearTimeout(toastTimer);toastTimer=setTimeout(()=>root.innerHTML='',2600);}
function palletPhotoKey(palletId){return palletId||state.pallet?.id||`DRAFT-${state.selectedPlanId||'pallet'}`;}
function palletPhotoItems(palletId){return state.palletPhotos[palletPhotoKey(palletId)]||[];}
function openPalletPhotos(palletId,planId){if(planId)state.selectedPlanId=planId;state.photoPalletId=palletId;state.page='photo-detail';render();}
function handlePalletPhoto(input){const file=input.files&&input.files[0];if(!file)return;const key=palletPhotoKey(state.photoPalletId||state.pallet?.id);const reader=new FileReader();reader.onload=()=>{state.palletPhotos[key]??=[];state.palletPhotos[key].push(reader.result);state.photoPalletId=key;render()};reader.readAsDataURL(file);}
function photoCaptureButton(){return `<label class="btn primary photo-capture">${icon('camera')}<span>${t('takePhoto')}</span><input type="file" accept="image/*" capture="environment" onchange="handlePalletPhoto(this)"></label>`;}
function photoListMarkup(palletId){const photos=palletPhotoItems(palletId);return photos.length?`<div class="photo-grid">${photos.map((src,index)=>`<div class="photo-step done"><div class="photo-preview"><img src="${src}" alt="${t('photoItem')} ${index+1}"></div><strong>${t('photoItem')} ${index+1}</strong></div>`).join('')}</div>`:`<div class="empty">${t('noPhotos')}</div>`;}
function photoDetailView(){const key=state.photoPalletId||palletPhotoKey();const saved=state.pallets.find(x=>x.id===key);const plan=plans.find(x=>x.id===saved?.planId)||selectedPlan();return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('photoDetails')}</h2><p>${esc(key)} · ${esc(saved?.destinationCode||plan?.destinationCode||'—')}</p></div><span class="status blue">${palletPhotoItems(key).length}</span></div>${photoListMarkup(key)}<div class="card-actions">${photoCaptureButton()}</div></section>`,t('photoDetails'),t('palletPhotoHint'),'pallet-list');}
function injectPalletListPhotoButtons(){if(state.page!=='pallet-list')return;const consolidating=state.palletMode==='consolidate';const activePlans=consolidating?plans:[selectedPlan()];const slots=activePlans.flatMap(plan=>Array.from({length:plan.expectedPallets},(_,index)=>({plan,no:plannedPalletNo(plan,index)})));document.querySelectorAll('.list-card').forEach((card,index)=>{const slot=slots[index];const actions=card.querySelector('.card-actions');if(!slot||!actions||actions.querySelector('.photo-details-btn'))return;const button=document.createElement('button');button.className='btn ghost photo-details-btn';button.textContent=t('photoDetails');button.onclick=()=>openPalletPhotos(slot.no,slot.plan.id);actions.prepend(button);});}
function injectPalletPhotoFeatures(){if(state.page!=='pallet'||!state.pallet||state.palletOrderType!=='overseas')return;const root=document.querySelector('.content');if(!root||root.querySelector('.pallet-site-photos'))return;const key=palletPhotoKey();const section=document.createElement('section');section.className='work-card pallet-site-photos';section.innerHTML=`<div class="work-head"><div><h2>${t('sitePhotos')}</h2><p>${t('palletPhotoHint')}</p></div><span class="status blue">${palletPhotoItems(key).length}</span></div>${photoListMarkup(key)}<div class="card-actions">${photoCaptureButton()}</div>`;const bottom=root.querySelector('.bottom-bar');if(bottom)root.insertBefore(section,bottom);else root.appendChild(section);}
function internalEntriesMarkup(){return `<button class="entry packing-entry" onclick="navigate('packing')"><div class="entry-icon">${icon('packing')}</div><h3>${t('packing')}</h3><p>${t('packingHint')}</p></button><button class="entry relabel-entry" onclick="navigate('relabel')"><div class="entry-icon">${icon('relabel')}</div><h3>${t('relabel')}</h3><p>${t('relabelHint')}</p></button><button class="entry exception-entry" onclick="navigate('exception')"><div class="entry-icon">${icon('exception')}</div><h3>${t('exception')}</h3><p>${t('exceptionDesc')}</p></button>`;}
function injectReservedEntries(){if(state.page!=='workbench')return;const grids=document.querySelectorAll('.entry-grid');const grid=grids[grids.length-1];if(!grid)return;const more=grid.querySelectorAll('.entry')[1];if(more)more.style.display='none';grid.insertAdjacentHTML('beforeend',internalEntriesMarkup());}
function injectInboundPalletUnits(){if(state.page!=='workbench')return;document.querySelectorAll('.task-strip .task-main p').forEach((el,index)=>{const p=plans.filter(x=>x.status==='待卸货')[index];if(p)el.textContent=`${p.customer} · ${p.destinationCode} · ${p.expectedPallets} ${t('palletCount')}`});}
function injectOutboundEntryUnits(){if(state.page!=='workbench')return;document.querySelectorAll('.entry-grid')[1]?.classList.add('outbound-entry-grid');}
function injectInternalEntryUnits(){if(state.page!=='workbench')return;document.querySelectorAll('.entry-grid')[2]?.classList.add('internal-entry-grid');}
function injectWorkbenchCleanup(){if(state.page!=='workbench')return;document.querySelectorAll('.section-title > span').forEach(span=>span.remove());document.querySelector('.task-strip')?.remove();document.querySelectorAll('.entry .badge').forEach(badge=>badge.remove());}
function injectPalletProgress(){return;}
function injectReceiveControls(){if(state.page!=='receive')return;document.querySelectorAll('.work-card:nth-of-type(2) .task-row').forEach((row,index)=>{const item=cargo.filter(c=>c.planNo===selectedPlan().id&&c.received)[index];if(!item||row.querySelector('select'))return;const select=document.createElement('select');select.className='chip';select.innerHTML=[['normal','正常'],['short','短少'],['damaged','破损'],['hold','待处理']].map(([v,l])=>`<option value="${v}" ${state.receiveStatus[item.id]===v?'selected':''}>${t(v)||l}</option>`).join('');select.onchange=()=>{state.receiveStatus[item.id]=select.value;render()};row.appendChild(select)});}
function injectMergeColumn(){if(state.page!=='pallet'||!state.pallet?.items.length||state.palletMode==='consolidate'||state.palletOrderType!=='overseas')return;const table=document.querySelector('.table');if(!table)return;const head=table.querySelector('thead tr');const headers=head?.querySelectorAll('th');if(head&&headers?.length){const th=document.createElement('th');th.textContent=t('mergeFlag');head.insertBefore(th,headers[headers.length-1])}table.querySelectorAll('tbody tr').forEach((row,index)=>{const cells=row.querySelectorAll('td');const item=state.pallet.items[index];if(!cells.length||!item)return;const td=document.createElement('td');td.innerHTML=`<span class="status ${item.isMerged?'blue':'wait'}">${item.isMerged?t('yesMerge'):t('noMerge')}</span>`;row.insertBefore(td,cells[cells.length-1])});}
function injectPickDetails(){if(state.page!=='pick')return;const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];const list=document.querySelector('.work-card .detail-list');if(!task||!list)return;list.innerHTML=`<div class="detail-row"><span>${t('outboundOrderNo')}</span><strong>${esc(task.id)}</strong></div><div class="detail-row"><span>${t('inboundCode')}</span><strong>${esc(task.inboundCode)}</strong></div><div class="detail-row"><span>${t('plateNo')}</span><strong>${esc(task.plateNo)}</strong></div><div class="detail-row"><span>${t('driver')}</span><strong>${esc(task.driver)}</strong></div><div class="detail-row"><span>${t('outboundTime')}</span><strong>${esc(task.outboundTime)}</strong></div><div class="detail-row"><span>${t('totalPalletBoxes')}</span><strong>${task.totalPallets} ${t('palletCount')} / ${task.boxes} ${t('boxes')}</strong></div><div class="detail-row"><span>${t('linkedOrder')}</span><strong>${esc(task.orderInfo)}</strong></div>`;}
function injectPalletDetailColumns(){if(state.page!=='pallet'||!state.pallet?.items.length||state.palletMode==='consolidate'||state.palletOrderType!=='overseas')return;const table=document.querySelector('.table');if(!table)return;table.style.minWidth='560px';table.innerHTML=`<thead><tr><th>${t('boxMark')}</th><th>${t('inboundCode')}</th><th>${t('productName')}</th><th>${t('remove')}</th></tr></thead><tbody>${state.pallet.items.map((item,index)=>{const plan=plans.find(x=>x.id===item.planNo);return `<tr><td>${esc(item.boxNo)}</td><td>${esc(plan?.inboundCode||'—')}</td><td>${esc(item.name||'—')}</td><td><button class="btn danger" style="min-height:44px;padding:0 8px" onclick="removePalletItem(${index})">${t('remove')}</button></td></tr>`}).join('')}</tbody>`;}
function injectPalletReadonly(){if(state.page!=='pallet'||!state.pallet?.persisted)return;document.querySelector('.scan-box')?.remove();document.querySelector('.bottom-bar')?.remove();document.querySelectorAll('.fba-pallet-choice').forEach(button=>{button.disabled=true;button.setAttribute('aria-disabled','true')});}
function injectPalletNavigation(){if(state.page!=='pallet'||state.pallet?.persisted||state.palletMode==='consolidate')return;const button=document.querySelector('.bottom-bar .btn.ghost');if(!button)return;button.textContent=t('back');button.onclick=()=>navigate('plans');}
function palletLabelOption(plan,no){const saved=state.pallets.find(x=>x.id===no);const active=state.pallet?.id===no;const occupied=Boolean(saved||(active&&state.pallet.items.length));const destination=saved?.destinationCode||plan.destinationCode||'—';return `<option value="${esc(plan.id)}|${esc(no)}" ${active?'selected':''}>${esc(no)} · ${esc(destination)} · ${t(occupied?'occupied':'idle')}</option>`;}
function injectPalletLabelSelector(){if(state.page!=='pallet'||!state.pallet||state.palletMode==='consolidate'||state.palletOrderType!=='overseas')return;const plansForLabels=state.palletMode==='consolidate'?plans:[selectedPlan()];const labels=plansForLabels.flatMap(plan=>Array.from({length:plan.expectedPallets},(_,index)=>palletLabelOption(plan,plannedPalletNo(plan,index))));const cards=document.querySelectorAll('.work-card');const details=cards[1];if(!details)return;const section=document.createElement('section');section.className='work-card pallet-label-selector';section.innerHTML=`<div class="work-head"><div><h2>${t('selectPalletLabel')}</h2><p>${t('palletLabel')} · ${t('palletStatus')}</p></div></div><div class="field" style="margin:12px 0 0"><label for="pallet-label-select">${t('palletLabel')}</label><select id="pallet-label-select" onchange="handlePalletLabelChange(this)">${labels.join('')}</select></div>`;details.insertAdjacentElement('afterend',section);}
function handlePalletLabelChange(select){const [planId,palletId]=String(select.value).split('|');selectPalletLabel(palletId,planId);}
function selectPalletLabel(palletId,planId){if(state.pallet?.id===palletId)return;const switchLabel=()=>{state.pallet=null;closeModal();selectPallet(palletId,planId)};if(state.pallet?.items.length&&!state.pallet.persisted){showModal(t('exitConfirm'),switchLabel);return}switchLabel();}
function injectIconReplacements(){document.querySelectorAll('.scan-box button').forEach(button=>{button.innerHTML=icon('scan');});}
function injectPalletScanModal(){if(state.page!=='pallet'||state.palletMode==='consolidate')return;const input=document.getElementById('pallet-scan');if(state.palletOrderType==='overseas'&&input&&!input.dataset.quantityModal){input.dataset.quantityModal='true';input.addEventListener('click',openPalletScanModal);}}
function render(){const app=document.getElementById('app');if(!state.user&&state.page!=='login')state.page='login';app.dataset.page=state.page;let view=state.page==='login'?loginView():state.page==='workbench'?workbenchView():state.page==='profile'?profileView():state.page==='plans'?plansPalletView():state.page==='receive-list'?receiveListView():state.page==='receive'?receivePhotoView():state.page==='pallet-list'?palletListView():state.page==='pallet'?palletView():state.page==='pick'?pickView():state.page==='review'?reviewView():state.page==='handover'?handoverView():state.page==='packing'?packingView():state.page==='express-signin'?expressSignInView():state.page==='express-signin-detail'?expressSignInDetailView():state.page==='relabel'?relabelView():state.page==='relabel-photo'?relabelPhotoView():state.page==='relabel-box'?relabelBoxView():state.page==='relabel-sku'?relabelSkuView():state.page==='exception'?exceptionView():state.page==='inventory'?inventoryView():loginView();app.innerHTML=view;injectReservedEntries();injectInboundPalletUnits();injectOutboundEntryUnits();injectInternalEntryUnits();injectWorkbenchCleanup();injectPalletProgress();injectPalletDetailColumns();injectMergeColumn();injectPickDetails();injectPalletLabelSelector();injectPalletNavigation();injectPalletReadonly();injectIconReplacements();injectPalletScanModal();document.documentElement.lang=state.lang==='zh'?'zh-CN':state.lang==='es'?'es':'en';}
WORK_LABELS.zh.directEnter='拆托';WORK_LABELS.en.directEnter='Depalletize';WORK_LABELS.es.directEnter='Despaletizar';
Object.assign(WORK_LABELS.zh,{palletOrderType:'页面类型',fbaWarehouse:'FBA仓',overseasWarehouse:'海外仓',oneClickDropship:'一键代发',selectPalletLabels:'选择托盘标(多选)',selectAtLeastOnePallet:'请至少选择一个托盘标'});Object.assign(WORK_LABELS.en,{palletOrderType:'Page type',fbaWarehouse:'FBA',overseasWarehouse:'Overseas warehouse',oneClickDropship:'One-click fulfillment',selectPalletLabels:'Select pallet labels (multiple)',selectAtLeastOnePallet:'Select at least one pallet label'});Object.assign(WORK_LABELS.es,{palletOrderType:'Tipo de página',fbaWarehouse:'FBA',overseasWarehouse:'Almacén exterior',oneClickDropship:'Envío con un clic',selectPalletLabels:'Seleccionar etiquetas (múltiple)',selectAtLeastOnePallet:'Selecciona al menos una etiqueta'});
Object.assign(WORK_LABELS.zh,{cargoInfo:'货物信息'});Object.assign(WORK_LABELS.en,{cargoInfo:'Cargo information'});Object.assign(WORK_LABELS.es,{cargoInfo:'Información de carga'});
Object.assign(WORK_LABELS.zh,{weight:'重量',newGeneratedPallet:'新生成托盘标',generatePalletLabel:'生成托盘标',palletizeFooter:'打托',scanCartonQty:'箱数',inputCartonQty:'输入箱数',remainingSkuCartons:'该 SKU 剩余箱数'});Object.assign(WORK_LABELS.en,{weight:'Weight',newGeneratedPallet:'Generate pallet label',generatePalletLabel:'Generate pallet label',palletizeFooter:'Palletize',scanCartonQty:'Cartons',inputCartonQty:'Enter carton quantity',remainingSkuCartons:'Remaining cartons for SKU'});Object.assign(WORK_LABELS.es,{weight:'Peso',newGeneratedPallet:'Generar etiqueta de pallet',generatePalletLabel:'Generar etiqueta de pallet',palletizeFooter:'Paletizar',scanCartonQty:'Cajas',inputCartonQty:'Introducir cantidad de cajas',remainingSkuCartons:'Cajas restantes del SKU'});
Object.assign(EXTRA_LABELS.zh,{vehicleType:'车型',vehicleTypeValue:'大车',reviewPallets:'托数',palletReferenceValue:'30托',reviewPalletProgress:'托数进度',reviewScanPallet:'扫描托盘标'});
Object.assign(EXTRA_LABELS.en,{vehicleType:'Vehicle type',vehicleTypeValue:'Large truck',reviewPallets:'Pallets',palletReferenceValue:'30 pallets',reviewPalletProgress:'Pallet progress',reviewScanPallet:'Scan pallet label'});
Object.assign(EXTRA_LABELS.es,{vehicleType:'Tipo de vehículo',vehicleTypeValue:'Camión grande',reviewPallets:'Pallets',palletReferenceValue:'30 pallets',reviewPalletProgress:'Progreso de pallets',reviewScanPallet:'Escanea la etiqueta del pallet'});
Object.assign(EXTRA_LABELS.zh,{skipReasonTitle:'跳拣原因',skipReasonNotFound:'未找到其他货物',skipReasonOutOfStock:'缺货',skipReasonOther:'其他'});
Object.assign(EXTRA_LABELS.en,{skipReasonTitle:'Skip reason',skipReasonNotFound:'No other freight found',skipReasonOutOfStock:'Out of stock',skipReasonOther:'Other'});
Object.assign(EXTRA_LABELS.es,{skipReasonTitle:'Motivo de omisión',skipReasonNotFound:'No se encontró otra carga',skipReasonOutOfStock:'Sin stock',skipReasonOther:'Otro'});
Object.assign(EXTRA_LABELS.zh,{photoDetails:'照片明细',sitePhotos:'现场照片',photoItem:'照片',noPhotos:'暂无照片',palletPhotoHint:'查看或拍摄托盘现场照片'});
Object.assign(EXTRA_LABELS.en,{photoDetails:'Photo details',sitePhotos:'Site photos',photoItem:'Photo',noPhotos:'No photos yet',palletPhotoHint:'View or capture pallet site photos'});
Object.assign(EXTRA_LABELS.es,{photoDetails:'Detalles de fotos',sitePhotos:'Fotos en sitio',photoItem:'Foto',noPhotos:'Aún no hay fotos',palletPhotoHint:'Ver o capturar fotos del pallet'});
Object.assign(EXTRA_LABELS.zh,{newConsolidation:'新增合托',noConsolidationPallets:'暂无合托托盘',reconsolidate:'重新合托',submitConsolidation:'提交合托',consolidationUpdated:'合托已更新'});
Object.assign(EXTRA_LABELS.en,{newConsolidation:'New consolidation',noConsolidationPallets:'No consolidated pallets',reconsolidate:'Re-consolidate',submitConsolidation:'Submit consolidation',consolidationUpdated:'Consolidation updated'});
Object.assign(EXTRA_LABELS.es,{newConsolidation:'Nueva consolidación',noConsolidationPallets:'No hay pallets consolidados',reconsolidate:'Reconsolidar',submitConsolidation:'Enviar consolidación',consolidationUpdated:'Consolidación actualizada'});
Object.assign(EXTRA_LABELS.zh,{consolidationItems:'合托明细',scanFreight:'扫描货物',destinationWarehouseShort:'目的仓'});
Object.assign(EXTRA_LABELS.en,{consolidationItems:'Consolidation details',scanFreight:'Scan freight',destinationWarehouseShort:'Destination WH'});
Object.assign(EXTRA_LABELS.es,{consolidationItems:'Detalles de consolidación',scanFreight:'Escanear carga',destinationWarehouseShort:'Almacén destino'});
Object.assign(EXTRA_LABELS.zh,{palletUnit:'托',pieceUnit:'件',orderCount:'{count} 个订单'});
Object.assign(EXTRA_LABELS.en,{palletUnit:'pallets',pieceUnit:'pcs',orderCount:'{count} orders'});
Object.assign(EXTRA_LABELS.es,{palletUnit:'pallets',pieceUnit:'uds.',orderCount:'{count} pedidos'});
Object.assign(EXTRA_LABELS.zh,{receivePalletBoxes:'托数/箱数',other:'其他'});
Object.assign(EXTRA_LABELS.en,{receivePalletBoxes:'Pallets/Cartons',other:'Other'});
Object.assign(EXTRA_LABELS.es,{receivePalletBoxes:'Pallets/Cajas',other:'Otro'});
Object.assign(EXTRA_LABELS.zh,{relabelPhotoTitle:'换标拍照',relabelPhotoHint:'拍摄换标完成后的现场照片',submitRelabelPhoto:'提交照片',relabelPhotoRequired:'请先完成换标拍照'});
Object.assign(EXTRA_LABELS.en,{relabelPhotoTitle:'Relabel photo',relabelPhotoHint:'Capture the completed relabeling',submitRelabelPhoto:'Submit photo',relabelPhotoRequired:'Take a relabel photo before submitting'});
Object.assign(EXTRA_LABELS.es,{relabelPhotoTitle:'Foto de reetiquetado',relabelPhotoHint:'Captura el reetiquetado terminado',submitRelabelPhoto:'Enviar foto',relabelPhotoRequired:'Toma una foto antes de enviar'});
const PAGE_TITLES={
zh:{'receive-list':'待卸货集装箱',receive:'拆柜收货',plans:'入库打托','pallet-list':'托盘列表',pallet:'入库打托',pick:'出库拣货',handover:'出库交接',inventory:'库存查询',packing:'拣货装箱',relabel:'换标作业','relabel-photo':'换标拍照',exception:'异常处理',profile:'个人中心','photo-detail':'照片明细'},
en:{'receive-list':'Unloading',receive:'Receiving',plans:'Palletizing','pallet-list':'Pallets',pallet:'Pallet',pick:'Picking',handover:'Handover',inventory:'Inventory',packing:'Pick & pack',relabel:'Relabeling','relabel-photo':'Relabel photo',exception:'Exceptions',profile:'Profile','photo-detail':'Photos'},
es:{'receive-list':'Descarga',receive:'Recepción',plans:'Paletizado','pallet-list':'Pallets',pallet:'Pallet',pick:'Picking',handover:'Entrega',inventory:'Inventario',packing:'Picking y embalaje',relabel:'Reetiquetado','relabel-photo':'Foto de reetiquetado',exception:'Excepciones',profile:'Perfil','photo-detail':'Fotos'}
};
function compactPageTitle(title){if(state.page==='pallet'&&state.palletMode==='consolidate'&&state.pallet)return t('mergeOnly');return PAGE_TITLES[state.lang]?.[state.page]||title;}
const basePalletListView=palletListView;
palletListView=function(){const consolidating=state.palletMode==='consolidate';if(!consolidating)return basePalletListView();const created=state.pallets.filter(pallet=>pallet.mode==='consolidate'&&pallet.items?.length>0);return layout(`<div class="toolbar"><span class="subtle">${created.length} ${t('palletCount')}</span></div>${created.length?`<div class="list">${created.map(pallet=>{const sourcePlan=plans.find(plan=>plan.id===pallet.items[0]?.planNo);return `<article class="list-card"><div class="list-top"><div><div class="list-title">${esc(pallet.id)}</div><div class="list-meta">${esc(sourcePlan?.id||'—')} · ${esc(pallet.container||sourcePlan?.container||'—')}</div></div></div><div class="list-grid"><div><span>${t('destination')}</span><strong>${esc(pallet.destinationCode||'—')}</strong></div><div><span>${t('cartonCount')}</span><strong>${pallet.items.length}</strong></div><div><span>${t('weight')}</span><strong>${palletWeight(pallet).toFixed(1)} kg</strong></div></div><div class="card-actions"><button class="btn ghost" style="margin-right:auto" onclick="openPalletPhotos('${esc(pallet.id)}','${esc(sourcePlan?.id||'')}')">${t('photoDetails')}</button><button class="btn primary" onclick="reconsolidatePallet('${esc(pallet.id)}')">${t('reconsolidate')}</button></div></article>`}).join('')}</div>`:`<div class="empty">${t('noConsolidationPallets')}</div>`}<div class="bottom-bar"><button class="btn primary" onclick="startNewConsolidation()">${t('newConsolidation')}</button></div>`,t('mergeOnly'),t('mergeDesc'),'workbench');};
function startNewConsolidation(){state.palletMode='consolidate';state.palletMessage=null;state.photoPalletId='';resetPalletTypeInputs();const slot=plans.flatMap(plan=>Array.from({length:plan.expectedPallets},(_,index)=>({plan,no:plannedPalletNo(plan,index)}))).find(entry=>!state.pallets.some(pallet=>pallet.id===entry.no));if(!slot){state.pallet=null;startPallet();return}state.selectedPlanId=slot.plan.id;state.pallet={id:slot.no,mode:'consolidate',planId:null,targetPallets:null,destinationCode:'',destinationAddress:'',items:[]};state.page='pallet';render();}
function reconsolidatePallet(palletId){const saved=state.pallets.find(pallet=>pallet.id===palletId&&pallet.mode==='consolidate');if(!saved)return;state.palletMode='consolidate';resetPalletTypeInputs();state.pallet={...JSON.parse(JSON.stringify(saved)),originalPalletId:palletId};state.palletMessage=null;state.photoPalletId='';state.page='pallet';render();}
function submitConsolidation(){if(!state.pallet?.items.length){state.palletMessage={type:'error',text:t('emptyPallet')};render();return}const originalPalletId=state.pallet.originalPalletId;const originalIndex=state.pallets.findIndex(pallet=>pallet.id===originalPalletId);if(originalIndex<0){submitPallet();return}const updated=JSON.parse(JSON.stringify(state.pallet));delete updated.originalPalletId;delete updated.persisted;state.pallets[originalIndex]=updated;updated.items.forEach(item=>{const source=cargo.find(c=>c.id===item.id);if(source)source.palletNo=updated.id});state.pallet=null;state.palletMessage=null;toast(t('consolidationUpdated'),'good');state.page='pallet-list';render();}
photoCaptureButton=function(id='pallet-photo-capture-input'){return `<button type="button" class="btn primary photo-capture" onclick="document.getElementById('${id}').click()" style="display:flex;align-items:center;justify-content:center;gap:7px;white-space:nowrap">${icon('camera')}<span>${t('takePhoto')}</span></button><input id="${id}" type="file" accept="image/*" capture="environment" onchange="handlePalletPhoto(this)" style="display:none">`;};
photoAddButton=function(){const id='pallet-site-photo-input';return `<button type="button" class="photo-add-tile" onclick="document.getElementById('${id}').click()" aria-label="${t('takePhoto')}" style="display:grid;place-items:center;width:76px;height:76px;border:1px solid #d0d5dd;border-radius:10px;background:#fff;color:#98a2b3;font-size:32px;line-height:1;cursor:pointer"><span aria-hidden="true">+</span></button><input id="${id}" type="file" accept="image/*" capture="environment" onchange="handlePalletPhoto(this)" style="display:none">`;};
photoDetailView=function(){const key=state.photoPalletId||palletPhotoKey();const saved=state.pallets.find(x=>x.id===key);const plan=plans.find(x=>x.id===saved?.planId)||selectedPlan();return layout(`<section class="work-card"><div class="work-head"><div><h2>${t('photoDetails')}</h2><p>${esc(key)} · ${esc(saved?.destinationCode||plan?.destinationCode||'—')}</p></div><span class="status blue">${palletPhotoItems(key).length}</span></div>${photoListMarkup(key)}</section><div class="bottom-bar">${photoCaptureButton()}</div>`,t('photoDetails'),t('palletPhotoHint'),'pallet-list');};
injectPalletPhotoFeatures=function(){if(state.page!=='pallet'||!state.pallet||state.palletOrderType!=='overseas')return;const root=document.querySelector('.content');if(!root||root.querySelector('.pallet-site-photos'))return;const key=palletPhotoKey();const photos=palletPhotoItems(key);const photoTiles=photos.length?`<div class="photo-grid">${photos.map((src,index)=>`<div class="photo-step done"><div class="photo-preview"><img src="${src}" alt="${t('photoItem')} ${index+1}"></div><strong>${t('photoItem')} ${index+1}</strong></div>`).join('')}</div>`:'';const section=document.createElement('section');section.className='work-card pallet-site-photos';section.innerHTML=`<div class="work-head"><div><h2>${t('sitePhotos')}</h2><p>${t('palletPhotoHint')}</p></div><span class="status blue">${photos.length}</span></div>${photoTiles}<div style="margin-top:12px">${photoAddButton()}</div>`;const bottom=root.querySelector('.bottom-bar');if(bottom)root.insertBefore(section,bottom);else root.appendChild(section);};
injectPalletListPhotoButtons=function(){if(state.page!=='pallet-list')return;document.querySelectorAll('.list-card .card-actions').forEach(actions=>{actions.querySelector('.btn.ghost')?.style.setProperty('margin-right','auto');});};
function injectReconsolidationSubmit(){if(state.page!=='pallet'||!state.pallet?.originalPalletId)return;const button=document.querySelector('.bottom-bar .btn.primary');if(!button)return;button.textContent=t('submitConsolidation');button.disabled=!state.pallet.items.length;button.onclick=submitConsolidation;}
function injectCurrentPalletCleanup(){if(state.page!=='pallet'||!state.pallet)return;document.querySelector('.content > .work-card:first-child .work-head > .status')?.remove();}
function legacyConsolidationDetailView(){const pallet=state.pallet;const labels=plans.flatMap(plan=>Array.from({length:plan.expectedPallets},(_,index)=>palletLabelOption(plan,plannedPalletNo(plan,index))));const saved=state.pallets.find(item=>item.id===pallet.id);const status=saved||pallet.items.length?t('occupied'):t('idle');const photos=palletPhotoItems();const submitAction=pallet.originalPalletId?'submitConsolidation()':'submitPallet()';return layout(`<section class="work-card pallet-label-selector"><div class="work-head"><div><h2>${t('selectPalletLabel')}</h2><p>${t('palletLabel')} · ${t('palletStatus')}</p></div></div><div class="field" style="margin:12px 0 0"><label for="pallet-label-select">${t('palletLabel')}</label><select id="pallet-label-select" onchange="handlePalletLabelChange(this)">${labels.join('')}</select></div></section><section class="work-card current-pallet-card"><div class="work-head"><div><h2>${t('currentPallet')}</h2><p>${t('mergeDesc')}</p></div><span class="status ${status===t('occupied')?'blue':'good'}">${status}</span></div><div class="detail-list"><div class="detail-row"><span>${t('palletNo')}</span><strong>${esc(pallet.id||'DRAFT')}</strong></div><div class="detail-row"><span>${t('destinationWarehouseShort')}</span><strong>${esc(pallet.destinationCode||'—')}</strong></div><div class="detail-row"><span>${t('palletStatus')}</span><strong>${status}</strong></div><div class="detail-row"><span>${t('cartonCount')}</span><strong>${pallet.items.length}</strong></div></div></section><section class="work-card consolidation-scan-card"><div class="work-head"><div><h2>${t('scanFreight')}</h2><p>${t('sameDest')}</p></div></div><div class="scan-box"><input id="pallet-scan" placeholder="${t('scanFreight')}" onkeydown="if(event.key==='Enter')handlePalletScan()"><button onclick="handlePalletScan()" aria-label="${t('scan')}">${icon('scan')}</button></div>${state.palletMessage?`<div class="scan-result ${state.palletMessage.type}">${esc(state.palletMessage.text)}</div>`:''}</section><section class="work-card consolidation-items-card"><div class="work-head"><div><h2>${t('consolidationItems')}</h2><p>${t('trace')}</p></div><span class="status blue">${pallet.items.length}</span></div>${pallet.items.length?`<div class="table-wrap"><table class="table"><thead><tr><th>${t('boxMark')}</th><th>${t('inboundCode')}</th><th>${t('productName')}</th><th>${t('remove')}</th></tr></thead><tbody>${pallet.items.map((item,index)=>{const plan=plans.find(entry=>entry.id===item.planNo);return `<tr><td>${esc(item.boxNo)}</td><td>${esc(plan?.inboundCode||'—')}</td><td>${esc(item.name||'—')}</td><td><button class="btn danger" style="min-height:44px;padding:0 8px" onclick="removePalletItem(${index})">${t('remove')}</button></td></tr>`}).join('')}</tbody></table></div>`:`<div class="empty">${t('emptyPallet')}</div>`}</section><section class="work-card pallet-site-photos"><div class="work-head"><div><h2>${t('sitePhotos')}</h2><p>${t('palletPhotoHint')}</p></div><span class="status blue">${photos.length}</span></div>${photoListMarkup()}<div style="margin-top:12px">${photoAddButton()}</div></section><div class="bottom-bar"><div style="display:flex;gap:8px"><button class="btn ghost" style="flex:1" onclick="navigate('pallet-list')">${t('back')}</button><button class="btn primary" style="flex:2" onclick="${submitAction}" ${pallet.items.length?'':'disabled'}>${t('submitConsolidation')}</button></div></div>`,t('mergeOnly'),t('mergeDesc'),'pallet-list');}
function consolidationDetailView(){const rendered=legacyConsolidationDetailView().split(t('scanFreight')).join(t('scanPalletLabel'));const selectorClassName=['pallet','label','selector'].join('-');const selectorClass=`<section class="work-card ${selectorClassName}">`;const selectorAt=rendered.indexOf(selectorClass);if(selectorAt<0)return rendered;const selectorEnd=rendered.indexOf('</section>',selectorAt)+('</section>').length;const withoutSelector=rendered.slice(0,selectorAt)+rendered.slice(selectorEnd);const scanClass='consolidation-scan-card';const scanAt=withoutSelector.indexOf(scanClass);const currentAt=withoutSelector.indexOf('current-pallet-card');if(scanAt<0||currentAt<0||scanAt<currentAt)return withoutSelector;const scanStart=withoutSelector.lastIndexOf('<section',scanAt);const scanEnd=withoutSelector.indexOf('</section>',scanAt)+('</section>'.length);const scan=withoutSelector.slice(scanStart,scanEnd);return withoutSelector.slice(0,scanStart)+withoutSelector.slice(scanEnd,currentAt)+scan+withoutSelector.slice(currentAt);}
const baseConsolidationDetailView=consolidationDetailView;
consolidationDetailView=function(){const rendered=baseConsolidationDetailView();const scanClass='consolidation-scan-card';const scanAt=rendered.indexOf(scanClass);const currentAt=rendered.indexOf('current-pallet-card');if(scanAt<0||currentAt<0||scanAt<currentAt)return rendered;const scanStart=rendered.lastIndexOf('<section',scanAt);const scanEnd=rendered.indexOf('</section>',scanAt)+('</section>').length;const scan=rendered.slice(scanStart,scanEnd);return rendered.slice(0,currentAt)+scan+rendered.slice(currentAt,scanStart)+rendered.slice(scanEnd);};
const reorderConsolidationDetailView=consolidationDetailView;
consolidationDetailView=function(){const rendered=reorderConsolidationDetailView();const scanAt=rendered.indexOf('consolidation-scan-card');const currentAt=rendered.indexOf('current-pallet-card');if(scanAt<0||currentAt<0||scanAt<currentAt)return rendered;const scanStart=rendered.lastIndexOf('<section',scanAt);const scanEnd=rendered.indexOf('</section>',scanAt)+('</section>').length;const currentStart=rendered.lastIndexOf('<section',currentAt);return rendered.slice(0,currentStart)+rendered.slice(scanStart,scanEnd)+rendered.slice(currentStart,scanStart)+rendered.slice(scanEnd);};
const finalConsolidationDetailView=baseConsolidationDetailView;
consolidationDetailView=function(){const rendered=finalConsolidationDetailView().split(t('scanFreight')).join(t('scanPalletLabel'));const selectorClassName=['pallet','label','selector'].join('-');const selectorClass=`<section class="work-card ${selectorClassName}">`;const selectorAt=rendered.indexOf(selectorClass);if(selectorAt<0)return rendered;const selectorEnd=rendered.indexOf('</section>',selectorAt)+('</section>').length;const withoutSelector=rendered.slice(0,selectorAt)+rendered.slice(selectorEnd);const scanAt=withoutSelector.indexOf('consolidation-scan-card');const currentAt=withoutSelector.indexOf('current-pallet-card');if(scanAt<0||currentAt<0||scanAt<currentAt)return withoutSelector;const scanStart=withoutSelector.lastIndexOf('<section',scanAt);const scanEnd=withoutSelector.indexOf('</section>',scanAt)+('</section>').length;const currentStart=withoutSelector.lastIndexOf('<section',currentAt);return withoutSelector.slice(0,currentStart)+withoutSelector.slice(scanStart,scanEnd)+withoutSelector.slice(currentStart,scanStart)+withoutSelector.slice(scanEnd);};
consolidationDetailView=function(){const rendered=legacyConsolidationDetailView().split(t('scanFreight')).join(t('scanPalletLabel'));const selectorClassName=['pallet','label','selector'].join('-');const selectorClass=`<section class="work-card ${selectorClassName}">`;const selectorAt=rendered.indexOf(selectorClass);if(selectorAt<0)return rendered;const selectorEnd=rendered.indexOf('</section>',selectorAt)+('</section>').length;const withoutSelector=rendered.slice(0,selectorAt)+rendered.slice(selectorEnd);const scanAt=withoutSelector.indexOf('consolidation-scan-card');const currentAt=withoutSelector.indexOf('current-pallet-card');if(scanAt<0||currentAt<0||scanAt<currentAt)return withoutSelector;const scanStart=withoutSelector.lastIndexOf('<section',scanAt);const scanEnd=withoutSelector.indexOf('</section>',scanAt)+('</section>').length;const currentStart=withoutSelector.lastIndexOf('<section',currentAt);return withoutSelector.slice(0,currentStart)+withoutSelector.slice(scanStart,scanEnd)+withoutSelector.slice(currentStart,scanStart)+withoutSelector.slice(scanEnd);};
const positionedConsolidationDetailView=consolidationDetailView;
consolidationDetailView=function(){const rendered=positionedConsolidationDetailView();const emptyPhotoMarkup=`<div class="empty">${t('noPhotos')}</div>`;return rendered.split(emptyPhotoMarkup).join('');};
const basePalletView=palletView;
palletView=function(){return state.palletMode==='consolidate'&&state.pallet?consolidationDetailView():basePalletView();};
injectPalletPhotoFeatures=function(){if(state.page!=='pallet'||!state.pallet||state.palletMode==='consolidate'||state.palletOrderType!=='overseas')return;const root=document.querySelector('.content');if(!root||root.querySelector('.pallet-site-photos'))return;const key=palletPhotoKey();const photos=palletPhotoItems(key);const photoTiles=photos.length?`<div class="photo-grid">${photos.map((src,index)=>`<div class="photo-step done"><div class="photo-preview"><img src="${src}" alt="${t('photoItem')} ${index+1}"></div><strong>${t('photoItem')} ${index+1}</strong></div>`).join('')}</div>`:'';const section=document.createElement('section');section.className='work-card pallet-site-photos';section.innerHTML=`<div class="work-head"><div><h2>${t('sitePhotos')}</h2><p>${t('palletPhotoHint')}</p></div><span class="status blue">${photos.length}</span></div>${photoTiles}<div style="margin-top:12px">${photoAddButton()}</div>`;const bottom=root.querySelector('.bottom-bar');if(bottom)root.insertBefore(section,bottom);else root.appendChild(section);};
photoAddButton=function(){const id='pallet-site-photo-input';return `<button type="button" class="photo-add-tile photo-add-action" onclick="document.getElementById('${id}').click()" aria-label="${t('takePhoto')}" style="display:grid;place-items:center;width:76px;height:76px;margin:0 auto;border:1px solid #d0d5dd;border-radius:10px;background:#fff;color:#98a2b3;font-size:32px;line-height:1;cursor:pointer"><span aria-hidden="true">+</span></button><input id="${id}" type="file" accept="image/*" capture="environment" onchange="handlePalletPhoto(this)" style="display:none">`;};
photoAddButton=function(){const id='pallet-site-photo-input';return `<button type="button" class="photo-add-tile photo-add-action" onclick="document.getElementById('${id}').click()" aria-label="${t('takePhoto')}" style="display:grid;place-items:center;width:76px;height:76px;margin:0;border:1px solid #d0d5dd;border-radius:10px;background:#fff;color:#98a2b3;font-size:32px;line-height:1;cursor:pointer"><span aria-hidden="true">+</span></button><input id="${id}" type="file" accept="image/*" capture="environment" onchange="handlePalletPhoto(this)" style="display:none">`;};
Object.assign(EXTRA_LABELS.zh,{collapse:'收起',expand:'展开'});
Object.assign(EXTRA_LABELS.en,{collapse:'Collapse',expand:'Expand'});
Object.assign(EXTRA_LABELS.es,{collapse:'Contraer',expand:'Expandir'});
ICON_PATHS.chevronDown='<path d="m6 9 6 6 6-6"/>';
ICON_PATHS.chevronUp='<path d="m6 15 6-6 6 6"/>';
pickView=function(){const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];if(!state.pickTaskId)state.pickTaskId=task.id;const done=task.items.filter(x=>x.status||x.picked>=x.qty).length;const total=task.items.length;const pickedPallets=task.done?task.palletCount:Math.min(task.palletCount,Math.floor(done/total*task.palletCount));const status=task.done?t('taskDone'):t('pending');const collapsed=Boolean(state.pickDetailsCollapsed);const detailRows=`<div class="detail-row"><span>${t('outboundOrderNo')}</span><strong>${esc(task.id)}</strong></div><div class="detail-row"><span>${t('inboundCode')}</span><strong>${esc(task.inboundCode)}</strong></div><div class="detail-row"><span>${t('plateNo')}</span><strong>${esc(task.plateNo)}</strong></div><div class="detail-row"><span>${t('driver')}</span><strong>${esc(task.driver)}</strong></div><div class="detail-row"><span>${t('outboundTime')}</span><strong>${esc(task.outboundTime)}</strong></div><div class="detail-row"><span>${t('totalPalletBoxes')}</span><strong>${task.totalPallets} ${t('palletCount')} / ${task.boxes} ${t('boxes')}</strong></div><div class="detail-row"><span>${t('linkedOrder')}</span><strong>${esc(task.orderInfo)}</strong></div>`;return layout(`<section class="work-card pick-scan-card"><div class="scan-box"><input id="pick-scan" placeholder="${t('reviewScan')}" onkeydown="if(event.key==='Enter')handlePickScan()"><button onclick="handlePickScan()" aria-label="${t('scan')}">${icon('scan')}</button></div>${state.pickMessage?`<div class="scan-result ${state.pickMessage.type}">${esc(state.pickMessage.text)}</div>`:''}<div class="progress"><span style="width:${task.palletCount?Math.round(pickedPallets/task.palletCount*100):0}%"></span></div><div class="progress-line"><span>${t('pickProgress')}</span><strong>${pickedPallets} / ${task.palletCount} ${t('palletCount')}</strong></div></section><section class="work-card pick-order-details ${collapsed?'is-collapsed':''}"><div class="work-head pick-order-head"><div><h2>${t('details')}</h2><p>${esc(task.id)} · ${esc(task.customer)}</p></div><button type="button" class="pick-details-toggle" aria-expanded="${!collapsed}" aria-controls="pick-order-detail-list" aria-label="${collapsed?t('expand'):t('collapse')}" onclick="togglePickDetails()">${icon(collapsed?'chevronDown':'chevronUp')}<span>${collapsed?t('expand'):t('collapse')}</span></button></div><div class="pick-order-summary" aria-label="${t('details')}"><div class="summary-item"><span>${t('outboundOrderNo')}</span><strong>${esc(task.id)}</strong></div><div class="summary-item"><span>${t('status')}</span><strong class="status ${task.done?'good':'wait'}">${status}</strong></div><div class="summary-item"><span>${t('customer')}</span><strong>${esc(task.customer)}</strong></div><div class="summary-item"><span>${t('destinationWarehouseShort')}</span><strong>${esc(task.destination)}</strong></div></div><div id="pick-order-detail-list" class="detail-list">${collapsed?'':detailRows}</div></section><section class="work-card"><div class="list">${task.items.map((x,i)=>`<div class="list-card" style="padding:12px"><div class="list-top"><div><div class="list-title">${esc(x.sku)}</div><div class="list-meta">${x.picked} / ${x.qty} ${t('pieces')}${x.skipReason?` · ${esc(x.skipReason)}`:''}</div></div><span class="status ${x.status==='picked'?'good':x.status==='skipped'?'bad':x.status==='partial'?'blue':'wait'}">${t(x.status||'pending')}</span></div><div class="card-actions" style="justify-content:stretch;flex-wrap:wrap"><button class="btn secondary" onclick="pickOne(${i})">${t('confirmPick')}</button><button class="btn ghost" onclick="openPartialPick(${i})">${t('partialPick')}</button><button class="btn danger" onclick="skipPick(${i})">${t('skipPick')}</button>${x.status?`<button class="btn ghost" onclick="undoPick(${i})">${t('undo')}</button>`:''}</div></div>`).join('')}</div></section><div class="bottom-bar"><button class="btn primary" onclick="submitPick()" ${done===total?'':'disabled'}>${t('submitPick')}</button></div>`,t('pick'),t('pickDesc'),'workbench');};
function togglePickDetails(){state.pickDetailsCollapsed=!state.pickDetailsCollapsed;render();}
injectPickDetails=function(){};
const basePickViewForOrderDisplay=pickView;
pickView=function(){const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];let rendered=basePickViewForOrderDisplay();if(state.page!=='pick'||!task)return rendered;rendered=rendered.replace('class="pick-order-summary"','class="pick-order-summary hidden"');rendered=rendered.replace(`<h2>${t('details')}</h2>`,`<h2>${t('outboundOrderNo')}</h2>`);const orderRow=`<div class="detail-row"><span>${t('outboundOrderNo')}</span><strong>${esc(task.id)}</strong></div>`;const destinationRow=`<div class="detail-row"><span>${t('destinationWarehouseShort')}</span><strong>${esc(task.destination)}</strong></div>`;return rendered.replace(orderRow,destinationRow);};
const basePickViewForOrderHeader=pickView;
pickView=function(){const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];let rendered=basePickViewForOrderHeader();if(state.page!=='pick'||!task)return rendered;const orderTitleMarkup=`<h2>${t('outboundOrderNo')}</h2>`;const orderNumberMarkup=`<h2>${esc(task.id)}</h2>`;const orderSubMarkup=`<p>${esc(task.id)} · ${esc(task.customer)}</p>`;const destinationSubMarkup=`<p>${esc(task.destination)}</p>`;return rendered.replace(orderTitleMarkup,orderNumberMarkup).replace(orderSubMarkup,destinationSubMarkup);};
const basePickViewForDestinationLabel=pickView;
pickView=function(){const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];let rendered=basePickViewForDestinationLabel();if(state.page!=='pick'||!task)return rendered;const destinationSubMarkup=`<p>${esc(task.destination)}</p>`;const labeledDestinationSub=`<p>${t('destinationWarehouseShort')}:${esc(task.destination)}</p>`;const redundantDestinationRow=`<div class="detail-row"><span>${t('destinationWarehouseShort')}</span><strong>${esc(task.destination)}</strong></div>`;return rendered.replace(destinationSubMarkup,labeledDestinationSub).replace(redundantDestinationRow,'');};
Object.assign(EXTRA_LABELS.zh,{linkedInboundCode:'关联入仓码'});
Object.assign(EXTRA_LABELS.en,{linkedInboundCode:'Linked inbound code'});
Object.assign(EXTRA_LABELS.es,{linkedInboundCode:'Código de entrada vinculado'});
const basePickViewForLinkedInboundCode=pickView;
pickView=function(){const task=pickTasks.find(x=>x.id===state.pickTaskId)||pickTasks.find(x=>!x.done)||pickTasks[0];let rendered=basePickViewForLinkedInboundCode();if(state.page!=='pick'||!task)return rendered;const hiddenInboundCodeRow=`<div class="detail-row"><span>${t('inboundCode')}</span><strong>${esc(task.inboundCode)}</strong></div>`;const linkedOrderRow=`<div class="detail-row"><span>${t('linkedOrder')}</span><strong>${esc(task.orderInfo)}</strong></div>`;const linkedInboundCodeRow=`<div class="detail-row"><span>${t('linkedInboundCode')}</span><strong>${esc(task.inboundCode)}</strong></div>`;return rendered.replace(hiddenInboundCodeRow,'').replace(linkedOrderRow,linkedInboundCodeRow);};
Object.assign(EXTRA_LABELS.zh,{relabelBoxMode:'贴箱唛',relabelSkuMode:'贴SKU标',relabelStart:'开始作业',relabelBoxTitle:'贴箱唛',relabelSkuTitle:'贴SKU标',relabelOrderNo:'换标单号',relabelBoxCount:'箱数',relabelUnitsPerBox:'每箱个数',relabelRequirement:'贴标要求',relabelScanBox:'扫描箱唛',relabelScanSku:'扫描SKU',relabelProgress:'贴标进度',relabelPreview:'标签预览',relabelPreviewEmpty:'扫描后显示标签预览',relabelPrint:'打印标签',relabelPrinted:'已打印并完成',relabelScanRequired:'请先扫描箱唛或 SKU',relabelScanNotFound:'未找到匹配的箱唛或 SKU',relabelScanSuccess:'扫描成功',relabelWorkHint:'扫描后确认标签内容并打印'});
Object.assign(EXTRA_LABELS.en,{relabelBoxMode:'Carton mark',relabelSkuMode:'SKU label',relabelStart:'Start work',relabelBoxTitle:'Carton mark',relabelSkuTitle:'SKU label',relabelOrderNo:'Relabel order no.',relabelBoxCount:'Cartons',relabelUnitsPerBox:'Units per carton',relabelRequirement:'Label requirement',relabelScanBox:'Scan carton mark',relabelScanSku:'Scan SKU',relabelProgress:'Labeling progress',relabelPreview:'Label preview',relabelPreviewEmpty:'Scan to show a label preview',relabelPrint:'Print label',relabelPrinted:'Printed and completed',relabelScanRequired:'Scan a carton mark or SKU first',relabelScanNotFound:'No matching carton mark or SKU',relabelScanSuccess:'Scan accepted',relabelWorkHint:'Confirm the label content after scanning, then print'});
Object.assign(EXTRA_LABELS.es,{relabelBoxMode:'Marca de caja',relabelSkuMode:'Etiqueta SKU',relabelStart:'Iniciar trabajo',relabelBoxTitle:'Marca de caja',relabelSkuTitle:'Etiqueta SKU',relabelOrderNo:'N.º de reetiquetado',relabelBoxCount:'Cajas',relabelUnitsPerBox:'Unidades por caja',relabelRequirement:'Requisito de etiqueta',relabelScanBox:'Escanea la marca de caja',relabelScanSku:'Escanea el SKU',relabelProgress:'Progreso de etiquetado',relabelPreview:'Vista previa',relabelPreviewEmpty:'Escanea para ver la etiqueta',relabelPrint:'Imprimir etiqueta',relabelPrinted:'Impresa y completada',relabelScanRequired:'Escanea primero una caja o SKU',relabelScanNotFound:'No se encontró la caja o SKU',relabelScanSuccess:'Escaneo aceptado',relabelWorkHint:'Confirma el contenido y luego imprime'});
function relabelModeKey(mode,planId){return `${mode}:${planId}`;}
function setRelabelMode(mode){if(!['box','sku'].includes(mode))return;state.relabelMode=mode;state.relabelMessage=null;render();}
function openRelabelWork(planId,mode=state.relabelMode){const plan=plans.find(p=>p.id===planId);if(!plan)return;state.selectedRelabelId=plan.id;state.relabelMode=mode;state.relabelMessage=null;state.page=mode==='sku'?'relabel-sku':'relabel-box';render();}
function relabelItemsForPlan(planId){return cargo.filter(item=>item.planNo===planId);}
function relabelDone(mode,planId){return Boolean(state.relabelDoneOrders[relabelModeKey(mode,planId)]);}
function relabelListValues(mode,p){const items=relabelItemsForPlan(p.id);const unitsPerBox=items.length?items.map(item=>item.qty).join(' / '):'—';return {orderNo:p.id,boxCount:p.expectedBoxes??items.length,unitsPerBox,requirement:p.relabelRequirement||t(mode==='sku'?'relabelSkuMode':'relabelBoxMode'),status:relabelDone(mode,p.id)};}
function relabelView(){const mode=state.relabelMode==='sku'?'sku':'box';const modeLabel=mode==='sku'?t('relabelSkuMode'):t('relabelBoxMode');return layout(`<div class="relabel-mode-tabs" role="tablist" aria-label="${esc(t('relabelTitle'))}"><button type="button" role="tab" aria-selected="${mode==='box'}" class="${mode==='box'?'active':''}" onclick="setRelabelMode('box')">${t('relabelBoxMode')}</button><button type="button" role="tab" aria-selected="${mode==='sku'}" class="${mode==='sku'?'active':''}" onclick="setRelabelMode('sku')">${t('relabelSkuMode')}</button></div><div class="toolbar"><span class="subtle">${plans.length} ${t('orderList')} · ${modeLabel}</span></div><div class="list">${plans.map(p=>{const values=relabelListValues(mode,p);return `<article class="list-card"><div class="list-grid relabel-list-grid"><div><span>${t('relabelOrderNo')}</span><strong>${esc(values.orderNo)}</strong></div>${mode==='box'?`<div><span>${t('relabelBoxCount')}</span><strong>${values.boxCount} ${t('boxes')}</strong></div>`:`<div><span>${t('relabelUnitsPerBox')}</span><strong>${esc(values.unitsPerBox)} ${t('pieceUnit')}</strong></div>`}<div><span>${t('relabelRequirement')}</span><strong>${esc(values.requirement)}</strong></div><div><span>${t('status')}</span><strong>${values.status?t('relabelDone'):t('pending')}</strong></div></div><div class="card-actions"><button class="btn ${values.status?'ghost':'primary'}" onclick="openRelabelWork('${p.id}','${mode}')" ${values.status?'disabled':''}>${values.status?t('complete'):t('relabelStart')}</button></div></article>`}).join('')}</div>`,t('relabelTitle'),t('relabelHint'),'workbench');}
function relabelDetailView(mode){const p=selectedRelabelOrder();if(!p)return relabelView();const key=relabelModeKey(mode,p.id);const scan=state.relabelScans[mode]?.[p.id];const item=scan?cargo.find(c=>c.id===scan.itemId):null;const done=relabelDone(mode,p.id);const totalBoxes=Number(p.expectedBoxes??relabelItemsForPlan(p.id).length)||0;const progressTotal=mode==='sku'?(Number(item?.qty)||0):totalBoxes;const progressCompleted=done?progressTotal:0;const progressPercent=progressTotal?Math.round(progressCompleted/progressTotal*100):0;const title=mode==='sku'?t('relabelSkuTitle'):t('relabelBoxTitle');const scanHint=mode==='sku'?t('relabelScanSku'):t('relabelScanBox');const previewId=mode==='sku'?'relabel-sku-preview':'relabel-box-preview';const value=item?(mode==='sku'?item.sku:item.boxNo):'';const secondary=item?(mode==='sku'?item.name:`${item.sku} · ${item.name}`):t('relabelPreviewEmpty');return layout(`<section class="work-card relabel-scan-card"><div class="scan-box"><input id="relabel-scan" value="${esc(scan?.value||'')}" placeholder="${esc(scanHint)}" onkeydown="if(event.key==='Enter')handleRelabelScan()"><button type="button" onclick="handleRelabelScan()" aria-label="${esc(t('scan'))}">${icon('scan')}</button></div>${state.relabelMessage?`<div class="scan-result ${state.relabelMessage.type}">${esc(state.relabelMessage.text)}</div>`:''}<div class="progress"><span style="width:${progressPercent}%"></span></div><div class="progress-line"><span>${t('relabelProgress')}</span><strong>${progressCompleted} / ${progressTotal} ${mode==='sku'?t('pieces'):t('boxes')}</strong></div></section><section class="work-card"><div class="work-head"><div><h2>${t('relabelPreview')}</h2><p>${t('relabelWorkHint')}</p></div></div><div id="${previewId}" class="relabel-label-preview"><div class="preview-kind">${title}</div><div class="preview-value">${esc(value||'—')}</div><div class="preview-meta">${esc(secondary)}</div></div><div class="relabel-print-actions"><button type="button" class="btn ghost" onclick="navigate('relabel')">${t('back')}</button><button type="button" class="btn primary" onclick="printRelabelLabel()" ${item?'':'disabled'}>${done?t('relabelPrinted'):t('relabelPrint')}</button></div></section>`,title,t('relabelWorkHint'),'relabel');}
function relabelBoxView(){return relabelDetailView('box');}
function relabelSkuView(){return relabelDetailView('sku');}
function handleRelabelScan(){const mode=state.page==='relabel-sku'?'sku':'box';const p=selectedRelabelOrder();const input=document.getElementById('relabel-scan');const value=(input?.value||'').trim();if(!p||!value){state.relabelMessage={type:'error',text:t('relabelScanRequired')};render();return}const item=relabelItemsForPlan(p.id).find(c=>mode==='sku'?c.sku===value:c.boxNo===value);if(!item){state.relabelMessage={type:'error',text:t('relabelScanNotFound')};render();return}state.relabelScans[mode][p.id]={value,itemId:item.id};state.relabelMessage={type:'good',text:`${t('relabelScanSuccess')}: ${value}`};render();}
function printRelabelLabel(){const mode=state.page==='relabel-sku'?'sku':'box';const p=selectedRelabelOrder();const scan=p&&state.relabelScans[mode]?.[p.id];if(!p||!scan){toast(t('relabelScanRequired'),'bad');return}state.relabelDoneOrders[relabelModeKey(mode,p.id)]=true;state.relabelMessage={type:'good',text:`${t('relabelPrinted')}: ${p.id}`};window.print();render();}
let lastRenderedPage='';
function settlePageScroll(){if(lastRenderedPage===state.page)return;lastRenderedPage=state.page;requestAnimationFrame(()=>window.scrollTo({top:0,left:0,behavior:'auto'}));}
function injectFinalCopy(){
if(state.page==='relabel'){
const count=document.querySelector('.toolbar .subtle');
if(count)count.textContent=t('orderCount',{count:plans.length});
}
const replacements=[[t('palletCount'),t('palletUnit')],[t('pieces'),t('pieceUnit')]];
document.querySelectorAll('strong,.toolbar .subtle,.progress-line strong,.scan-result.info,.list-meta').forEach(element=>{
replacements.forEach(([label,unit])=>{element.textContent=element.textContent.split(label).join(unit)});
});
}
const baseRender=render;
render=function(){if(redirectForCurrentPage())return;if(state.page==='photo-detail'){const app=document.getElementById('app');app.dataset.page=state.page;app.innerHTML=photoDetailView();injectIconReplacements();injectFinalCopy();settlePageScroll();document.documentElement.lang=state.lang==='zh'?'zh-CN':state.lang==='es'?'es':'en';return}baseRender();injectPalletListPhotoButtons();injectPalletPhotoFeatures();injectReconsolidationSubmit();injectCurrentPalletCleanup();injectFinalCopy();settlePageScroll();};
function bootstrap(){restoreAppSession();initializeSectionPage();render();}
bootstrap();