import React, { useCallback, useEffect, useMemo, useState } from 'react' import axios from 'axios' import Drawer from './Drawer' import { useAuth, hasPermission } from '../auth' /** * 订单支出(业务员记的杂费:快递费、吊卡费、商检、报关…)。 * * 为什么要有这个:订单上只有「卖给客户多少钱」,而一张单子真实花出去的还有寄样品的 * 快递费、印吊卡的费用、商检报关拖车。不记下来就算不出这单到底赚没赚 —— 小单子 * 几百块杂费能把毛利吃掉一截。 * * 币种是这里最容易出错的地方:杂费基本都是人民币,订单常常是美元。所以每笔支出 * 带自己的币种,跨币种时**必须填汇率**(不猜,猜出来的毛利没人敢用), * 后端会把汇率快照存下来,之后汇率再变历史账也不动。 */ const num = value => { const parsed = Number(value) return Number.isFinite(parsed) ? parsed : 0 } const fmt = (value, currency) => `${currency || ''} ${num(value).toFixed(2)}`.trim() const today = () => new Date().toISOString().slice(0, 10) const EMPTY = { category: '快递费', amount: '', currency: 'CNY', rate_to_order: '', spent_at: today(), payee: '', voucher_no: '', is_paid: false, note: '' } function OrderExpenses({ order, onClose, onChanged }) { const { user } = useAuth() const canWrite = hasPermission(user, 'order.expense') const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [form, setForm] = useState(EMPTY) const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [editingId, setEditingId] = useState(null) const orderCurrency = String(order?.currency || 'USD').toUpperCase() const crossCurrency = String(form.currency || '').toUpperCase() !== orderCurrency const load = useCallback(async () => { setLoading(true) try { const { data: payload } = await axios.get(`/api/orders/${order.id}/expenses`) setData(payload) } catch (err) { setError(err?.response?.data?.error || '读取支出失败') } finally { setLoading(false) } }, [order.id]) useEffect(() => { load() }, [load]) const summary = data?.summary /* 毛利口径写清楚:这里减掉的只是杂费,没减货款采购成本 —— 标签上必须注明,否则会被当成真毛利去报价。 */ const afterExpense = useMemo( () => (summary ? num(order.total_amount) - num(summary.total) : null), [summary, order.total_amount] ) const reset = () => { setForm({ ...EMPTY, currency: 'CNY' }); setEditingId(null); setError('') } const submit = async event => { event.preventDefault() setSaving(true) setError('') try { const body = { ...form, amount: Number(form.amount), rate_to_order: crossCurrency ? Number(form.rate_to_order) : 1, supplier_id: null } if (editingId) await axios.patch(`/api/orders/${order.id}/expenses/${editingId}`, body) else await axios.post(`/api/orders/${order.id}/expenses`, body) reset() await load() onChanged?.() } catch (err) { setError(err?.response?.data?.error || '保存失败') } finally { setSaving(false) } } const startEdit = row => { setEditingId(row.id) setError('') setForm({ category: row.category, amount: String(row.amount ?? ''), currency: row.currency, rate_to_order: String(row.rate_to_order ?? ''), spent_at: row.spent_at || today(), payee: row.payee || '', voucher_no: row.voucher_no || '', is_paid: !!row.is_paid, note: row.note || '' }) } const remove = async row => { if (!window.confirm(`删除这笔「${row.category} ${fmt(row.amount, row.currency)}」?`)) return try { await axios.delete(`/api/orders/${order.id}/expenses/${row.id}`) if (editingId === row.id) reset() await load() onChanged?.() } catch (err) { setError(err?.response?.data?.error || '删除失败') } } const togglePaid = async row => { try { await axios.patch(`/api/orders/${order.id}/expenses/${row.id}`, { is_paid: row.is_paid ? 0 : 1 }) await load() onChanged?.() } catch (err) { setError(err?.response?.data?.error || '保存失败') } } const set = (key, value) => setForm(prev => ({ ...prev, [key]: value })) return ( {loading ?

读取中…

: ( <>
杂费合计 {fmt(summary?.total, orderCurrency)}
其中未付 0 ? 'oe-unpaid' : ''}>{fmt(summary?.unpaid, orderCurrency)}
订单金额 − 杂费 {fmt(afterExpense, orderCurrency)} 未扣货款采购成本
{!!summary?.count && (
{Object.entries(summary.byCategory).map(([name, value]) => ( {name} {num(value).toFixed(2)} ))}
)} {error &&

{error}

} {canWrite && (
{editingId ? '修改这笔支出' : '记一笔支出'}
{/* 跨币种才出现:同币种硬填汇率只会填错 */} {crossCurrency && ( )}
{editingId && }
)} {!data?.expenses?.length ? ( ) : data.expenses.map(row => ( ))}
日期类型金额 折 {orderCurrency}收款方 / 单号状态
还没记过支出。快递费、吊卡费都记在这儿。
{row.spent_at || '—'} {row.category} {fmt(row.amount, row.currency)} {num(row.amount_in_order_currency).toFixed(2)} {row.payee || '—'} {row.voucher_no &&
{row.voucher_no}
} {row.note &&
{row.note}
}
{canWrite && <> }
)}
) } export default OrderExpenses