368 lines
12 KiB
TypeScript
368 lines
12 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt } from 'lucide-react';
|
||
|
|
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||
|
|
import { PageHeader } from '../../../../components/layout';
|
||
|
|
|
||
|
|
const POSPage: React.FC = () => {
|
||
|
|
const [cart, setCart] = useState<Array<{
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
price: number;
|
||
|
|
quantity: number;
|
||
|
|
category: string;
|
||
|
|
}>>([]);
|
||
|
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||
|
|
const [customerInfo, setCustomerInfo] = useState({
|
||
|
|
name: '',
|
||
|
|
email: '',
|
||
|
|
phone: '',
|
||
|
|
});
|
||
|
|
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
|
||
|
|
const [cashReceived, setCashReceived] = useState('');
|
||
|
|
|
||
|
|
const products = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
name: 'Pan de Molde Integral',
|
||
|
|
price: 4.50,
|
||
|
|
category: 'bread',
|
||
|
|
stock: 25,
|
||
|
|
image: '/api/placeholder/100/100',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
name: 'Croissants de Mantequilla',
|
||
|
|
price: 1.50,
|
||
|
|
category: 'pastry',
|
||
|
|
stock: 32,
|
||
|
|
image: '/api/placeholder/100/100',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3',
|
||
|
|
name: 'Baguette Francesa',
|
||
|
|
price: 2.80,
|
||
|
|
category: 'bread',
|
||
|
|
stock: 18,
|
||
|
|
image: '/api/placeholder/100/100',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '4',
|
||
|
|
name: 'Tarta de Chocolate',
|
||
|
|
price: 25.00,
|
||
|
|
category: 'cake',
|
||
|
|
stock: 8,
|
||
|
|
image: '/api/placeholder/100/100',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '5',
|
||
|
|
name: 'Magdalenas',
|
||
|
|
price: 0.75,
|
||
|
|
category: 'pastry',
|
||
|
|
stock: 48,
|
||
|
|
image: '/api/placeholder/100/100',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '6',
|
||
|
|
name: 'Empanadas',
|
||
|
|
price: 2.50,
|
||
|
|
category: 'other',
|
||
|
|
stock: 24,
|
||
|
|
image: '/api/placeholder/100/100',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const categories = [
|
||
|
|
{ id: 'all', name: 'Todos' },
|
||
|
|
{ id: 'bread', name: 'Panes' },
|
||
|
|
{ id: 'pastry', name: 'Bollería' },
|
||
|
|
{ id: 'cake', name: 'Tartas' },
|
||
|
|
{ id: 'other', name: 'Otros' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const filteredProducts = products.filter(product =>
|
||
|
|
selectedCategory === 'all' || product.category === selectedCategory
|
||
|
|
);
|
||
|
|
|
||
|
|
const addToCart = (product: typeof products[0]) => {
|
||
|
|
setCart(prevCart => {
|
||
|
|
const existingItem = prevCart.find(item => item.id === product.id);
|
||
|
|
if (existingItem) {
|
||
|
|
return prevCart.map(item =>
|
||
|
|
item.id === product.id
|
||
|
|
? { ...item, quantity: item.quantity + 1 }
|
||
|
|
: item
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
return [...prevCart, {
|
||
|
|
id: product.id,
|
||
|
|
name: product.name,
|
||
|
|
price: product.price,
|
||
|
|
quantity: 1,
|
||
|
|
category: product.category,
|
||
|
|
}];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const updateQuantity = (id: string, quantity: number) => {
|
||
|
|
if (quantity <= 0) {
|
||
|
|
setCart(prevCart => prevCart.filter(item => item.id !== id));
|
||
|
|
} else {
|
||
|
|
setCart(prevCart =>
|
||
|
|
prevCart.map(item =>
|
||
|
|
item.id === id ? { ...item, quantity } : item
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const clearCart = () => {
|
||
|
|
setCart([]);
|
||
|
|
};
|
||
|
|
|
||
|
|
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||
|
|
const taxRate = 0.21; // 21% IVA
|
||
|
|
const tax = subtotal * taxRate;
|
||
|
|
const total = subtotal + tax;
|
||
|
|
const change = cashReceived ? Math.max(0, parseFloat(cashReceived) - total) : 0;
|
||
|
|
|
||
|
|
const processPayment = () => {
|
||
|
|
if (cart.length === 0) return;
|
||
|
|
|
||
|
|
// Process payment logic here
|
||
|
|
console.log('Processing payment:', {
|
||
|
|
cart,
|
||
|
|
customerInfo,
|
||
|
|
paymentMethod,
|
||
|
|
total,
|
||
|
|
cashReceived: paymentMethod === 'cash' ? parseFloat(cashReceived) : undefined,
|
||
|
|
change: paymentMethod === 'cash' ? change : undefined,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Clear cart after successful payment
|
||
|
|
setCart([]);
|
||
|
|
setCustomerInfo({ name: '', email: '', phone: '' });
|
||
|
|
setCashReceived('');
|
||
|
|
|
||
|
|
alert('Venta procesada exitosamente');
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-6 h-screen flex flex-col">
|
||
|
|
<PageHeader
|
||
|
|
title="Punto de Venta"
|
||
|
|
description="Sistema de ventas integrado"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
|
||
|
|
{/* Products Section */}
|
||
|
|
<div className="lg:col-span-2 space-y-6">
|
||
|
|
{/* Categories */}
|
||
|
|
<div className="flex space-x-2 overflow-x-auto">
|
||
|
|
{categories.map(category => (
|
||
|
|
<Button
|
||
|
|
key={category.id}
|
||
|
|
variant={selectedCategory === category.id ? 'default' : 'outline'}
|
||
|
|
onClick={() => setSelectedCategory(category.id)}
|
||
|
|
className="whitespace-nowrap"
|
||
|
|
>
|
||
|
|
{category.name}
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Products Grid */}
|
||
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||
|
|
{filteredProducts.map(product => (
|
||
|
|
<Card
|
||
|
|
key={product.id}
|
||
|
|
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
|
||
|
|
onClick={() => addToCart(product)}
|
||
|
|
>
|
||
|
|
<img
|
||
|
|
src={product.image}
|
||
|
|
alt={product.name}
|
||
|
|
className="w-full h-20 object-cover rounded mb-3"
|
||
|
|
/>
|
||
|
|
<h3 className="font-medium text-sm mb-1 line-clamp-2">{product.name}</h3>
|
||
|
|
<p className="text-lg font-bold text-[var(--color-success)]">€{product.price.toFixed(2)}</p>
|
||
|
|
<p className="text-xs text-[var(--text-tertiary)]">Stock: {product.stock}</p>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Cart and Checkout Section */}
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* Cart */}
|
||
|
|
<Card className="p-6">
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<h3 className="text-lg font-semibold flex items-center">
|
||
|
|
<ShoppingCart className="w-5 h-5 mr-2" />
|
||
|
|
Carrito ({cart.length})
|
||
|
|
</h3>
|
||
|
|
{cart.length > 0 && (
|
||
|
|
<Button variant="outline" size="sm" onClick={clearCart}>
|
||
|
|
Limpiar
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-3 max-h-64 overflow-y-auto">
|
||
|
|
{cart.length === 0 ? (
|
||
|
|
<p className="text-[var(--text-tertiary)] text-center py-8">Carrito vacío</p>
|
||
|
|
) : (
|
||
|
|
cart.map(item => (
|
||
|
|
<div key={item.id} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded">
|
||
|
|
<div className="flex-1">
|
||
|
|
<h4 className="text-sm font-medium">{item.name}</h4>
|
||
|
|
<p className="text-xs text-[var(--text-tertiary)]">€{item.price.toFixed(2)} c/u</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
variant="outline"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
updateQuantity(item.id, item.quantity - 1);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Minus className="w-3 h-3" />
|
||
|
|
</Button>
|
||
|
|
<span className="w-8 text-center text-sm">{item.quantity}</span>
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
variant="outline"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
updateQuantity(item.id, item.quantity + 1);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Plus className="w-3 h-3" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
<div className="ml-4 text-right">
|
||
|
|
<p className="text-sm font-medium">€{(item.price * item.quantity).toFixed(2)}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{cart.length > 0 && (
|
||
|
|
<div className="mt-4 pt-4 border-t">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span>Subtotal:</span>
|
||
|
|
<span>€{subtotal.toFixed(2)}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between">
|
||
|
|
<span>IVA (21%):</span>
|
||
|
|
<span>€{tax.toFixed(2)}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between text-lg font-bold border-t pt-2">
|
||
|
|
<span>Total:</span>
|
||
|
|
<span>€{total.toFixed(2)}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Customer Info */}
|
||
|
|
<Card className="p-6">
|
||
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||
|
|
<User className="w-5 h-5 mr-2" />
|
||
|
|
Cliente (Opcional)
|
||
|
|
</h3>
|
||
|
|
<div className="space-y-3">
|
||
|
|
<Input
|
||
|
|
placeholder="Nombre"
|
||
|
|
value={customerInfo.name}
|
||
|
|
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
placeholder="Email"
|
||
|
|
type="email"
|
||
|
|
value={customerInfo.email}
|
||
|
|
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
placeholder="Teléfono"
|
||
|
|
value={customerInfo.phone}
|
||
|
|
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Payment */}
|
||
|
|
<Card className="p-6">
|
||
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||
|
|
<Calculator className="w-5 h-5 mr-2" />
|
||
|
|
Método de Pago
|
||
|
|
</h3>
|
||
|
|
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div className="grid grid-cols-3 gap-2">
|
||
|
|
<Button
|
||
|
|
variant={paymentMethod === 'cash' ? 'default' : 'outline'}
|
||
|
|
onClick={() => setPaymentMethod('cash')}
|
||
|
|
className="flex items-center justify-center"
|
||
|
|
>
|
||
|
|
<Banknote className="w-4 h-4 mr-1" />
|
||
|
|
Efectivo
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant={paymentMethod === 'card' ? 'default' : 'outline'}
|
||
|
|
onClick={() => setPaymentMethod('card')}
|
||
|
|
className="flex items-center justify-center"
|
||
|
|
>
|
||
|
|
<CreditCard className="w-4 h-4 mr-1" />
|
||
|
|
Tarjeta
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant={paymentMethod === 'transfer' ? 'default' : 'outline'}
|
||
|
|
onClick={() => setPaymentMethod('transfer')}
|
||
|
|
className="flex items-center justify-center"
|
||
|
|
>
|
||
|
|
Transferencia
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{paymentMethod === 'cash' && (
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Input
|
||
|
|
placeholder="Efectivo recibido"
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
value={cashReceived}
|
||
|
|
onChange={(e) => setCashReceived(e.target.value)}
|
||
|
|
/>
|
||
|
|
{cashReceived && parseFloat(cashReceived) >= total && (
|
||
|
|
<div className="p-2 bg-green-50 rounded text-center">
|
||
|
|
<p className="text-sm text-[var(--color-success)]">
|
||
|
|
Cambio: <span className="font-bold">€{change.toFixed(2)}</span>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Button
|
||
|
|
onClick={processPayment}
|
||
|
|
disabled={cart.length === 0 || (paymentMethod === 'cash' && (!cashReceived || parseFloat(cashReceived) < total))}
|
||
|
|
className="w-full"
|
||
|
|
size="lg"
|
||
|
|
>
|
||
|
|
<Receipt className="w-5 h-5 mr-2" />
|
||
|
|
Procesar Venta - €{total.toFixed(2)}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default POSPage;
|