Artifact e8517cff87f24aa830773334404b1da2f35e87e90950d31eb278a9356a24178f:
- File
web/src/pages/Moves.tsx
— part of check-in
[bee9efb316]
at
2026-10-23 06:37:04
on branch trunk
— 同一仓库内移库,并留下波次已占用的数量
- 把一个批次从源库位转到启用的目标库位,库存立刻变更 - 未完成波次占用的数量不能移走,撤销波次后才能继续移 - 移库记录留在本地,不写回来肯 (user: pluto size: 6403)
import { useEffect, useState } from 'react' import { api, qty } from '../api' import { useSession } from '../session' import { Banner, Empty, Field, Page } from '../ui' type Batch = { location_code: string sku_id: number goods_code: string batch_no: string produced_on: string expires_on: string qty: number } type Spot = { code: string; enabled: boolean } type Move = { id: string from_location: string to_location: string goods_code: string batch_no: string expires_on: string qty: number created_by: string created_at: string } function batchKey(row: Batch) { return [row.location_code, row.sku_id, row.batch_no, row.produced_on, row.expires_on].join('|') } export function Moves() { const { user } = useSession() const warehouseId = user?.current_warehouse_id ?? 0 const [batches, setBatches] = useState<Batch[]>([]) const [spots, setSpots] = useState<Spot[]>([]) const [moves, setMoves] = useState<Move[]>([]) const [source, setSource] = useState('') const [target, setTarget] = useState('') const [amount, setAmount] = useState('') const [error, setError] = useState('') const [ok, setOk] = useState('') const [pending, setPending] = useState(false) const [ready, setReady] = useState(false) function load() { if (warehouseId <= 0) return setReady(false) Promise.all([ api<{ batches: Batch[] }>(`/api/inventory?warehouse_id=${warehouseId}`), api<{ locations: Spot[] }>(`/api/locations?warehouse_id=${warehouseId}`), api<{ moves: Move[] }>(`/api/moves?warehouse_id=${warehouseId}`), ]) .then(([stock, locations, history]) => { setBatches(stock.batches) setSpots(locations.locations.filter((row) => row.enabled)) setMoves(history.moves) setError('') }) .catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败')) .finally(() => setReady(true)) } useEffect(load, [warehouseId]) const chosen = batches.find((row) => batchKey(row) === source) const targets = spots.filter((row) => row.code !== chosen?.location_code) const qtyValue = Number(amount) const canSubmit = warehouseId > 0 && chosen !== undefined && target !== '' && qtyValue > 0 && !pending function onSource(value: string) { setSource(value) setOk('') const row = batches.find((item) => batchKey(item) === value) setAmount(row ? String(row.qty) : '') if (row && target === row.location_code) setTarget('') } async function submit(event: { preventDefault(): void }) { event.preventDefault() if (!chosen || !canSubmit) return if (!window.confirm(`把 ${amount} 从 ${chosen.location_code} 移到 ${target}?库存会立刻变更。`)) return setPending(true) setError('') setOk('') try { await api('/api/moves', { method: 'POST', body: JSON.stringify({ from_location: chosen.location_code, sku_id: chosen.sku_id, batch_no: chosen.batch_no, produced_on: chosen.produced_on, expires_on: chosen.expires_on, qty: qtyValue, to_location: target, }), }) setOk(`${chosen.batch_no} 已从 ${chosen.location_code} 移到 ${target}`) setSource('') setTarget('') setAmount('') load() } catch (err) { setError(err instanceof Error ? err.message : '移库失败') } finally { setPending(false) } } return ( <Page title="移库"> <Banner kind="error" text={error} /> <Banner kind="ok" text={ok} /> {warehouseId <= 0 && <Banner kind="error" text="请先在右上角选择当前作业仓库" />} <p className="muted">只在当前作业仓库内转移同一批次。未完成波次占用的数量会留在原库位。</p> {warehouseId <= 0 || !ready ? null : batches.length === 0 ? ( <Empty text="这个仓库还没有批次库存。上架之后才能移库。" /> ) : ( <form className="panel" onSubmit={submit}> <Field label="批次库存"> <select value={source} onChange={(event) => onSource(event.target.value)}> <option value="">请选择</option> {batches.map((row) => ( <option key={batchKey(row)} value={batchKey(row)}> {row.location_code} · {row.goods_code} · {row.batch_no} · 到期 {row.expires_on || '未填'} · {qty(row.qty)} </option> ))} </select> </Field> <Field label="移动数量"> <input value={amount} onChange={(event) => setAmount(event.target.value)} inputMode="decimal" /> </Field> <Field label="目标库位"> <select value={target} onChange={(event) => setTarget(event.target.value)}> <option value="">请选择</option> {targets.map((row) => ( <option key={row.code} value={row.code}> {row.code} </option> ))} </select> </Field> <button type="submit" disabled={!canSubmit}> 确认移库 </button> </form> )} <h2>移库记录</h2> {warehouseId <= 0 || !ready ? null : moves.length === 0 ? ( <Empty text="还没有移库记录" /> ) : ( <div className="table-wrap"> <table> <thead> <tr> <th>时间</th> <th>从</th> <th>到</th> <th>商品</th> <th>批次</th> <th>到期</th> <th>数量</th> <th>操作人</th> </tr> </thead> <tbody> {moves.map((row) => ( <tr key={row.id}> <td>{row.created_at}</td> <td className="mono">{row.from_location}</td> <td className="mono">{row.to_location}</td> <td className="mono">{row.goods_code}</td> <td className="mono">{row.batch_no}</td> <td>{row.expires_on}</td> <td>{qty(row.qty)}</td> <td>{row.created_by}</td> </tr> ))} </tbody> </table> </div> )} </Page> ) }