Fosi Fossil Service
Small teams. Open source. Built on Fossil.

IaC / SwiftFlow

High-performance WMS with wave picking, smart routing, and real-time inventory visibility.

Public | Owner: IaC | Default branch: trunk | Last update: 2026-10-23 14:37

Artifact e8517cff87f24aa830773334404b1da2f35e87e90950d31eb278a9356a24178f:


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>
  )
}

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/IaC/SwiftFlow

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.