import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, onAuthStateChanged, signInWithEmailAndPassword, signOut, } from 'firebase/auth'; import { getFirestore, collection, addDoc, getDocs, query, where, doc, updateDoc, onSnapshot, setDoc, deleteDoc, runTransaction, getDoc } from 'firebase/firestore'; import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; // --- ÍCONOS SVG --- const PlusIcon = () => ; const HomeIcon = () => ; const DocumentTextIcon = () => ; const UsersIcon = () => ; const CollectionIcon = () => ; const ChartBarIcon = () => ; const CashIcon = () => ; const LogoutIcon = () => ; const MenuIcon = () => ; const XIcon = () => ; const TrashIcon = () => ; const PencilIcon = () => ; const PrinterIcon = () => ; const UserGroupIcon = () => ; const ReceiptIcon = () => ; const SortIcon = ({ direction, active }) => ( {direction === 'asc' ? '↑' : '↓'} ); // --- CONFIGURACIÓN DE FIREBASE --- const firebaseConfig = { apiKey: "AIzaSyBjSRsPF-dgdvAbBASb5kNh39C5VgXMC2o", authDomain: "miappdeventas-f0318.firebaseapp.com", projectId: "miappdeventas-f0318", storageBucket: "miappdeventas-f0318.appspot.com", messagingSenderId: "471807009356", appId: "1:471807009356:web:95d3aabc797bf55a550ae9" }; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // --- COMPONENTES DE LA UI --- const Spinner = () => (
); const getLocalDateString = (date = new Date()) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const Modal = ({ children, isOpen, onClose, title }) => { if (!isOpen) return null; return (

{String(title)}

{children}
); }; // --- PÁGINA DE LOGIN --- const LoginPage = ({ setUserData }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleLogin = async (e) => { e.preventDefault(); setIsLoading(true); setError(''); try { await signInWithEmailAndPassword(auth, email, password); } catch (err) { setError('Usuario o contraseña incorrectos. Por favor, intente de nuevo.'); setIsLoading(false); } }; return (

Publi Server

Diseño Gráfico

Iniciar Sesión

setEmail(e.target.value)} className="w-full p-3 border rounded-xl shadow-inner focus:ring-2 focus:ring-blue-100 outline-none font-bold" required />
setPassword(e.target.value)} className="w-full p-3 border rounded-xl shadow-inner focus:ring-2 focus:ring-blue-100 outline-none font-bold" required />
{error &&

{error}

}
); }; // --- DASHBOARD --- const Dashboard = ({ userData, workOrders, users }) => { const statusCounts = workOrders.reduce((acc, order) => { const s = order.status || 'pendiente'; acc[s] = (acc[s] || 0) + 1; return acc; }, {}); const statusData = [ { name: 'Pendiente', value: statusCounts.pendiente || 0 }, { name: 'En Proceso', value: statusCounts.en_proceso || 0 }, { name: 'Listo', value: statusCounts.listo || 0 }, { name: 'Completado', value: statusCounts.completado || 0 }, ]; const COLORS = ['#FF8042', '#0088FE', '#FFBB28', '#00C49F']; const salesByVendor = workOrders.reduce((acc, order) => { const vendorName = String(order.vendorName || 'Sin Vendedor'); const total = parseFloat(order.total) || 0; acc[vendorName] = (acc[vendorName] || 0) + total; return acc; }, {}); const salesData = Object.keys(salesByVendor).map(name => ({ name, total: salesByVendor[name] })); const totalPendingBalance = workOrders .filter(order => order.status !== 'completado') .reduce((sum, order) => sum + (parseFloat(order.balance) || 0), 0); const stats = [ { name: 'Total Órdenes', value: workOrders.length, color: 'bg-blue-600' }, { name: 'En Taller', value: (statusCounts.pendiente || 0) + (statusCounts.en_proceso || 0), color: 'bg-yellow-500' }, { name: 'Listo Entrega', value: statusCounts.listo || 0, color: 'bg-green-500' }, { name: 'Saldo Global', value: totalPendingBalance, color: 'bg-orange-500', isCurrency: true }, ]; return (

Panel de Control General

{stats.map(stat => (

{stat.name}

{stat.isCurrency ? `$${stat.value.toFixed(2)}` : stat.value}

))}

Facturación por Vendedor ($)

{userData.role === 'admin' ? (
) : (
Datos restringidos al Administrador
)}

Distribución de Estados

