import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, doc, setDoc, addDoc, getDocs, onSnapshot, query, serverTimestamp } from 'firebase/firestore'; // --- CONFIGURACIÓN E INICIALIZACIÓN DE FIREBASE --- const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "mock-api-key", authDomain: "mock-auth-domain", projectId: "mock-project-id", storageBucket: "mock-storage-bucket", messagingSenderId: "mock-sender-id", appId: "mock-app-id" }; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'polleria-pos-app'; // --- MENÚ DE PRODUCTOS Y PRECIOS --- const MENU_ITEMS = { pollo: [ { id: 'broaster', name: 'Pollo Broaster', icon: '🍗', basePrice: 23 }, { id: 'espiedo', name: 'Pollo al Espiedo', icon: '🔥', basePrice: 22 }, { id: 'milanesa', name: 'Milanesa de Pollo', icon: '🥩', basePrice: 20 }, ], portions: [ { id: 'economico', name: 'Económico', factor: 0.65, label: 'Económico' }, { id: 'cuarto', name: 'Cuarto de Pollo', factor: 1.0, label: '1/4 Pollo' }, { id: 'medio', name: 'Medio Pollo', factor: 1.9, label: '1/2 Pollo' }, { id: 'entero', name: 'Pollo Entero', factor: 3.6, label: '1 Pollo Entero' }, ], drinks: [ { id: 'mocochinchi_v', name: 'Mocochinchi (Vaso)', price: 5, icon: '🥤' }, { id: 'mocochinchi_j', name: 'Mocochinchi (Jarra)', price: 15, icon: '🍯' }, { id: 'gaseosa_p', name: 'Gaseosa Personal', price: 6, icon: '🍼' }, { id: 'gaseosa_2l', name: 'Gaseosa 2 Litros', price: 15, icon: '🍾' }, { id: 'porcion_papas', name: 'Porción Extra Papas', price: 7, icon: '🍟' }, { id: 'porcion_arroz', name: 'Porción Extra Arroz', price: 5, icon: '🍚' }, ] }; export default function App() { // --- ESTADOS DE AUTH Y CONEXIÓN --- const [user, setUser] = useState(null); const [loadingDb, setLoadingDb] = useState(true); // --- ESTADOS DE LA APLICACIÓN --- const [activeTab, setActiveTab] = useState('pos'); // 'pos' o 'caja' const [sales, setSales] = useState([]); const [ticketCounter, setTicketCounter] = useState(100); // --- FORMULARIO DE CLIENTE --- const [clientName, setClientName] = useState(''); const [clientCI, setClientCI] = useState(''); // --- SELECCIONES ACTUALES --- const [selectedChicken, setSelectedChicken] = useState('broaster'); // ID del pollo const [selectedPortion, setSelectedPortion] = useState('cuarto'); // ID de porción const [selectedDrinks, setSelectedDrinks] = useState({}); // { [id]: cantidad } const [notes, setNotes] = useState(''); const [serviceType, setServiceType] = useState('mesa'); // 'mesa' o 'llevar' // --- ESTADO DE NOTIFICACIONES --- const [notification, setNotification] = useState(null); const [ticketToPrint, setTicketToPrint] = useState(null); const [showPrintModal, setShowPrintModal] = useState(false); const [showConfirmResetModal, setShowConfirmResetModal] = useState(false); // --- EFECTO 1: INICIALIZAR AUTENTICACIÓN --- useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Error de autenticación Firebase:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (usr) => { setUser(usr); }); return () => unsubscribe(); }, []); // --- EFECTO 2: CARGAR DATOS DESDE FIRESTORE --- useEffect(() => { if (!user) return; setLoadingDb(true); // Ruta estricta de datos públicos según REGLA 1: /artifacts/{appId}/public/data/{collectionName} const salesCollectionRef = collection(db, 'artifacts', appId, 'public', 'data', 'sales'); // Suscribirse a cambios en tiempo real con control de errores requerido const unsubscribe = onSnapshot(salesCollectionRef, (snapshot) => { const salesList = []; snapshot.forEach((doc) => { const data = doc.data(); salesList.push({ id: doc.id, ...data, // Convertir timestamps de firebase createdAt: data.createdAt?.seconds ? new Date(data.createdAt.seconds * 1000) : new Date() }); }); // Ordenar localmente por fecha de manera descendente (Evitando compound queries en Firestore - REGLA 2) salesList.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); setSales(salesList); // Calcular el correlativo de fichas en base a la cantidad de ventas del día if (salesList.length > 0) { setTicketCounter(100 + salesList.length + 1); } else { setTicketCounter(101); } setLoadingDb(false); }, (error) => { console.error("Error obteniendo ventas de Firestore:", error); showToast("Error de conexión con la base de datos.", "error"); setLoadingDb(false); }); return () => unsubscribe(); }, [user]); // --- AUXILIARES Y CÁLCULOS --- const showToast = (message, type = 'success') => { setNotification({ message, type }); setTimeout(() => { setNotification(null); }, 4000); }; // Calcular precio del pollo seleccionado según la porción const getChickenPrice = () => { const pollo = MENU_ITEMS.pollo.find(p => p.id === selectedChicken); const portion = MENU_ITEMS.portions.find(p => p.id === selectedPortion); if (!pollo || !portion) return 0; return Math.round(pollo.basePrice * portion.factor); }; // Calcular precio de bebidas extras añadidas const getDrinksTotal = () => { let total = 0; Object.keys(selectedDrinks).forEach(drinkId => { const qty = selectedDrinks[drinkId] || 0; const drink = MENU_ITEMS.drinks.find(d => d.id === drinkId); if (drink && qty > 0) { total += drink.price * qty; } }); return total; }; const getGrandTotal = () => { return getChickenPrice() + getDrinksTotal(); }; // Agregar / restar bebidas extras const handleDrinkQuantity = (drinkId, amount) => { setSelectedDrinks(prev => { const current = prev[drinkId] || 0; const next = current + amount; if (next <= 0) { const copy = { ...prev }; delete copy[drinkId]; return copy; } return { ...prev, [drinkId]: next }; }); }; // Reiniciar formulario de POS const resetForm = () => { setClientName(''); setClientCI(''); setSelectedChicken('broaster'); setSelectedPortion('cuarto'); setSelectedDrinks({}); setNotes(''); setServiceType('mesa'); }; // Registrar venta en Firestore const handleRegisterSale = async (e) => { e.preventDefault(); if (!user) { showToast("No se ha podido autenticar la sesión.", "error"); return; } const chickenItem = MENU_ITEMS.pollo.find(p => p.id === selectedChicken); const portionItem = MENU_ITEMS.portions.find(p => p.id === selectedPortion); // Preparar el desglose del pedido const chickenDetails = { id: selectedChicken, name: chickenItem.name, portion: portionItem.label, price: getChickenPrice() }; const extrasList = []; Object.keys(selectedDrinks).forEach(drinkId => { const qty = selectedDrinks[drinkId]; const item = MENU_ITEMS.drinks.find(d => d.id === drinkId); if (item && qty > 0) { extrasList.push({ id: drinkId, name: item.name, quantity: qty, price: item.price, subtotal: item.price * qty }); } }); const newSale = { ticketNumber: ticketCounter, clientName: clientName.trim() || 'Sin Nombre (S/N)', clientCI: clientCI.trim() || '0', chicken: chickenDetails, extras: extrasList, notes: notes.trim(), serviceType: serviceType, totalAmount: getGrandTotal(), createdAt: serverTimestamp(), sellerId: user.uid }; try { // Registrar en la base de datos siguiendo REGLA 1 const salesCollectionRef = collection(db, 'artifacts', appId, 'public', 'data', 'sales'); await addDoc(salesCollectionRef, newSale); // Almacenar localmente los datos de la ficha activa para la simulación de impresión setTicketToPrint({ ...newSale, createdAt: new Date() // Usar fecha local inmediata para el ticket }); showToast(`¡Ficha Nº ${ticketCounter} registrada con éxito!`); setShowPrintModal(true); resetForm(); } catch (error) { console.error("Error al registrar venta:", error); showToast("Error de conexión. No se pudo guardar la venta.", "error"); } }; // Función de impresión nativa (Utilizando estilos @media print controlados) const triggerPrint = () => { window.print(); }; // Generar estadísticas agregadas en memoria (Evitando querys complejas - REGLA 2) const getCajaStatistics = () => { let totalRevenue = 0; let totalTickets = sales.length; let broasterCount = 0; let espiedoCount = 0; let milanesaCount = 0; let itemsPorcion = { economico: 0, cuarto: 0, medio: 0, entero: 0 }; sales.forEach(sale => { totalRevenue += sale.totalAmount || 0; // Contar categorías de pollo if (sale.chicken?.id === 'broaster') broasterCount++; if (sale.chicken?.id === 'espiedo') espiedoCount++; if (sale.chicken?.id === 'milanesa') milanesaCount++; // Contar tipos de porciones const portionName = sale.chicken?.portion?.toLowerCase(); if (portionName?.includes('econó') || portionName?.includes('econo')) itemsPorcion.economico++; if (portionName?.includes('1/4') || portionName?.includes('cuarto')) itemsPorcion.cuarto++; if (portionName?.includes('1/2') || portionName?.includes('medio')) itemsPorcion.medio++; if (portionName?.includes('entero') || portionName?.includes('1 pollo')) itemsPorcion.entero++; }); return { totalRevenue, totalTickets, broasterCount, espiedoCount, milanesaCount, itemsPorcion }; }; const stats = getCajaStatistics(); return (
{/* --- ESTILOS COMPLEMENTARIOS PARA IMPRESIÓN --- */} {/* --- BARRA DE NOTIFICACIÓN DE TOASTS --- */} {notification && (
{notification.type === 'error' ? '⚠️' : '🍗'}
{notification.message}
)} {/* --- CABECERA PRINCIPAL (Ocultada al imprimir) --- */}
{/* Logo de la pollería */}
🍗

