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 2ba58e16ad43956b232bc8e980452cabcfeede4eafa6b6060dee339a3c5b6928:


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

type Summary = {
  bill_id: number
  bill_code: string
  customer_name: string
  warehouse_name: string
  bill_date: string
  status_name: string
  total_qty: number
  total_money: number
}

type Item = {
  bill_detail_id: number
  goods_code: string
  goods_name: string
  barcode: string
  base_qty: number
  base_unit: string
  price: number
  total_money: number
}

type Shipment = {
  id: string
  created_at: string
  lines: { batch_no: string; location_code: string; qty: number; goods_name: string; expires_on: string }[]
}

export function Orders() {
  const { billId } = useParams()
  if (billId) return <OrderDetail billId={billId} />
  return <OrderList />
}

function OrderList() {
  const navigate = useNavigate()
  const [pending, setPending] = useState(true)
  const [offset, setOffset] = useState(0)
  const [total, setTotal] = useState(0)
  const [rows, setRows] = useState<Summary[]>([])
  const [error, setError] = useState('')
  const limit = 50

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

  return (
    <Page
      title="销售订单"
      extra={
        <div className="actions">
          <button type="button" className={pending ? '' : 'ghost'} onClick={() => { setPending(true); setOffset(0) }}>
            待发货
          </button>
          <button type="button" className={pending ? 'ghost' : ''} onClick={() => { setPending(false); setOffset(0) }}>
            全部
          </button>
        </div>
      }
    >
      {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(`/orders/${row.bill_id}`)}>
                  <td><Link to={`/orders/${row.bill_id}`}>{row.bill_code}</Link></td>
                  <td>{row.customer_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 OrderDetail({ billId }: { billId: string }) {
  const [order, setOrder] = useState<(Summary & { items: Item[]; remark: string }) | null>(null)
  const [shipments, setShipments] = useState<Shipment[]>([])
  const [error, setError] = useState('')

  useEffect(() => {
    api<{ order: Summary & { items: Item[]; remark: string } }>(`/api/orders/detail/${billId}`)
      .then((data) => setOrder(data.order))
      .catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败'))
    api<{ shipments: Shipment[] }>(`/api/orders/${billId}/shipments`)
      .then((data) => setShipments(data.shipments))
      .catch((err: unknown) => setError(err instanceof Error ? err.message : '出库记录加载失败'))
  }, [billId])

  return (
    <Page title={order ? order.bill_code : '销售订单'}>
      <Link className="back" to="/orders">返回列表</Link>
      {error && <p className="banner error">{error}</p>}
      {order && (
        <>
          <div className="cards">
            <div className="card"><span>客户</span><strong>{order.customer_name}</strong></div>
            <div className="card"><span>仓库</span><strong>{order.warehouse_name}</strong></div>
            <div className="card"><span>状态</span><strong>{order.status_name}</strong></div>
            <div className="card"><span>数量 / 金额</span><strong>{qty(order.total_qty)} / {money(order.total_money)}</strong></div>
          </div>
          {order.remark && <p className="muted">备注:{order.remark}</p>}
          <div className="table-wrap">
            <table>
              <thead>
                <tr>
                  <th>商品</th>
                  <th>编码</th>
                  <th>条码</th>
                  <th>数量</th>
                  <th>单价</th>
                  <th>金额</th>
                </tr>
              </thead>
              <tbody>
                {order.items.map((item) => (
                  <tr key={item.bill_detail_id}>
                    <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>{money(item.price)}</td>
                    <td>{money(item.total_money)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          <h2>出库记录</h2>
          {shipments.length === 0 ? (
            <Empty text="还没有本地出库" />
          ) : (
            shipments.map((shipment) => (
              <div key={shipment.id} className="panel">
                <strong>{shipment.created_at}</strong>
                <div className="table-wrap">
                  <table>
                    <thead>
                      <tr>
                        <th>商品</th>
                        <th>批次</th>
                        <th>库位</th>
                        <th>到期</th>
                        <th>数量</th>
                      </tr>
                    </thead>
                    <tbody>
                      {shipment.lines.map((line, index) => (
                        <tr key={`${line.batch_no}-${index}`}>
                          <td>{line.goods_name}</td>
                          <td className="mono">{line.batch_no}</td>
                          <td className="mono">{line.location_code}</td>
                          <td>{line.expires_on}</td>
                          <td>{qty(line.qty)}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              </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.