{statusData.map((entry, index) => ( ))}
); }; // --- GESTIÓN DE PRODUCTOS / SERVICIOS --- const ProductCatalog = ({ services, onSave, onDelete }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [editingProduct, setEditingProduct] = useState(null); const [formData, setFormData] = useState({ code: '', name: '', price: '' }); useEffect(() => { if (editingProduct) { setFormData({ code: String(editingProduct.code || ''), name: String(editingProduct.name), price: String(editingProduct.price) }); } else { setFormData({ code: '', name: '', price: '' }); } }, [editingProduct]); const handleSave = () => { onSave({ ...editingProduct, ...formData, price: parseFloat(String(formData.price).replace(',', '.')) || 0 }); setIsModalOpen(false); setEditingProduct(null); }; return (

Catálogo de Productos y Servicios

{services.map(s => ( ))}
Código Descripción Precio Base Acciones
{String(s.code || '-')} {String(s.name)} ${parseFloat(s.price || 0).toFixed(2)}
setIsModalOpen(false)} title={editingProduct ? "Editar Producto" : "Nuevo Producto"}>
setFormData({...formData, code: e.target.value})} placeholder="Escanea o escribe..." className="w-full p-3 border rounded-xl font-mono font-bold shadow-inner" />
setFormData({...formData, name: e.target.value})} placeholder="Nombre del producto" className="w-full p-3 border rounded-xl font-bold shadow-inner" />
setFormData({...formData, price: e.target.value})} placeholder="0.00" className="w-full p-3 border rounded-xl font-black text-blue-700 text-xl shadow-inner" />
); }; // --- GESTIÓN DE USUARIOS --- const UsersManager = ({ users, onSave, onDelete }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); const [formData, setFormData] = useState({ name: '', email: '', role: 'vendedor' }); useEffect(() => { if (editingUser) { setFormData({ name: String(editingUser.name), email: String(editingUser.email), role: String(editingUser.role) }); } else { setFormData({ name: '', email: '', role: 'vendedor' }); } }, [editingUser]); const handleSave = () => { if (!formData.name || !formData.email) { alert("Nombre y email son obligatorios."); return; } onSave({ ...editingUser, ...formData }); setIsModalOpen(false); setEditingUser(null); }; return (

Gestión de Usuarios

