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 edc78ba93954d4875c3c00183efb6c25f479af14ae46da13a29292652ad3b1ad:


import { useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { api, can, money, qty } from '../api'
import { useSession } from '../session'
import { Empty, Page, Pager } from '../ui'

type Summary = {
  bill_id: number
  bill_code: string
  supplier_name: string
  warehouse_name: string
  warehouse_id: number
  bill_date: string
  status_name: string
  total_qty: number
  total_money: number
  remark: string
}

type Item = {
  bill_detail_id: number
  batch_code: string
  goods_code: string
  goods_name: string
  barcode: string
  base_qty: number
  base_unit: string
  position_code: string
}

const meta = {
  receipt: {
    title: '采购入库单',
    list: '/api/purchase-receipts',
    detail: '/api/purchase-receipts/detail',
    base: '/receipts',
    batch: true,
  },
  order: {
    title: '采购订单',
    list: '/api/inbounds/pending',
    detail: '/api/inbounds/detail',
    base: '/inbounds',
    batch: false,
  },
}

export function Documents({ kind }: { kind: 'receipt' | 'order' }) {
  const { billId } = useParams()
  if (billId) return <Detail kind={kind} billId={billId} />
  return <List kind={kind} />
}

function List({ kind }: { kind: 'receipt' | 'order' }) {
  const spec = meta[kind]
  const navigate = useNavigate()
  const [offset, setOffset] = useState(0)
  const [total, setTotal] = useState(0)
  const [rows, setRows] = useState<Summary[]>([])
  const [error, setError] = useState('')
  const limit = 50

  useEffect(() => {
    api<{ total: number; inbounds: Summary[] }>(`${spec.list}?limit=${limit}&offset=${offset}`)
      .then((data) => {
        setTotal(data.total)
        setRows(data.inbounds)
      })
      .catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败'))
  }, [spec.list, offset])

  return (
    <Page title={spec.title}>
      {error && <p className="banner error">{error}</p>}
      {rows.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>
              </tr>
            </thead>
            <tbody>
              {rows.map((row) => (
                <tr key={row.bill_id} className="clickable" onClick={() => navigate(`${spec.base}/${row.bill_id}`)}>
                  <td><Link to={`${spec.base}/${row.bill_id}`}>{row.bill_code}</Link></td>
                  <td>{row.supplier_name}</td>
                  <td>{row.warehouse_name}</td>
                  <td>{row.bill_date}</td>
                  <td>{row.status_name}</td>
                  <td>{qty(row.total_qty)}</td>
                  <td>{money(row.total_money)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      <Pager total={total} limit={limit} offset={offset} onPage={setOffset} />
    </Page>
  )
}

function Detail({ kind, billId }: { kind: 'receipt' | 'order'; billId: string }) {
  const spec = meta[kind]
  const { user } = useSession()
  const allowPutaway = spec.batch && can(user, 'putaway.execute')
  const [doc, setDoc] = useState<(Summary & { items: Item[] }) | null>(null)
  const [error, setError] = useState('')

  useEffect(() => {
    api<{ inbound: Summary & { items: Item[] } }>(`${spec.detail}/${billId}`)
      .then((data) => setDoc(data.inbound))
      .catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败'))
  }, [spec.detail, billId])

  return (
    <Page title={doc ? doc.bill_code : spec.title}>
      <Link className="back" to={spec.base}>返回列表</Link>
      {error && <p className="banner error">{error}</p>}
      {doc && (
        <>
          <div className="cards">
            <div className="card"><span>供应商</span><strong>{doc.supplier_name}</strong></div>
            <div className="card"><span>仓库</span><strong>{doc.warehouse_name}</strong></div>
            <div className="card"><span>状态</span><strong>{doc.status_name}</strong></div>
            <div className="card"><span>数量</span><strong>{qty(doc.total_qty)}</strong></div>
          </div>
          {doc.remark && <p className="muted">备注:{doc.remark}</p>}
          <div className="table-wrap">
            <table>
              <thead>
                <tr>
                  {spec.batch && <th>批次码</th>}
                  <th>商品</th>
                  <th>编码</th>
                  <th>条码</th>
                  <th>数量</th>
                  <th>来肯货位</th>
                </tr>
              </thead>
              <tbody>
                {doc.items.map((item) => (
                  <tr key={item.bill_detail_id}>
                    {spec.batch && (
                      <td className="mono">
                        {allowPutaway && item.batch_code ? (
                          <Link to={`/putaway?code=${encodeURIComponent(item.batch_code)}`}>
                            {item.batch_code}
                          </Link>
                        ) : (
                          item.batch_code
                        )}
                      </td>
                    )}
                    <td>{item.goods_name}</td>
                    <td className="mono">{item.goods_code}</td>
                    <td className="mono">{item.barcode}</td>
                    <td>{qty(item.base_qty)} {item.base_unit}</td>
                    <td>{item.position_code}</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.