Files
keeppay/components/consumption-dialog.tsx
T
root 5ddd93038c feat(fund-pool): 添加资金池管理功能
- 新增资金池分配、余额、配置和支出相关的 API 路由
- 添加资金池支出对话框和页面组件
- 更新相关依赖,支持新功能

这些更改为资金池管理提供了完整的功能支持。
2026-05-12 21:43:58 +08:00

284 lines
13 KiB
TypeScript

'use client'
import { useState, useMemo, useEffect, useRef } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { DatePicker } from '@/components/date-picker'
import { format } from 'date-fns'
import type { ConsumptionRecord, Waiter, Reservation, CommissionItem, ConsumptionState } from '@/lib/types'
interface ConsumptionDialogProps {
open: boolean
onOpenChange: (v: boolean) => void
onSave: (record: Omit<ConsumptionRecord, 'id'>) => void
editRecord?: ConsumptionRecord | null
defaultValues?: Partial<ConsumptionRecord>
}
const today = new Date()
const todayStr = today.toISOString().slice(0, 10)
const nowStr = `${String(today.getHours()).padStart(2, '0')}:${String(today.getMinutes()).padStart(2, '0')}`
export function ConsumptionDialog({ open, onOpenChange, onSave, editRecord, defaultValues }: ConsumptionDialogProps) {
const [customerName, setCustomerName] = useState('')
const [date, setDate] = useState(todayStr)
const [time, setTime] = useState(nowStr)
const [waiterId, setWaiterId] = useState('')
const [amount, setAmount] = useState('')
const [state, setState] = useState<ConsumptionState>('待分账')
useEffect(() => {
if (editRecord) {
setCustomerName(editRecord.customerName)
setDate(editRecord.date)
setTime(editRecord.time)
setWaiterId(editRecord.waiterId)
setAmount(String(editRecord.amount))
setState(editRecord.state || '待分账')
} else if (defaultValues) {
setCustomerName(defaultValues.customerName || '')
setDate(defaultValues.date || todayStr)
setTime(defaultValues.time || nowStr)
setWaiterId(defaultValues.waiterId || '')
setAmount(defaultValues.amount ? String(defaultValues.amount) : '')
setState(defaultValues.state || '待分账')
} else {
setCustomerName('')
setDate(todayStr)
setTime(nowStr)
setWaiterId('')
setAmount('')
setState('待分账')
}
}, [editRecord, open, defaultValues])
const [waiters, setWaiters] = useState<Waiter[]>([])
const [reservations, setReservations] = useState<Reservation[]>([])
const [commissions, setCommissions] = useState<CommissionItem[]>([])
const [showSuggestions, setShowSuggestions] = useState(false)
const nameInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
Promise.all([
fetch('/api/waiter').then((r) => r.json()),
fetch('/api/reservation').then((r) => r.json()),
fetch('/api/commission').then((r) => r.json()),
])
.then(([w, r, c]) => {
setWaiters(w)
setReservations(r)
setCommissions(c)
})
.catch(() => {})
}, [open])
const customerNames = useMemo(() => {
const names = new Set(reservations.map((r) => r.customerName))
return Array.from(names)
}, [reservations])
const filteredSuggestions = useMemo(() => {
if (!customerName) return customerNames.slice(0, 5)
return customerNames.filter((n) => n.includes(customerName)).slice(0, 5)
}, [customerName, customerNames])
const selectedNameData = useMemo(() => {
if (!customerName) return null
return reservations.find((r) => r.customerName === customerName)
}, [customerName, reservations])
const selectedWaiter = useMemo(() => waiters.find((w) => w.id === waiterId), [waiterId, waiters])
const numAmount = Number(amount) || 0
const handleSelectName = (name: string) => {
setCustomerName(name)
setShowSuggestions(false)
const record = reservations.find((r) => r.customerName === name)
if (record) {
setDate(record.date)
setTime(record.time)
}
}
const handleSave = () => {
if (!customerName || !waiterId || !amount) return
const waiter = waiters.find((w) => w.id === waiterId)
if (!waiter) return
const amt = Number(amount)
if (isNaN(amt) || amt <= 0) return
onSave({
customerName,
date,
time,
waiterId: waiter.id,
waiterName: waiter.name,
commissionRate: waiter.commissionRate,
amount: amt,
waiterEarnings: (amt * waiter.commissionRate) / 100,
adminEarnings: commissions.reduce((s, c) => s + (amt * c.rate) / 100, 0),
state,
})
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{editRecord ? '编辑消费记录' : '新增消费记录'}</DialogTitle>
</DialogHeader>
{/* Commission cards grid at top */}
<div className="grid grid-cols-2 gap-3">
<Card>
<CardContent className="p-3">
<div className="text-xs text-muted-foreground">
{selectedWaiter ? selectedWaiter.name : '服务员'}
</div>
<div className="text-lg font-semibold mt-0.5">
¥
{selectedWaiter && numAmount > 0
? ((numAmount * selectedWaiter.commissionRate) / 100).toLocaleString()
: '0'}
</div>
<div className="text-xs text-muted-foreground">
{selectedWaiter ? `比例 ${selectedWaiter.commissionRate}%` : '未选择'}
</div>
</CardContent>
</Card>
{commissions.map((c) => {
const earn = numAmount > 0 ? (numAmount * c.rate) / 100 : 0
return (
<Card key={c.id}>
<CardContent className="p-3">
<div className="text-xs text-muted-foreground">{c.name}</div>
<div className="text-lg font-semibold mt-0.5">¥{earn.toLocaleString()}</div>
<div className="text-xs text-muted-foreground"> {c.rate}%</div>
</CardContent>
</Card>
)
})}
</div>
{/* Form */}
<div className="space-y-4">
<div className="grid grid-cols-4 items-center gap-4 relative">
<Label className="text-right"> *</Label>
<div className="col-span-3 relative">
<Input
ref={nameInputRef}
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
onFocus={() => setShowSuggestions(true)}
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
placeholder="输入或选择顾客姓名"
/>
{showSuggestions && filteredSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 z-50 mt-1 rounded-md border bg-popover shadow-md">
<div className="py-1">
{filteredSuggestions.map((name) => (
<button
key={name}
type="button"
className="w-full px-3 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
onMouseDown={() => handleSelectName(name)}
>
{name}
</button>
))}
</div>
</div>
)}
</div>
</div>
{selectedNameData && (
<div className="grid grid-cols-4 items-center gap-4">
<div className="col-start-2 col-span-3 text-xs text-muted-foreground bg-muted rounded px-3 py-1.5">
: {selectedNameData.date} {selectedNameData.time}
{selectedNameData.package ? ` · ${selectedNameData.package}` : ''}
</div>
</div>
)}
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right"> *</Label>
<div className="col-span-3">
<DatePicker
value={date ? new Date(date + 'T00:00:00') : undefined}
onChange={(d) => {
if (d) setDate(format(d, 'yyyy-MM-dd'))
}}
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right"> *</Label>
<Input
className="col-span-3"
type="time"
value={time}
onChange={(e) => setTime(e.target.value)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right"> *</Label>
<div className="col-span-3">
<Select value={waiterId} onValueChange={setWaiterId}>
<SelectTrigger>
<SelectValue placeholder="请选择服务员" />
</SelectTrigger>
<SelectContent>
{waiters.map((w) => (
<SelectItem key={w.id} value={w.id}>
{w.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right"> *</Label>
<div className="col-span-3 flex items-center gap-2">
<Input
type="number"
min={0}
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right"></Label>
<div className="col-span-3">
<Select value={state} onValueChange={(v) => setState(v as ConsumptionState)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="待分账"></SelectItem>
<SelectItem value="已分账"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave} disabled={!customerName || !waiterId || !amount}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}