{users.map(u => ( ))}
NombreEmailRolAcciones
{String(u.name)} {String(u.email)} {String(u.role)}
setIsModalOpen(false)} title={editingUser ? "Modificar Perfil" : "Nuevo Usuario"}>
setFormData({...formData, name: e.target.value})} placeholder="Nombre completo" className="w-full p-3 border rounded-xl shadow-inner font-bold" /> setFormData({...formData, email: e.target.value})} placeholder="correo@ejemplo.com" className="w-full p-3 border rounded-xl shadow-inner font-bold" disabled={!!editingUser} />
); }; // --- FORMULARIO CLIENTE --- const CustomerForm = ({ customer, onSave, onCancel }) => { const [formData, setFormData] = useState({ idNumber: String(customer?.idNumber || ''), phone: String(customer?.phone || ''), email: String(customer?.email || ''), address: String(customer?.address || ''), fullName: customer?.name ? `${customer.name} ${customer.lastName}`.trim() : '', }); const handleSubmit = (e) => { e.preventDefault(); const nameParts = formData.fullName.trim().split(' '); const name = nameParts.shift() || ''; const lastName = nameParts.join(' ') || ''; const dataToSave = { idNumber: formData.idNumber, phone: formData.phone, email: formData.email, address: formData.address, name, lastName, }; if (customer && customer.id) dataToSave.id = customer.id; onSave(dataToSave); }; return (
setFormData({...formData, fullName: e.target.value})} placeholder="Nombre Completo" className="w-full p-3 border rounded-xl shadow-inner font-bold focus:ring-2 focus:ring-blue-100 outline-none" required />
setFormData({...formData, idNumber: e.target.value})} placeholder="Cédula o RUC" className="w-full p-3 border rounded-xl shadow-inner font-bold focus:ring-2 focus:ring-blue-100 outline-none" required /> setFormData({...formData, phone: e.target.value})} placeholder="Teléfono de contacto" className="w-full p-3 border rounded-xl shadow-inner font-bold focus:ring-2 focus:ring-blue-100 outline-none" />
setFormData({...formData, email: e.target.value})} placeholder="Correo electrónico" className="w-full p-3 border rounded-xl shadow-inner font-bold focus:ring-2 focus:ring-blue-100 outline-none" /> setFormData({...formData, address: e.target.value})} placeholder="Dirección domiciliaria" className="w-full p-3 border rounded-xl shadow-inner font-bold focus:ring-2 focus:ring-blue-100 outline-none" />
); }; const CustomersList = ({ customers, onEdit }) => { const [searchTerm, setSearchTerm] = useState(''); const filtered = customers.filter(c => String(`${c.name} ${c.lastName} ${c.idNumber}`).toLowerCase().includes(searchTerm.toLowerCase()) ); return (

Gestión de Clientes

setSearchTerm(e.target.value)} placeholder="🔎 Buscar cliente por nombre, cédula o RUC..." className="w-full p-4 border rounded-2xl mb-6 shadow-inner font-bold outline-none focus:ring-2 focus:ring-blue-100" />
{filtered.map(c => ( ))}
Nombres y ApellidosIdentificaciónAcciones
{String(c.name)} {String(c.lastName)} {String(c.idNumber)}
); }; // --- GESTIÓN DE ÓRDENES DE TRABAJO --- const WorkOrderForm = ({ order, onSave, onCancel, customers, services, users, userData }) => { const initialFormData = { creationDate: getLocalDateString(), deliveryDate: '', customerId: '', customerName: '', vendorId: String(userData.uid), vendorName: String(userData.name), status: 'pendiente', items: [{ id: Date.now(), code: '', description: '', quantity: "1", unitPrice: "0", subtotal: 0 }], subtotal: 0, iva: 0, totalIva: 0, total: 0, payments: [], balance: 0, notes: '' }; const [formData, setFormData] = useState((order && order.id) ? { ...order, iva: order.iva !== undefined ? order.iva : 0 } : initialFormData); const [customerSearch, setCustomerSearch] = useState(''); const [showCustomerDropdown, setShowCustomerDropdown] = useState(false); const [showServiceDropdown, setShowServiceDropdown] = useState(null); const [isCustomerModalOpen, setIsCustomerModalOpen] = useState(false); const [validationError, setValidationError] = useState(''); useEffect(() => { if (order && order.id) { setFormData({ ...order, iva: order.iva !== undefined ? order.iva : 0 }); } else { setFormData(initialFormData); } }, [order]); useEffect(() => { const calculateTotals = () => { const subtotal = (formData.items || []).reduce((acc, item) => { const price = parseFloat(String(item.unitPrice || 0).replace(',', '.')) || 0; const qty = parseFloat(String(item.quantity || 0).replace(',', '.')) || 0; return acc + (qty * price); }, 0); const ivaRate = (parseFloat(String(formData.iva || 0).replace(',', '.')) || 0) / 100; const totalIva = subtotal * ivaRate; const total = subtotal + totalIva; const totalPaid = (formData.payments || []).reduce((acc, p) => { return acc + (parseFloat(String(p.amount || 0).replace(',', '.')) || 0); }, 0); const balance = total - totalPaid; setFormData(prev => ({ ...prev, subtotal, totalIva, total, balance })); }; calculateTotals(); }, [formData.items, formData.iva, formData.payments]); const handleItemChange = (index, field, value) => { const newItems = [...formData.items]; newItems[index][field] = value; if (field === 'code' && value.trim() !== '') { const matchedProduct = services.find(s => (String(s.code) || '').toLowerCase() === value.toLowerCase()); if (matchedProduct) { newItems[index].description = String(matchedProduct.name); newItems[index].unitPrice = String(matchedProduct.price); } } if (field === 'quantity' || field === 'unitPrice' || field === 'code') { const qty = parseFloat(String(newItems[index].quantity || 0).replace(',', '.')) || 0; const price = parseFloat(String(newItems[index].unitPrice || 0).replace(',', '.')) || 0; newItems[index].subtotal = qty * price; } setFormData({ ...formData, items: newItems }); }; const handlePaymentChange = (index, field, value) => { const newPayments = [...formData.payments]; newPayments[index][field] = value; setFormData({ ...formData, payments: newPayments }); }; const handleCustomerSelect = (customer) => { setFormData({ ...formData, customerId: String(customer.id), customerName: String(`${customer.name} ${customer.lastName}`) }); setCustomerSearch(''); setShowCustomerDropdown(false); setValidationError(''); }; const handleServiceSelectFromDropdown = (product, itemIndex) => { const newItems = [...formData.items]; newItems[itemIndex].code = String(product.code || ''); newItems[itemIndex].description = String(product.name); newItems[itemIndex].unitPrice = String(product.price); const qty = parseFloat(String(newItems[itemIndex].quantity || 1).replace(',', '.')) || 1; newItems[itemIndex].subtotal = qty * product.price; setFormData({ ...formData, items: newItems }); setShowServiceDropdown(null); }; const handleFormSubmit = () => { if (!formData.customerId) { setValidationError('⚠️ Error: Debe asignar un cliente a la orden de trabajo antes de guardar.'); return; } setValidationError(''); onSave(formData); }; const selectedCustomerDetails = customers.find(c => c.id === formData.customerId); return (

Formulario de Orden

{validationError && (
{validationError}
)}

Receptor del Trabajo