SABOR DORADO SISTEMA POS

Control de Caja y Fichas Térmicas

{/* Selector de pestañas */}
{/* Indicador de estado de base de datos */}
{loadingDb ? 'Sincronizando...' : (user ? 'Servidor Nube Activo' : 'Desconectado')}
{/* --- CONTENIDO PRINCIPAL --- */}
{/* PESTAÑA 1: NUEVA FICHA / PUNTO DE VENTA */} {activeTab === 'pos' && (
{/* Formulario e Interfaz de Selección (Columna Izquierda - 7 de 12cols) */}
{/* Bloque 1: Datos del Cliente */}

👤 Datos del Cliente

setClientName(e.target.value)} className="w-full bg-neutral-900 border border-neutral-800 focus:border-amber-500 rounded-xl px-4 py-3 text-sm text-white placeholder-neutral-600 focus:outline-none transition duration-200" />
setClientCI(e.target.value)} className="w-full bg-neutral-900 border border-neutral-800 focus:border-amber-500 rounded-xl px-4 py-3 text-sm text-white placeholder-neutral-600 focus:outline-none transition duration-200" />
{/* Bloque 2: Selección de Plato de Pollo */}

🍗 1. Seleccione el Plato de Pollo

{MENU_ITEMS.pollo.map((pollo) => ( ))}
{/* Bloque 3: Selección de Porción / Opción */}

