275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import { useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Receipt, Users, CreditCard, Percent } from 'lucide-react';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
import { toast } from 'sonner';
|
|
|
|
interface Bill {
|
|
id: string;
|
|
tableNumber: number;
|
|
items: BillItem[];
|
|
subtotal: number;
|
|
tip: number;
|
|
total: number;
|
|
status: 'open' | 'split' | 'paid';
|
|
splits?: number;
|
|
}
|
|
|
|
interface BillItem {
|
|
name: string;
|
|
quantity: number;
|
|
price: number;
|
|
}
|
|
|
|
const BillManagement = () => {
|
|
const [bills, setBills] = useState<Bill[]>([
|
|
{
|
|
id: '1',
|
|
tableNumber: 5,
|
|
items: [
|
|
{ name: 'Paella Valenciana', quantity: 2, price: 18.50 },
|
|
{ name: 'Gazpacho', quantity: 2, price: 6.50 },
|
|
{ name: 'Vino Tinto', quantity: 1, price: 12.00 }
|
|
],
|
|
subtotal: 62.00,
|
|
tip: 6.20,
|
|
total: 68.20,
|
|
status: 'open'
|
|
},
|
|
{
|
|
id: '2',
|
|
tableNumber: 3,
|
|
items: [
|
|
{ name: 'Pulpo a la Gallega', quantity: 1, price: 16.00 }
|
|
],
|
|
subtotal: 16.00,
|
|
tip: 0,
|
|
total: 16.00,
|
|
status: 'open'
|
|
}
|
|
]);
|
|
|
|
const [selectedBill, setSelectedBill] = useState<Bill | null>(null);
|
|
const [tipPercentage, setTipPercentage] = useState('10');
|
|
const [splitCount, setSplitCount] = useState('2');
|
|
|
|
const calculateTip = (subtotal: number, percentage: string) => {
|
|
const percent = parseFloat(percentage) || 0;
|
|
return (subtotal * percent) / 100;
|
|
};
|
|
|
|
const applySplitBill = () => {
|
|
if (!selectedBill) return;
|
|
const splits = parseInt(splitCount) || 2;
|
|
const amountPerPerson = selectedBill.total / splits;
|
|
|
|
setBills(bills.map(bill =>
|
|
bill.id === selectedBill.id
|
|
? { ...bill, status: 'split', splits }
|
|
: bill
|
|
));
|
|
|
|
toast.success(`Cuenta dividida en ${splits} partes: €${amountPerPerson.toFixed(2)} por persona`);
|
|
};
|
|
|
|
const applyTip = (billId: string) => {
|
|
setBills(bills.map(bill => {
|
|
if (bill.id === billId) {
|
|
const tip = calculateTip(bill.subtotal, tipPercentage);
|
|
return {
|
|
...bill,
|
|
tip,
|
|
total: bill.subtotal + tip
|
|
};
|
|
}
|
|
return bill;
|
|
}));
|
|
toast.success(`Propina aplicada: €${calculateTip(selectedBill?.subtotal || 0, tipPercentage).toFixed(2)}`);
|
|
};
|
|
|
|
const closeBill = (billId: string) => {
|
|
setBills(bills.map(bill =>
|
|
bill.id === billId ? { ...bill, status: 'paid' } : bill
|
|
));
|
|
toast.success('Cuenta cerrada correctamente');
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{bills.filter(b => b.status !== 'paid').map((bill) => (
|
|
<Card key={bill.id}>
|
|
<CardHeader>
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-lg">Mesa {bill.tableNumber}</CardTitle>
|
|
<Badge variant={bill.status === 'split' ? 'default' : 'secondary'}>
|
|
{bill.status === 'split' ? `Dividida (${bill.splits})` : 'Abierta'}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
{bill.items.map((item, idx) => (
|
|
<div key={idx} className="flex justify-between text-sm">
|
|
<span>{item.quantity}x {item.name}</span>
|
|
<span>€{(item.quantity * item.price).toFixed(2)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-1 text-sm">
|
|
<div className="flex justify-between">
|
|
<span>Subtotal:</span>
|
|
<span>€{bill.subtotal.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-muted-foreground">
|
|
<span>Propina:</span>
|
|
<span>€{bill.tip.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between font-bold text-base pt-1">
|
|
<span>Total:</span>
|
|
<span className="text-primary">€{bill.total.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{bill.status === 'split' && (
|
|
<div className="bg-primary/10 p-2 rounded text-sm">
|
|
<div className="font-medium">Por persona:</div>
|
|
<div className="text-lg font-bold text-primary">
|
|
€{(bill.total / (bill.splits || 1)).toFixed(2)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2 pt-2">
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 gap-2"
|
|
onClick={() => setSelectedBill(bill)}
|
|
>
|
|
<Users className="h-4 w-4" />
|
|
Dividir
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Dividir Cuenta - Mesa {bill.tableNumber}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="split-count">Número de Personas</Label>
|
|
<Input
|
|
id="split-count"
|
|
type="number"
|
|
min="2"
|
|
value={splitCount}
|
|
onChange={(e) => setSplitCount(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="p-4 bg-muted rounded-lg">
|
|
<div className="text-sm text-muted-foreground">Monto por persona:</div>
|
|
<div className="text-2xl font-bold text-primary">
|
|
€{(bill.total / (parseInt(splitCount) || 2)).toFixed(2)}
|
|
</div>
|
|
</div>
|
|
<Button className="w-full" onClick={applySplitBill}>
|
|
Aplicar División
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex-1 gap-2"
|
|
onClick={() => setSelectedBill(bill)}
|
|
>
|
|
<Percent className="h-4 w-4" />
|
|
Propina
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Agregar Propina - Mesa {bill.tableNumber}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="tip-percentage">Porcentaje de Propina</Label>
|
|
<Input
|
|
id="tip-percentage"
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
value={tipPercentage}
|
|
onChange={(e) => setTipPercentage(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<Button variant="outline" onClick={() => setTipPercentage('5')}>5%</Button>
|
|
<Button variant="outline" onClick={() => setTipPercentage('10')}>10%</Button>
|
|
<Button variant="outline" onClick={() => setTipPercentage('15')}>15%</Button>
|
|
</div>
|
|
<div className="p-4 bg-muted rounded-lg">
|
|
<div className="flex justify-between text-sm mb-2">
|
|
<span>Subtotal:</span>
|
|
<span>€{bill.subtotal.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm mb-2">
|
|
<span>Propina ({tipPercentage}%):</span>
|
|
<span>€{calculateTip(bill.subtotal, tipPercentage).toFixed(2)}</span>
|
|
</div>
|
|
<Separator className="my-2" />
|
|
<div className="flex justify-between">
|
|
<span className="font-bold">Total:</span>
|
|
<span className="text-xl font-bold text-primary">
|
|
€{(bill.subtotal + calculateTip(bill.subtotal, tipPercentage)).toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<Button className="w-full" onClick={() => applyTip(bill.id)}>
|
|
Aplicar Propina
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
<Button
|
|
className="w-full gap-2"
|
|
onClick={() => closeBill(bill.id)}
|
|
>
|
|
<CreditCard className="h-4 w-4" />
|
|
Cerrar Cuenta
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{bills.filter(b => b.status !== 'paid').length === 0 && (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
<Receipt className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>No hay cuentas abiertas</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BillManagement;
|