{formData.customerId ? (

{String(formData.customerName)}

{selectedCustomerDetails && (
📞 Telf: {String(selectedCustomerDetails.phone || '-')}
📍 Dir: {String(selectedCustomerDetails.address || '-')}
)}
) : (
{ setCustomerSearch(e.target.value); setShowCustomerDropdown(true); }} onFocus={() => setShowCustomerDropdown(true)} placeholder="🔎 Escriba nombre del cliente..." className="w-full p-4 border rounded-2xl shadow-inner focus:ring-4 focus:ring-blue-50 outline-none font-bold text-lg" /> {showCustomerDropdown && (
{customers.filter(c => String(`${c.name} ${c.lastName} ${c.idNumber}`).toLowerCase().includes(customerSearch.toLowerCase())).map(c => (
handleCustomerSelect(c)} className="p-4 hover:bg-blue-600 hover:text-white cursor-pointer border-b last:border-b-0 transition-colors group">

{String(c.name)} {String(c.lastName)}

ID: {String(c.idNumber)}

))}
)}
)}

Cronograma y Responsable

setFormData({...formData, creationDate: e.target.value})} className="w-full p-3 border rounded-2xl shadow-sm text-sm font-black text-gray-700 outline-none focus:ring-2 focus:ring-blue-100" />
setFormData({...formData, deliveryDate: e.target.value})} className="w-full p-3 border rounded-2xl shadow-sm text-sm font-black text-blue-800 outline-none focus:ring-2 focus:ring-blue-100" />
{(formData.items || []).map((item, index) => ( ))}
Código Descripción de Trabajo Cant. Unitario Total
handleItemChange(index, 'code', e.target.value)} placeholder="Escaneo..." className="w-full p-3 border border-transparent focus:border-blue-400 rounded-xl text-xs font-mono font-black outline-none bg-transparent" /> handleItemChange(index, 'description', e.target.value)} onFocus={() => setShowServiceDropdown(index)} onBlur={() => { setTimeout(() => { if (showServiceDropdown === index) setShowServiceDropdown(null); }, 200); }} className="w-full p-3 border border-transparent focus:border-blue-400 rounded-xl text-xs font-black uppercase tracking-tight outline-none bg-transparent" placeholder="Escriba para buscar en catálogo..." /> {showServiceDropdown === index && (
{services .filter(s => String(s.name || '').toLowerCase().includes(String(item.description || '').toLowerCase()) || String(s.code || '').toLowerCase().includes(String(item.description || '').toLowerCase())) .map(s => (
{ e.preventDefault(); handleServiceSelectFromDropdown(s, index); }} className="p-4 hover:bg-blue-600 hover:text-white text-[11px] cursor-pointer border-b last:border-0 transition-colors flex justify-between items-center font-bold" > {String(s.code)} {String(s.name)} ${parseFloat(s.price).toFixed(2)}
))}
)}
handleItemChange(index, 'quantity', e.target.value)} className="w-full p-3 border border-transparent focus:border-blue-400 rounded-xl text-sm text-center font-black bg-transparent" /> handleItemChange(index, 'unitPrice', e.target.value)} className="w-full p-3 border border-transparent focus:border-blue-400 rounded-xl text-sm font-black text-blue-700 text-center bg-transparent" placeholder="0.00" /> ${parseFloat(item.subtotal || 0).toFixed(2)}

Cómputo Final

Subtotal:${(formData.subtotal || 0).toFixed(2)}
Impuesto IVA (%):
setFormData({...formData, iva: e.target.value})} className="w-16 border rounded-xl text-center font-black mr-4 p-2 shadow-inner text-lg" /> ${(formData.totalIva || 0).toFixed(2)}
VALOR TOTAL: ${(formData.total || 0).toFixed(2)}
Saldo Pendiente: ${(formData.balance || 0).toFixed(2)}

Gestión de Abonos