🍽️ 2. Elija la Porción / Tamaño

{MENU_ITEMS.portions.map((portion) => { const priceForOption = Math.round( (MENU_ITEMS.pollo.find(p => p.id === selectedChicken)?.basePrice || 0) * portion.factor ); return ( ); })}
{/* Bloque 4: Refrescos y Acompañamientos Extras */}

🥤 3. Refrescos & Extras Adicionales

{MENU_ITEMS.drinks.map((drink) => { const currentQty = selectedDrinks[drink.id] || 0; return (
{drink.icon}

{drink.name}

{drink.price} Bs.

{/* Controles de Cantidad */}
{currentQty > 0 && ( )} 0 ? 'text-amber-500' : 'text-neutral-600'}`}> {currentQty}
); })}
{/* Bloque 5: Detalles Extras y Servicio */}

⚙️ 4. Indicaciones y Modo de Entrega

setNotes(e.target.value)} className="w-full bg-neutral-900 border border-neutral-800 focus:border-amber-500 rounded-xl px-4 py-2 text-xs text-white placeholder-neutral-600 focus:outline-none transition duration-200" />
💡 Las porciones incluyen por defecto: papas fritas, arroz, ensalada, llajua y aderezos de la casa.
{/* Panel de Previsualización de Ficha (Columna Derecha - 5 de 12cols) */}
{/* Bloque de Ficha en Vivo */}

📋 Previsualización de Ficha Térmica

{/* Representación virtual de ticket térmico físico */}
{/* Efecto de corte de papel */}

SABOR DORADO

VENTA DE POLLOS DELICIOSOS

Calle Sucre #250 - La Paz, Bolivia

FICHA Nº: {ticketCounter}

Cliente: {clientName.trim() || 'Sin Nombre (S/N)'}

CI/NIT: {clientCI.trim() || '0'}

Fecha: {new Date().toLocaleDateString('es-BO')} - {new Date().toLocaleTimeString('es-BO', {hour: '2-digit', minute:'2-digit'})}

Servicio: {serviceType === 'mesa' ? '🍽️ CONSUMO EN MESA' : '🥡 PARA LLEVAR'}

{/* Ítems */}
DETALLE TOTAL
{/* Pollo Principal */}
1x {MENU_ITEMS.pollo.find(p => p.id === selectedChicken)?.name} ({MENU_ITEMS.portions.find(p => p.id === selectedPortion)?.label}) {getChickenPrice()} Bs.
{/* Bebidas / Extras */} {Object.keys(selectedDrinks).map(drinkId => { const qty = selectedDrinks[drinkId]; const drinkItem = MENU_ITEMS.drinks.find(d => d.id === drinkId); if (qty > 0 && drinkItem) { return (
+ {qty}x {drinkItem.name} {drinkItem.price * qty} Bs.
); } return null; })}
{/* Notas Especiales */} {notes.trim() && (
Obs: {notes}
)} {/* Total */}
TOTAL A PAGAR: {getGrandTotal()} Bs.

¡Gracias por su preferencia!

Conserve su ficha para recibir su plato

Monto a Cobrar: {getGrandTotal()} Bs.
)} {/* PESTAÑA 2: CONTABILIDAD, CAJA & REPORTES */} {activeTab === 'caja' && (
{/* 1. Indicadores de Caja */}

Total Recaudado

{stats.totalRevenue} Bs.

En caja real

💵

Fichas Emitidas

{stats.totalTickets}

Pedidos despachados

🎟️

Ticket Promedio

{stats.totalTickets > 0 ? Math.round(stats.totalRevenue / stats.totalTickets) : 0} Bs.

Por transacción

📊

Plato Más Vendido

{stats.broasterCount >= Math.max(stats.espiedoCount, stats.milanesaCount) && stats.broasterCount > 0 ? 'Broaster' : stats.espiedoCount >= Math.max(stats.broasterCount, stats.milanesaCount) && stats.espiedoCount > 0 ? 'Espiedo' : stats.milanesaCount > 0 ? 'Milanesa' : 'Ninguno'}

Sabor favorito del día

🔥
{/* 2. Distribución y Ventas por Producto */}
{/* Desglose de Ventas (Columna Izquierda - 4 de 12cols) */}

🍗 Distribución de Pollos Vendidos

Pollo Broaster 🍗 {stats.broasterCount} platos
0 ? (stats.broasterCount / stats.totalTickets) * 100 : 0}%` }} >
Pollo al Espiedo 🔥 {stats.espiedoCount} platos
0 ? (stats.espiedoCount / stats.totalTickets) * 100 : 0}%` }} >
Milanesa de Pollo 🥩 {stats.milanesaCount} platos
0 ? (stats.milanesaCount / stats.totalTickets) * 100 : 0}%` }} >

