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 */}
setActiveTab('pos')}
className={`flex-1 sm:flex-initial flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg text-sm font-bold transition-all ${
activeTab === 'pos'
? 'bg-amber-500 text-neutral-950 shadow-md'
: 'text-neutral-400 hover:text-white hover:bg-neutral-800'
}`}
>
🎟️ Nueva Ficha
setActiveTab('caja')}
className={`flex-1 sm:flex-initial flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg text-sm font-bold transition-all ${
activeTab === 'caja'
? 'bg-amber-500 text-neutral-950 shadow-md'
: 'text-neutral-400 hover:text-white hover:bg-neutral-800'
}`}
>
💰 Caja & Reportes
{sales.length > 0 && (
{sales.length}
)}
{/* 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) */}
{/* 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.
🗑️ Cancelar Todo
🖨️ Registrar y Ficha
)}
{/* 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
setShowConfirmResetModal(true)}
className="text-xs bg-red-950 hover:bg-red-900 border border-red-900 text-red-200 font-bold px-3 py-1.5 rounded-lg transition"
>
🔄 Resetear Caja
{sales.length === 0 ? (
📭
No hay transacciones registradas el día de hoy
) : (
Ficha
Hora
Cliente / CI
Menú Seleccionado
Servicio
Monto
Acciones
{sales.map((sale) => (
#{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.
{
setTicketToPrint(sale);
setShowPrintModal(true);
}}
className="bg-neutral-800 hover:bg-amber-500 text-neutral-300 hover:text-neutral-950 font-bold px-2.5 py-1.5 rounded-lg border border-neutral-700 hover:border-amber-500 transition"
title="Reimprimir Ficha"
>
🖨️ Ficha
))}
)}
)}
{/* --- 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}"
}
{ setShowPrintModal(false); setTicketToPrint(null); }}
className="flex-1 py-2.5 bg-neutral-800 hover:bg-neutral-700 rounded-xl text-xs font-bold text-neutral-300 transition"
>
Cerrar Ventana
Imprimir Ficha
)}
{/* --- 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.
setShowConfirmResetModal(false)}
className="flex-1 py-2.5 bg-neutral-800 hover:bg-neutral-700 rounded-xl text-xs font-bold text-neutral-300 transition"
>
Cancelar
{
setSales([]);
setTicketCounter(101);
setShowConfirmResetModal(false);
showToast("Caja reiniciada para un nuevo turno.");
}}
className="flex-1 py-2.5 bg-red-600 hover:bg-red-500 text-white font-black rounded-xl text-xs uppercase transition"
>
Sí, Resetear
)}
{/* --- 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) --- */}
);
}