{(formData.payments || []).map((p, i) => (
handlePaymentChange(i, 'date', e.target.value)} className="p-2 border rounded-xl text-xs font-black text-gray-700 bg-white shadow-sm outline-none" /> handlePaymentChange(i, 'amount', e.target.value)} className="p-2 border border-green-300 rounded-xl text-base font-black text-green-800 bg-white shadow-inner text-center" placeholder="0.00" />
{p.method === 'transferencia' && ( handlePaymentChange(i, 'reference', e.target.value)} className="p-2 border border-blue-200 rounded-xl text-[10px] bg-blue-50 w-full font-black focus:ring-2 focus:ring-blue-400 outline-none uppercase" /> )}
))}
setIsCustomerModalOpen(false)} title="Agregar Nuevo Cliente"> { if (data.id) updateDoc(doc(db, "customers", data.id), data); else addDoc(collection(db, "customers"), data); setIsCustomerModalOpen(false); }} onCancel={() => setIsCustomerModalOpen(false)} />
); }; // --- LISTA DE ÓRDENES DE TRABAJO --- const WorkOrdersList = ({ workOrders, onEdit, onDelete, onPrint, userData, customers }) => { const [filterStatus, setFilterStatus] = useState('todos'); const [customerSearchTerm, setCustomerSearchTerm] = useState(''); const [sortConfig, setSortConfig] = useState({ key: 'creationDate', direction: 'desc' }); const requestSort = (key) => { let direction = 'asc'; if (sortConfig.key === key && sortConfig.direction === 'asc') { direction = 'desc'; } setSortConfig({ key, direction }); }; const filteredOrders = workOrders.filter(order => { const status = order.status ? String(order.status).trim().toLowerCase() : ''; if (filterStatus === 'todos' && status === 'completado') return false; if (filterStatus !== 'todos' && filterStatus !== 'todos_visibles' && status !== filterStatus) return false; if (customerSearchTerm) { const search = customerSearchTerm.toLowerCase(); const customer = customers.find(c => String(c.id) === String(order.customerId)); const customerName = String(order.customerName || '').toLowerCase(); const idNumber = customer && customer.idNumber ? String(customer.idNumber).toLowerCase() : ''; if (!customerName.includes(search) && !idNumber.includes(search)) return false; } return true; }); const sortedOrders = [...filteredOrders].sort((a, b) => { let aValue = a[sortConfig.key]; let bValue = b[sortConfig.key]; if (sortConfig.key === 'orderNumber') { const numA = parseInt(String(aValue).replace('OT-', '')) || 0; const numB = parseInt(String(bValue).replace('OT-', '')) || 0; return sortConfig.direction === 'asc' ? numA - numB : numB - numA; } if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1; if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1; return 0; }); const totalBalance = sortedOrders.reduce((sum, o) => sum + (parseFloat(o.balance) || 0), 0); return (
setCustomerSearchTerm(e.target.value)} placeholder="🔎 Buscar por nombre o cédula..." className="p-4 border rounded-2xl flex-grow min-w-[150px] shadow-inner focus:ring-4 focus:ring-blue-50 outline-none font-bold uppercase text-[11px]" />
Saldo total pendiente: ${totalBalance.toFixed(2)}
{sortedOrders.map(order => ( ))}
requestSort('orderNumber')} className="px-6 py-5 cursor-pointer hover:bg-gray-200 transition-colors group border-r border-gray-200"> # OT Cliente / Entidad requestSort('creationDate')} className="px-6 py-5 cursor-pointer hover:bg-gray-200 transition-colors border-r border-gray-200 text-center">Fecha Vendedor Estado Deuda Acciones
{String(order.orderNumber)} {String(order.customerName)} {String(order.creationDate)} {String(order.vendorName || 'S/N')} {String(order.status)} ${parseFloat(order.balance || 0).toFixed(2)} {userData.role === 'admin' && }
{sortedOrders.length === 0 &&
Cero resultados
}
); }; // --- GESTIÓN DE GASTOS --- const ExpensesManager = ({ expenses, userData, onSave, onDelete }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [currentExpense, setCurrentExpense] = useState({ date: getLocalDateString(), description: '', amount: '' }); const [filterDate, setFilterDate] = useState(getLocalDateString()); const handleSave = () => { const amount = parseFloat(String(currentExpense.amount).replace(',', '.')); if (!currentExpense.description || isNaN(amount) || amount <= 0) { alert("⚠️ Descripción y monto son obligatorios."); return; } onSave({ ...currentExpense, amount, userId: String(userData.uid), userName: String(userData.name) }); setIsModalOpen(false); }; const filtered = (expenses || []).filter(e => String(e.date) === filterDate); return (

Registro de Gastos