🍽️ Preferencia de Porciones

Económico {stats.itemsPorcion.economico}
Cuarto {stats.itemsPorcion.cuarto}
Medio {stats.itemsPorcion.medio}
Entero {stats.itemsPorcion.entero}
{/* Historial de Fichas Emitidas (Columna Derecha - 8 de 12cols) */}

📋 Registro de Fichas del Turno

Mostrando las últimas ventas sincronizadas en tiempo real

{sales.length === 0 ? (
📭 No hay transacciones registradas el día de hoy
) : (
{sales.map((sale) => ( ))}
Ficha Hora Cliente / CI Menú Seleccionado Servicio Monto Acciones
#{sale.ticketNumber} {sale.createdAt.toLocaleTimeString('es-BO', { hour: '2-digit', minute: '2-digit' })}
{sale.clientName}
CI: {sale.clientCI}
{sale.chicken?.name}
{sale.chicken?.portion} {sale.extras && sale.extras.length > 0 && ` + ${sale.extras.length} extra(s)`}
{sale.serviceType === 'mesa' ? '🍽️ Mesa' : '🥡 Llevar'} {sale.totalAmount} Bs.
)}
)}
{/* --- MODAL DE CONFIRMACIÓN DE REIMPRESIÓN / ÉXITO --- */} {showPrintModal && ticketToPrint && (

🖨️ Ficha Lista Para Imprimir

Se ha registrado con éxito la ficha Nº {ticketToPrint.ticketNumber} de {ticketToPrint.clientName}.

{/* Preview visual rápida dentro del modal */}

SABOR DORADO

FICHA Nº {ticketToPrint.ticketNumber}

Cliente: {ticketToPrint.clientName}

Total: {ticketToPrint.totalAmount} Bs.

Pedido: 1x {ticketToPrint.chicken?.name} ({ticketToPrint.chicken?.portion})

{ticketToPrint.notes &&

Obs: "{ticketToPrint.notes}"

}
)} {/* --- MODAL DE CONFIRMACIÓN DE RESETEO DE BASE DE DATOS --- */} {showConfirmResetModal && (
⚠️

¿Seguro que deseas vaciar la caja?

Esta acción simula el inicio de un nuevo turno o día. Los registros se mantendrán limpios únicamente en la interfaz activa. Para operaciones reales consulte con TI.

)} {/* --- CONTENEDOR ESPECÍFICO DE IMPRESIÓN (OCULTO EN LA INTERFAZ WEB) --- */} {ticketToPrint && (

SABOR DORADO

POLLERÍA Y EMISIÓN DE FICHAS

La Paz, Bolivia

FICHA Nº: {ticketToPrint.ticketNumber}

Cliente: {ticketToPrint.clientName}

C.I. / NIT: {ticketToPrint.clientCI}

Fecha/Hora: {ticketToPrint.createdAt?.toLocaleDateString('es-BO')} - {ticketToPrint.createdAt?.toLocaleTimeString('es-BO', {hour: '2-digit', minute:'2-digit'})}

Entrega: {ticketToPrint.serviceType === 'mesa' ? 'MESA' : 'PARA LLEVAR'}

DESCRIPCIÓN TOTAL
1x {ticketToPrint.chicken?.name} ({ticketToPrint.chicken?.portion}) {ticketToPrint.chicken?.price} Bs.
{ticketToPrint.extras?.map((extra) => (
+ {extra.quantity}x {extra.name} {extra.subtotal} Bs.
))}
{ticketToPrint.notes && (
Observación: {ticketToPrint.notes}
)}
TOTAL PAGADO: {ticketToPrint.totalAmount} Bs.

¡Muchas gracias por su compra!

Sabor inigualable en cada presa

)} {/* --- PIE DE PÁGINA (Oculto al imprimir) --- */}
); }