'use client' /** * @description Point of Sale (POS) interface for processing service and product sales with multiple payment methods * @audit BUSINESS RULE: POS handles service/product sales with cash, card, transfer, giftcard, and membership payments * @audit SECURITY: Requires authenticated staff member (cashier) via useAuth hook * @audit Validate: Payment amounts must match cart total before processing * @audit AUDIT: All sales transactions logged through /api/aperture/pos endpoint * @audit PERFORMANCE: Optimized for touch interface with large touch targets */ import { useState, useEffect } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' import { ShoppingCart, Plus, Minus, Trash2, CreditCard, DollarSign, Banknote, Smartphone, Gift, Receipt, Calculator } from 'lucide-react' import { format } from 'date-fns' import { es } from 'date-fns/locale' import { useAuth } from '@/lib/auth/context' interface POSItem { id: string type: 'service' | 'product' name: string price: number quantity: number category?: string } interface Payment { method: 'cash' | 'card' | 'transfer' | 'giftcard' | 'membership' amount: number reference?: string } interface SaleResult { id: string subtotal: number total: number payments: Payment[] items: POSItem[] receipt: any } /** * @description Point of Sale component with cart management, customer selection, and multi-payment support * @returns {JSX.Element} Complete POS interface with service/product catalog, cart, and payment processing * @audit BUSINESS RULE: Cart items can be services or products with quantity management * @audit BUSINESS RULE: Multiple partial payments supported (split payments) * @audit SECURITY: Requires authenticated staff member; validates user permissions * @audit Validate: Cart cannot be empty when processing payment * @audit Validate: Payment total must equal or exceed cart subtotal * @audit PERFORMANCE: Auto-fetches services, products, and customers on mount * @audit AUDIT: Sales processed through /api/aperture/pos with full transaction logging */ export default function POSSystem() { const { user } = useAuth() const [cart, setCart] = useState([]) const [services, setServices] = useState([]) const [products, setProducts] = useState([]) const [customers, setCustomers] = useState([]) const [selectedCustomer, setSelectedCustomer] = useState('') const [paymentDialogOpen, setPaymentDialogOpen] = useState(false) const [payments, setPayments] = useState([]) const [currentPayment, setCurrentPayment] = useState>({ method: 'cash', amount: 0 }) const [receipt, setReceipt] = useState(null) const [receiptDialogOpen, setReceiptDialogOpen] = useState(false) const [loading, setLoading] = useState(false) useEffect(() => { fetchServices() fetchProducts() fetchCustomers() }, []) const fetchServices = async () => { try { const response = await fetch('/api/services') const data = await response.json() if (data.success) { setServices(data.services || []) } } catch (error) { console.error('Error fetching services:', error) } } const fetchProducts = async () => { // For now, we'll simulate products setProducts([ { id: 'prod-1', name: 'Shampoo Premium', price: 250, category: 'hair' }, { id: 'prod-2', name: 'Tratamiento Facial', price: 180, category: 'facial' }, { id: 'prod-3', name: 'Esmalte', price: 45, category: 'nails' } ]) } const fetchCustomers = async () => { try { const response = await fetch('/api/customers?limit=50') const data = await response.json() if (data.success) { setCustomers(data.customers || []) } } catch (error) { console.error('Error fetching customers:', error) } } const addToCart = (item: any, type: 'service' | 'product') => { const cartItem: POSItem = { id: item.id, type, name: item.name, price: item.base_price || item.price, quantity: 1, category: item.category } setCart(prev => { const existing = prev.find(i => i.id === item.id && i.type === type) if (existing) { return prev.map(i => i.id === item.id && i.type === type ? { ...i, quantity: i.quantity + 1 } : i ) } return [...prev, cartItem] }) } const updateQuantity = (itemId: string, type: 'service' | 'product', quantity: number) => { if (quantity <= 0) { removeFromCart(itemId, type) return } setCart(prev => prev.map(item => item.id === itemId && item.type === type ? { ...item, quantity } : item ) ) } const removeFromCart = (itemId: string, type: 'service' | 'product') => { setCart(prev => prev.filter(item => !(item.id === itemId && item.type === type))) } const getSubtotal = () => { return cart.reduce((sum, item) => sum + (item.price * item.quantity), 0) } const getTotal = () => { return getSubtotal() // Add tax/discount logic here if needed } const addPayment = () => { if (!currentPayment.method || !currentPayment.amount) return setPayments(prev => [...prev, currentPayment as Payment]) setCurrentPayment({ method: 'cash', amount: 0 }) } const removePayment = (index: number) => { setPayments(prev => prev.filter((_, i) => i !== index)) } const getTotalPayments = () => { return payments.reduce((sum, payment) => sum + payment.amount, 0) } const getRemainingAmount = () => { return Math.max(0, getTotal() - getTotalPayments()) } const processSale = async () => { if (cart.length === 0 || payments.length === 0) { alert('Agregue items al carrito y configure los pagos') return } if (getRemainingAmount() > 0.01) { alert('El total de pagos no cubre el monto total') return } setLoading(true) try { const saleData = { customer_id: selectedCustomer || null, items: cart, payments, notes: `Venta procesada en POS - ${format(new Date(), 'dd/MM/yyyy HH:mm', { locale: es })}` } const response = await fetch('/api/aperture/pos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(saleData) }) const data = await response.json() if (data.success) { setReceipt(data.transaction) setReceiptDialogOpen(true) // Reset state setCart([]) setPayments([]) setSelectedCustomer('') setPaymentDialogOpen(false) } else { alert(data.error || 'Error procesando la venta') } } catch (error) { console.error('Error processing sale:', error) alert('Error procesando la venta') } finally { setLoading(false) } } const printReceipt = () => { // Simple print functionality window.print() } const getPaymentMethodIcon = (method: string) => { switch (method) { case 'cash': return case 'card': return case 'transfer': return case 'giftcard': return case 'membership': return default: return } } const formatCurrency = (amount: number) => { return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount) } if (!user) return null return (

Punto de Venta

Sistema completo de ventas y cobros

{cart.length} items
{/* Products/Services Selection */}
{/* Services */} Servicios Disponibles Seleccione servicios para agregar al carrito
{services.slice(0, 9).map(service => ( ))}
{/* Products */} Productos Artículos disponibles para venta
{products.map(product => ( ))}
{/* Cart and Checkout */}
{/* Customer Selection */} Cliente {/* Cart */} Carrito de Compras {cart.length === 0 ? (

El carrito está vacío

) : (
{cart.map((item, index) => (
{item.name}
{formatCurrency(item.price)} × {item.quantity}
{item.quantity}
))}
Total: {formatCurrency(getTotal())}
)}
{/* Payment Dialog */} Procesar Pago Configure los métodos de pago para total: {formatCurrency(getTotal())}
{/* Current Payments */} {payments.length > 0 && (

Pagos Configurados:

{payments.map((payment, index) => (
{getPaymentMethodIcon(payment.method)} {payment.method} {payment.reference && ( ({payment.reference}) )}
{formatCurrency(payment.amount)}
))}
)} {/* Add Payment */}

Agregar Pago:

setCurrentPayment({...currentPayment, amount: parseFloat(e.target.value) || 0})} placeholder={getRemainingAmount().toFixed(2)} />
{(currentPayment.method === 'card' || currentPayment.method === 'transfer') && (
setCurrentPayment({...currentPayment, reference: e.target.value})} placeholder="Número de autorización" />
)}
{/* Payment Summary */}
Total a pagar: {formatCurrency(getTotal())}
Pagado: {formatCurrency(getTotalPayments())}
Restante: 0 ? 'text-red-600' : 'text-green-600'}> {formatCurrency(getRemainingAmount())}
{/* Receipt Dialog */} Recibo de Venta {receipt && (
ANCHOR:23
{format(new Date(), 'dd/MM/yyyy HH:mm', { locale: es })}
Recibo #{receipt.id}
{receipt.items?.map((item: POSItem, index: number) => (
{item.name} × {item.quantity} {formatCurrency(item.price * item.quantity)}
))}
Total: {formatCurrency(receipt.total)}
{receipt.payments?.map((payment: Payment, index: number) => (
{payment.method}: {formatCurrency(payment.amount)}
))}
¡Gracias por su preferencia!
)}
) }