setFilterDate(e.target.value)} className="p-2 border rounded-xl font-black text-gray-800 outline-none bg-white shadow-sm" />
{filtered.map(e => ( ))}
DescripciónValorUsuarioAcciones
{String(e.description)} -${parseFloat(e.amount).toFixed(2)} {String(e.userName).toUpperCase()}
setIsModalOpen(false)} title="Registrar Egreso Directo">
setCurrentExpense({...currentExpense, date: e.target.value})} className="w-full p-4 border rounded-2xl font-black text-gray-800 shadow-inner bg-gray-50" /> setCurrentExpense({...currentExpense, description: e.target.value})} placeholder="Detalle del gasto..." className="w-full p-4 border rounded-2xl shadow-inner outline-none font-bold" /> setCurrentExpense({...currentExpense, amount: e.target.value})} placeholder="Monto (0.00)" className="w-full p-4 border rounded-2xl font-black text-red-600 text-3xl shadow-inner text-center" />
); }; // --- COMPONENTE REPORTE DE CIERRE --- const CashClosingReport = ({ workOrders, expenses, users, userData }) => { const [selectedDate, setSelectedDate] = useState(getLocalDateString()); const [selectedUser, setSelectedUser] = useState(''); const payments = []; workOrders.forEach(o => { if (selectedUser && o.vendorId !== selectedUser) return; (o.payments || []).forEach(p => { if (p.date === selectedDate) { const jobsSummary = (o.items || []).map(i => String(i.description)).join(', '); payments.push({ ...p, ot: String(o.orderNumber), cliente: String(o.customerName), trabajos: jobsSummary, vendedor: String(o.vendorName || 'S/N') }); } }); }); const dailyExpenses = (expenses || []).filter(e => { const matchDate = String(e.date) === selectedDate; const matchUser = !selectedUser || String(e.userId) === selectedUser; return matchDate && matchUser; }); const totalCash = payments.filter(p => p.method !== 'transferencia').reduce((sum, p) => sum + parseFloat(String(p.amount || 0).replace(',', '.')), 0); const totalTrans = payments.filter(p => p.method === 'transferencia').reduce((sum, p) => sum + parseFloat(String(p.amount || 0).replace(',', '.')), 0); const totalExp = dailyExpenses.reduce((sum, e) => sum + parseFloat(String(e.amount || 0).replace(',', '.')), 0); return (

Cierre Diario de Caja

setSelectedDate(e.target.value)} className="p-4 border rounded-2xl w-full font-black text-gray-800 outline-none focus:ring-4 focus:ring-blue-50 bg-gray-50 shadow-inner" />
{userData.role === 'admin' && (
)}

Físico Recaudado

${totalCash.toFixed(2)}

Transferencias

${totalTrans.toFixed(2)}

Egresos / Gastos

-${totalExp.toFixed(2)}

Efectivo Real en Caja

${(totalCash - totalExp).toFixed(2)}

Base: Efectivo - Gastos

Detalle de Operaciones Diarias

RECAUDACIÓN BRUTA: ${(totalCash + totalTrans).toFixed(2)}
{payments.map((p, i) => ( ))}
# OT Cliente Detalle Trabajos Monto Modo Referencia
{String(p.ot)} {String(p.cliente)} {String(p.trabajos || 'S/D')} ${parseFloat(String(p.amount).replace(',', '.')).toFixed(2)} {String(p.method || 'Efectivo')} {p.method === 'transferencia' ? String(p.reference || 'N/D') : '-'}
{payments.length === 0 &&
Sin actividad financiera
}
); }; // --- COMPONENTE REPORTES --- const Reports = ({ workOrders, users }) => { const [startDate, setStartDate] = useState(getLocalDateString(new Date(new Date().setDate(1)))); const [endDate, setEndDate] = useState(getLocalDateString()); const [selectedUser, setSelectedUser] = useState(''); const filteredOrders = workOrders.filter(o => String(o.creationDate) >= startDate && String(o.creationDate) <= endDate && (!selectedUser || String(o.vendorId) === selectedUser) ); const totalVentas = filteredOrders.reduce((sum, o) => sum + parseFloat(String(o.total || 0).replace(',', '.')), 0); const totalCobrado = filteredOrders.reduce((sum, o) => sum + (o.payments || []).reduce((s, p) => s + parseFloat(String(p.amount || 0).replace(',', '.')), 0), 0); return (

Análisis de Rendimiento

setStartDate(e.target.value)} className="p-4 border rounded-2xl w-full font-black text-gray-800 shadow-inner focus:ring-4 focus:ring-blue-100 outline-none bg-gray-50" />
setEndDate(e.target.value)} className="p-4 border rounded-2xl w-full font-black text-gray-800 shadow-inner focus:ring-4 focus:ring-blue-100 outline-none bg-gray-50" />

Monto Facturado

${totalVentas.toFixed(2)}

{filteredOrders.length} Órdenes procesadas

Capital Cobrado

${totalCobrado.toFixed(2)}

Liquidaciones Efectivas
{selectedUser && (

📍 Auditando métricas individuales de: {users.find(u => u.uid === selectedUser)?.name.toUpperCase()}

)}
); }; // --- COMPONENTE PRINCIPAL APP --- export default function App() { const [user, setUser] = useState(null); const [userData, setUserData] = useState(null); const [loading, setLoading] = useState(true); const [currentPage, setCurrentPage] = useState('dashboard'); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [workOrders, setWorkOrders] = useState([]); const [customers, setCustomers] = useState([]); const [services, setServices] = useState([]); const [users, setUsers] = useState([]); const [expenses, setExpenses] = useState([]); const [editingOrder, setEditingOrder] = useState(null); const [editingCustomer, setEditingCustomer] = useState(null); const [orderToDelete, setOrderToDelete] = useState(null); useEffect(() => { const loadScripts = async () => { if (document.getElementById('jspdf-script')) return; const jspdf = document.createElement('script'); jspdf.id = 'jspdf-script'; jspdf.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; document.body.appendChild(jspdf); jspdf.onload = () => { const autoTable = document.createElement('script'); autoTable.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.23/jspdf.plugin.autotable.min.js'; document.body.appendChild(autoTable); }; }; loadScripts(); }, []); useEffect(() => { const unsubAuth = onAuthStateChanged(auth, async (u) => { if (u) { const snap = await getDocs(query(collection(db, "users"), where("uid", "==", u.uid))); if (!snap.empty) { const data = snap.docs[0].data(); const fData = { id: String(snap.docs[0].id), uid: String(u.uid), ...data }; setUserData(fData); onSnapshot(collection(db, "customers"), s => setCustomers(s.docs.map(d => ({ id: d.id, ...d.data() })))); onSnapshot(collection(db, "services"), s => setServices(s.docs.map(d => ({ id: d.id, ...d.data() })))); onSnapshot(collection(db, "expenses"), s => setExpenses(s.docs.map(d => ({ id: d.id, ...d.data() })))); onSnapshot(collection(db, "users"), s => setUsers(s.docs.map(d => ({ uid: d.id, ...d.data() })))); const qO = fData.role === 'admin' ? collection(db, "workOrders") : query(collection(db, "workOrders"), where("vendorId", "==", u.uid)); onSnapshot(qO, s => setWorkOrders(s.docs.map(d => ({ id: d.id, ...d.data() })))); } setUser(u); } else { setUser(null); setUserData(null); } setLoading(false); }); return () => unsubAuth(); }, []); const handleSaveUser = async (data) => { try { const userRef = doc(db, "users", String(data.uid || data.email)); await setDoc(userRef, data, { merge: true }); } catch (e) { console.error(e); } }; const handleSaveExpense = async (data) => { try { await addDoc(collection(db, "expenses"), data); } catch (e) { console.error(e); } }; const handleDeleteExpense = async (id) => { if (window.confirm("¿Eliminar este gasto?")) { try { await deleteDoc(doc(db, "expenses", id)); } catch (e) { console.error(e); } } }; const handleSaveCustomer = async (data) => { try { if (data.id) await updateDoc(doc(db, "customers", data.id), data); else await addDoc(collection(db, "customers"), data); setEditingCustomer(null); setCurrentPage('customers'); } catch (e) { console.error(e); } }; const handleSaveProduct = async (data) => { try { if (data.id) await updateDoc(doc(db, "services", data.id), data); else await addDoc(collection(db, "services"), data); } catch (e) { console.error(e); } }; const handleSaveWorkOrder = async (data) => { try { const normalizedData = { ...data, iva: parseFloat(String(data.iva).replace(',', '.')) || 0, items: (data.items || []).map(item => ({ ...item, quantity: parseFloat(String(item.quantity).replace(',', '.')) || 0, unitPrice: parseFloat(String(item.unitPrice).replace(',', '.')) || 0 })), payments: (data.payments || []).map(p => ({ ...p, amount: parseFloat(String(p.amount).replace(',', '.')) || 0, method: String(p.method || 'efectivo'), reference: String(p.reference || '') })) }; if (data.id) await updateDoc(doc(db, "workOrders", data.id), normalizedData); else { const counterRef = doc(db, "counters", "workOrderCounter"); await runTransaction(db, async (transaction) => { const counterDoc = await transaction.get(counterRef); const nextNum = counterDoc.exists() ? counterDoc.data().currentNumber + 1 : 1; const orderNum = `OT-${String(nextNum).padStart(3, '0')}`; const newRef = doc(collection(db, "workOrders")); transaction.set(newRef, { ...normalizedData, orderNumber: orderNum, id: newRef.id }); transaction.set(counterRef, { currentNumber: nextNum }); }); } setEditingOrder(null); setCurrentPage('work-orders'); } catch (e) { console.error(e); } }; const generatePDF = (order) => { if (!window.jspdf || !window.jspdf.jsPDF) return; const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit: 'mm', format: [80, 250] }); const margin = 5; const pageWidth = doc.internal.pageSize.getWidth(); const contentWidth = pageWidth - (margin * 2); let y = 12; doc.setFontSize(16).setFont('helvetica', 'bold').text('CompuServer & PubliServer', pageWidth / 2, y, { align: 'center' }); y += 8; doc.setFontSize(10).setFont('helvetica', 'normal').text('Av. Luis Cordero 407', pageWidth / 2, y, { align: 'center' }); y += 6; doc.text('Telf: 099 201 2673', pageWidth / 2, y, { align: 'center' }); y += 8; doc.setLineWidth(0.3).line(margin, y, pageWidth - margin, y); y += 8; doc.setFontSize(12).setFont('helvetica', 'bold').text(`ORDEN: ${String(order.orderNumber)}`, margin, y); y += 6; doc.setFontSize(10).setFont('helvetica', 'normal').text(`Fecha: ${String(order.creationDate)}`, margin, y); y += 8; doc.setFontSize(11).setFont('helvetica', 'bold').text('DATOS DEL CLIENTE:', margin, y); y += 6; doc.setFontSize(10).setFont('helvetica', 'normal'); const customer = customers.find(c => String(c.id) === String(order.customerId)); const nameLines = doc.splitTextToSize(`Nombre: ${String(order.customerName)}`, contentWidth); doc.text(nameLines, margin, y); y += (nameLines.length * 5); doc.text(`RUC/Cédula: ${String(customer?.idNumber || 'S/N')}`, margin, y); y += 5; doc.text(`Teléfono: ${String(customer?.phone || 'S/N')}`, margin, y); y += 5; const dirLines = doc.splitTextToSize(`Dirección: ${String(customer?.address || 'S/N')}`, contentWidth); doc.text(dirLines, margin, y); y += (dirLines.length * 5); const emailLines = doc.splitTextToSize(`Correo: ${String(customer?.email || 'S/N')}`, contentWidth); doc.text(emailLines, margin, y); y += (emailLines.length * 5) + 3; doc.autoTable({ startY: y, head: [['Cant', 'Desc', 'PU', 'Sub']], body: (order.items || []).map(i => [ parseFloat(String(i.quantity).replace(',', '.')), String(i.description), parseFloat(String(i.unitPrice).replace(',', '.')).toFixed(2), parseFloat(i.subtotal || 0).toFixed(2) ]), theme: 'grid', styles: { fontSize: 9, cellPadding: 1.5 }, headStyles: { fillColor: [240, 240, 240], textColor: [0, 0, 0], fontStyle: 'bold' }, margin: { left: margin, right: margin } }); y = doc.lastAutoTable.finalY + 8; doc.setFontSize(10).setFont('helvetica', 'normal'); doc.text(`Subtotal: $${parseFloat(order.subtotal || 0).toFixed(2)}`, pageWidth - margin, y, { align: 'right' }); y += 5; doc.text(`IVA: $${parseFloat(order.totalIva || 0).toFixed(2)}`, pageWidth - margin, y, { align: 'right' }); y += 7; doc.setFontSize(12).setFont('helvetica', 'bold').text(`TOTAL: $${parseFloat(order.total || 0).toFixed(2)}`, pageWidth - margin, y, { align: 'right' }); y += 7; doc.setFontSize(11).text(`SALDO: $${parseFloat(order.balance || 0).toFixed(2)}`, pageWidth - margin, y, { align: 'right' }); y += 18; doc.setFontSize(10).setFont('helvetica', 'italic').text("Gracias por preferirnos, es un placer poder servirle", pageWidth / 2, y, { align: 'center' }); doc.save(`${String(order.orderNumber)}.pdf`); }; if (loading) return ; if (!user) return ; if (!userData) return ; const renderPage = () => { if (editingOrder) return setEditingOrder(null)} />; if (editingCustomer !== null) return

{editingCustomer.id ? 'Actualizar Ficha de Cliente' : 'Registro de Nuevo Cliente'}

setEditingCustomer(null)} />
; switch (currentPage) { case 'dashboard': return ; case 'work-orders': return (

Gestión Órdenes de Trabajo

setOrderToDelete(id)} onPrint={generatePDF} userData={userData} customers={customers} />
); case 'customers': return ; case 'catalog': return deleteDoc(doc(db, "services", id))} />; case 'users-management': return userData.role === 'admin' ? deleteDoc(doc(db, "users", id))} /> : null; case 'expenses': return ; case 'cash-closing': return userData.role === 'admin' ? : null; case 'reports': return userData.role === 'admin' ? : null; default: return ; } }; return (

{userData.name ? String(userData.name) : 'Usuario'}

{userData.role ? String(userData.role).toUpperCase() : ''}

{userData.name ? String(userData.name).charAt(0).toUpperCase() : 'U'}
{renderPage()}
setOrderToDelete(null)} title="Confirmar Acción Crítica">
⚠️

¿Está completamente seguro de eliminar este registro?
Esta operación no tiene reversa en la base de datos.

); }