/* ═══════════════════════════════════════════════ SALA VIEW — Product Registration (Mobile-first) ═══════════════════════════════════════════════ */ const SalaView = ({ user }) => { const [searchTerm, setSearchTerm] = React.useState(''); const [cantidad, setCantidad] = React.useState(''); const [fechaVenc, setFechaVenc] = React.useState(''); const [nota, setNota] = React.useState(''); const [tipo, setTipo] = React.useState('vencido'); const [estadoDanado, setEstadoDanado] = React.useState(''); const [filtroEstado, setFiltroEstado] = React.useState('todos'); const [showForm, setShowForm] = React.useState(false); const [editingId, setEditingId] = React.useState(null); const [searchResults, setSearchResults] = React.useState([]); const [selectedProduct, setSelectedProduct] = React.useState(null); const [saving, setSaving] = React.useState(false); const [searchLoading, setSearchLoading] = React.useState(false); const searchTimerRef = React.useRef(null); const sucId = user.sucursal || 'SM'; const sucUbicacion = window.MOCK.getPreferredUbicacionForSala ? window.MOCK.getPreferredUbicacionForSala(sucId) : sucId; const resolveUbicacionCode = (value) => { if (window.MOCK.resolveSalaDisplayCode) return window.MOCK.resolveSalaDisplayCode(value); return String(value || '').trim().toUpperCase() || sucUbicacion; }; const draftKey = React.useMemo(() => `vencidos_sala_draft_${user.id || 'anon'}_${sucId}`, [user.id, sucId]); const sortByArrival = React.useCallback((items) => { return [...items].sort((a, b) => (b.id || 0) - (a.id || 0)); }, []); const isMySucursalProduct = React.useCallback((product) => { if (window.MOCK.productMatchesSucursal) { return window.MOCK.productMatchesSucursal(product, sucId); } return (product && product.sucursal) === sucId; }, [sucId]); const [localProducts, setLocalProducts] = React.useState( sortByArrival(window.MOCK.MOCK_PRODUCTS.filter(isMySucursalProduct)) ); React.useEffect(() => { const handler = () => { setLocalProducts(sortByArrival(window.MOCK.MOCK_PRODUCTS.filter(isMySucursalProduct))); }; window.addEventListener('vencidos_data_changed', handler); return () => window.removeEventListener('vencidos_data_changed', handler); }, [isMySucursalProduct, sortByArrival]); React.useEffect(() => { try { const raw = localStorage.getItem(draftKey); if (!raw) return; const draft = JSON.parse(raw); if (!draft || typeof draft !== 'object') return; setSearchTerm(draft.searchTerm || ''); setCantidad(draft.cantidad || ''); setFechaVenc(draft.fechaVenc || ''); setNota(draft.nota || ''); setTipo(draft.tipo || 'vencido'); setEstadoDanado(draft.estadoDanado || ''); setShowForm(Boolean(draft.showForm)); if (draft.selectedProduct && typeof draft.selectedProduct === 'object') { setSelectedProduct(draft.selectedProduct); } } catch {} }, [draftKey]); React.useEffect(() => { try { const draft = { searchTerm, cantidad, fechaVenc, nota, tipo, estadoDanado, showForm, selectedProduct, }; localStorage.setItem(draftKey, JSON.stringify(draft)); } catch {} }, [draftKey, searchTerm, cantidad, fechaVenc, nota, tipo, estadoDanado, showForm, selectedProduct]); const filteredProducts = filtroEstado === 'todos' ? localProducts : localProducts.filter(p => p.estado === filtroEstado); const counts = React.useMemo(() => { const c = {}; window.MOCK.ESTADOS.forEach(e => { c[e.id] = 0; }); localProducts.forEach(p => { if (c[p.estado] !== undefined) c[p.estado]++; }); return c; }, [localProducts]); const handleSearch = (term) => { setSearchTerm(term); setSelectedProduct(null); if (searchTimerRef.current) clearTimeout(searchTimerRef.current); if (term.length < 2) { setSearchResults([]); return; } setSearchLoading(true); searchTimerRef.current = setTimeout(async () => { try { const res = await fetch(`${window.MOCK.getApiBase()}/catalog/search?q=${encodeURIComponent(term)}&sucursal=${sucUbicacion}&limit=8`); const data = await res.json(); setSearchResults(data); } catch { setSearchResults([]); } setSearchLoading(false); }, 300); }; const selectProduct = (prod) => { setSelectedProduct(prod); setSearchTerm(prod.nombre); setSearchResults([]); }; const handleAdd = async () => { const editingProduct = editingId ? window.MOCK.MOCK_PRODUCTS.find(p => p.id === editingId) : null; if (!selectedProduct && !editingProduct) return; if (!cantidad || (tipo === 'vencido' && !fechaVenc)) return; if (tipo === 'dañado' && !estadoDanado) return; setSaving(true); try { const now = new Date(); const baseProduct = selectedProduct || { nombre: editingProduct.nombre, barcode: editingProduct.codigo || '', proveedor: editingProduct.proveedor || '', categoria: editingProduct.cat1 || '', abc: editingProduct.abc || '', abcSucursal: editingProduct.abcSucursal || '', stock: editingProduct.stock || 0, sales30d: editingProduct.sales || 0, coverageDays: editingProduct.coverageDays || 0, cobertura: editingProduct.cobertura || '', }; const cantidadNum = parseInt(cantidad, 10); const stockDisponible = Number(baseProduct.stock || 0); if (!Number.isFinite(cantidadNum) || cantidadNum <= 0) { alert('La cantidad debe ser mayor a 0'); return; } if (cantidadNum > stockDisponible) { alert(`No puedes registrar más de ${stockDisponible} u (stock disponible).`); return; } const dias = tipo === 'vencido' ? window.MOCK.calcDiasRestantes(fechaVenc) : null; const estadoVenc = window.MOCK.calcEstadoVencimiento(dias); const provNombre = baseProduct.proveedor || window.MOCK.PROVEEDORES[0].name; const provCambio = window.MOCK.PROVEEDORES.find(p => p.name === provNombre)?.cambio ?? false; const accion = tipo === 'vencido' ? window.MOCK.calcAccionVencido(provNombre, provCambio, estadoVenc, !fechaVenc) : window.MOCK.calcAccionDanado(estadoDanado, provCambio, ''); const payload = { codigo: baseProduct.barcode || '', nombre: baseProduct.nombre, proveedor: provNombre, proveedorCambio: provCambio, sucursal: sucUbicacion, sucursalName: sucUbicacion, tipo, cantidad: cantidadNum, fechaVencimiento: tipo === 'vencido' ? fechaVenc : '', estadoDanado: tipo === 'dañado' ? estadoDanado : '', estado: tipo === 'vencido' ? window.MOCK.calcEstadoSOS(fechaVenc, provCambio) : (editingProduct?.estado || 'proximo_vencer'), accion, reportadoPor: user.name, reportadoPorId: user.id, fechaRegistro: editingProduct?.fechaRegistro || now.toISOString().split('T')[0], nota, ...(window.MOCK.parseCategoryPath ? window.MOCK.parseCategoryPath(baseProduct.categoria) : {}), abc: baseProduct.abc || '', abcSucursal: baseProduct.abcSucursal || '', stock: baseProduct.stock || 0, sales: baseProduct.sales30d || 0, coverageDays: baseProduct.coverageDays || 0, cobertura: baseProduct.cobertura || '', }; if (editingProduct) { const updated = await window.MOCK.updateProduct(editingId, payload, user); setLocalProducts(prev => sortByArrival(prev.map(p => p.id === editingId ? updated : p))); window.MOCK.addAuditEntry('Editó producto', `${updated.nombre} en ${resolveUbicacionCode(updated.sucursalName || updated.sucursal)}`, user.name, user.role, user.avatar); } else { const created = await window.MOCK.addProduct(payload); if (isMySucursalProduct(created)) { setLocalProducts(prev => sortByArrival([created, ...prev])); } window.MOCK.addAuditEntry('Registró producto', `${created.nombre} en ${resolveUbicacionCode(created.sucursalName || created.sucursal)}`, user.name, user.role, user.avatar); } resetForm(); } catch (e) { alert((editingId ? 'Error al editar: ' : 'Error al registrar: ') + e.message); } finally { setSaving(false); } }; const resetForm = () => { setSearchTerm(''); setCantidad(''); setFechaVenc(''); setNota(''); setSelectedProduct(null); setSearchResults([]); setTipo('vencido'); setEstadoDanado(''); setShowForm(false); setEditingId(null); try { localStorage.removeItem(draftKey); } catch {} }; const handleEdit = (prod) => { setEditingId(prod.id); setShowForm(true); setTipo(prod.tipo || 'vencido'); setCantidad(String(prod.cantidad || 1)); setFechaVenc(prod.fechaVencimiento || ''); setEstadoDanado(prod.estadoDanado || ''); setNota(prod.nota || ''); setSearchTerm(prod.nombre || ''); setSearchResults([]); setSelectedProduct({ id: prod.id, nombre: prod.nombre, barcode: prod.codigo || '', proveedor: prod.proveedor || '', categoria: prod.cat1 || '', abc: prod.abc || '', abcSucursal: prod.abcSucursal || '', stock: prod.stock || 0, sales30d: prod.sales || 0, coverageDays: prod.coverageDays || 0, cobertura: prod.cobertura || '', }); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const getDaysColor = (dias) => { if (dias < 0) return 'var(--status-danger)'; if (dias < 30) return 'var(--status-danger)'; if (dias < 90) return 'var(--status-warning)'; return 'var(--status-success)'; }; const s = { container: { padding: '24px', maxWidth: 800, margin: '0 auto', paddingBottom: 100 }, statsRow: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 24, }, stat: (color, bgColor) => ({ padding: '16px', borderRadius: 'var(--radius-lg)', background: bgColor, border: '1px solid var(--border-subtle)', }), statNum: (color) => ({ fontSize: 28, fontWeight: 800, color, letterSpacing: '-0.03em', lineHeight: 1, }), statLabel: { fontSize: 12, color: 'var(--text-tertiary)', marginTop: 4, fontWeight: 500 }, addBtn: { width: '100%', padding: '14px', borderRadius: 'var(--radius-md)', fontSize: 15, fontWeight: 700, marginBottom: 20, }, form: { background: 'var(--card-bg)', border: '1px solid var(--border-color)', borderRadius: 'var(--radius-lg)', padding: 20, marginBottom: 20, animation: 'slideUp 0.3s cubic-bezier(0.16,1,0.3,1) both', }, formTitle: { fontSize: 16, fontWeight: 700, marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', }, fieldRow: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }, field: { marginBottom: 12 }, fieldLabel: { display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6, }, searchDropdown: { position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 50, background: 'var(--bg-secondary)', border: '1px solid var(--border-color)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)', overflow: 'hidden', marginTop: 4, }, searchItem: { padding: '10px 14px', cursor: 'pointer', fontSize: 13, borderBottom: '1px solid var(--border-subtle)', transition: 'background var(--transition-fast)', }, searchCode: { fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }, stockWarn: { marginTop: 6, fontSize: 11, color: 'var(--status-danger)', fontWeight: 600, }, tipoTabs: { display: 'flex', gap: 4, padding: 3, borderRadius: 'var(--radius-md)', background: 'var(--bg-tertiary)', marginBottom: 12, }, tipoTab: (active) => ({ flex: 1, padding: '8px', textAlign: 'center', borderRadius: 'var(--radius-sm)', fontSize: 13, fontWeight: 600, cursor: 'pointer', border: 'none', background: active ? 'var(--bg-secondary)' : 'transparent', color: active ? 'var(--text-primary)' : 'var(--text-tertiary)', boxShadow: active ? 'var(--shadow-xs)' : 'none', fontFamily: 'var(--font-body)', transition: 'all var(--transition-fast)', }), filterRow: { display: 'flex', gap: 6, marginBottom: 16, flexWrap: 'wrap', }, filterChip: (active) => ({ padding: '6px 14px', borderRadius: 'var(--radius-full)', fontSize: 12, fontWeight: 600, cursor: 'pointer', border: 'none', background: active ? 'var(--accent)' : 'var(--bg-tertiary)', color: active ? 'white' : 'var(--text-secondary)', fontFamily: 'var(--font-body)', transition: 'all var(--transition-fast)', }), listHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, fontSize: 13, color: 'var(--text-tertiary)', fontWeight: 600, }, productCard: { background: 'var(--card-bg)', border: '1px solid var(--border-color)', borderRadius: 'var(--radius-md)', padding: '14px 16px', marginBottom: 8, transition: 'all var(--transition-fast)', animation: 'slideUp 0.35s cubic-bezier(0.16,1,0.3,1) both', }, prodTop: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, }, prodName: { fontSize: 14, fontWeight: 700, lineHeight: 1.3, flex: 1 }, prodMeta: { fontSize: 12, color: 'var(--text-tertiary)', marginTop: 4, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', }, prodActions: { display: 'flex', gap: 8, marginTop: 10, justifyContent: 'flex-end', }, daysBadge: (color) => ({ padding: '2px 8px', borderRadius: 'var(--radius-full)', fontSize: 11, fontWeight: 700, color: 'white', background: color, whiteSpace: 'nowrap', }), qtyBadge: { padding: '2px 8px', borderRadius: 'var(--radius-full)', fontSize: 11, fontWeight: 600, background: 'var(--bg-tertiary)', color: 'var(--text-secondary)', }, }; return (
{/* Stats */}
{window.MOCK.ESTADOS.map(e => (
{counts[e.id] || 0}
{e.label}
))}
{/* Add button or form */} {!showForm ? ( ) : (
{editingId ? 'Editar registro' : 'Nuevo registro'}
handleSearch(e.target.value)} style={selectedProduct ? {borderColor: 'var(--status-success)'} : {}} /> {searchResults.length > 0 && (
{searchResults.map(r => (
selectProduct(r)} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{r.nombre}
{r.barcode && {r.barcode}} {r.proveedor && {r.proveedor}} {r.stock > 0 && Stock: {r.stock} u} {r.stock === 0 && Sin stock} {r.fallback && ( DATOS GENERALES )}
))}
)} {selectedProduct && (
Stock: {selectedProduct.stock} u Cobertura: {selectedProduct.coverageDays > 0 ? `${selectedProduct.coverageDays} días` : 'S/D'} Ventas 30d: {selectedProduct.sales30d} u {selectedProduct.abc && ABC: {selectedProduct.abcSucursal || selectedProduct.abc}} {selectedProduct.proveedor && {selectedProduct.proveedor}} {selectedProduct.fallback && ( DATOS GENERALES )}
{selectedProduct.fallback && (
⚠ Sin datos específicos para tu sucursal — se muestran totales generales.
)}
)}
setCantidad(e.target.value)} /> {selectedProduct && Number(cantidad || 0) > Number(selectedProduct.stock || 0) && (
No puede exceder el stock ({selectedProduct.stock} u).
)}
setFechaVenc(e.target.value)} />
{tipo === 'dañado' && (
)}
setNota(e.target.value)} />
)} {/* Filters */}
{[ { id: 'todos', label: 'Todos' }, ...window.MOCK.ESTADOS.map(e => ({ id: e.id, label: `${e.label} (${counts[e.id] || 0})` })), ].map(f => ( ))}
{/* List header */}
{sucUbicacion} — {filteredProducts.length} registros
{/* Product list */} {filteredProducts.map((p, i) => (
{p.nombre}
{p.reportadoPor || user.name} · {resolveUbicacionCode(p.sucursalName || p.sucursal)} {p.tipo === 'dañado' && ( ·Dañado )}
{(() => { const e = window.MOCK.ESTADOS.find(x => x.id === p.estado); return e ? ( {e.label} ) : null; })()} {p.accion && ( {p.accion} )}
{p.diasRestantes !== undefined ? `${p.diasRestantes}d` : '—'} {p.cantidad} u
))}
); }; const SalaHistorialView = ({ user }) => { const [, setTick] = React.useState(0); React.useEffect(() => { const handler = () => setTick(t => t + 1); window.addEventListener('vencidos_data_changed', handler); return () => window.removeEventListener('vencidos_data_changed', handler); }, []); const fullLog = window.MOCK.AUDIT_LOG; const myLogs = fullLog.filter(entry => { const mySucursalId = user.sucursal || 'SM'; const myDisplayCode = window.MOCK.getPreferredUbicacionForSala ? window.MOCK.getPreferredUbicacionForSala(mySucursalId) : mySucursalId; const mySucursalObj = window.MOCK.SUCURSALES.find(s => s.id === mySucursalId); const mySucursalName = mySucursalObj ? mySucursalObj.name : ''; // 1. Check if the user who did the action belongs to the same sucursal const entryUserObj = window.MOCK.USERS.find(u => u.name === entry.user); if (entryUserObj && entryUserObj.sucursal === mySucursalId) { return true; } // 2. Check if the log detail or action mentions our sucursal name or code const detailLower = entry.detail.toLowerCase(); const actionLower = entry.action.toLowerCase(); const hasMySucName = mySucursalName && (detailLower.includes(mySucursalName.toLowerCase()) || actionLower.includes(mySucursalName.toLowerCase())); const hasMySucCode = detailLower.includes(` en ${mySucursalId.toLowerCase()}`) || detailLower.includes(` - ${mySucursalId.toLowerCase()}`); const hasMyDisplayCode = myDisplayCode && ( detailLower.includes(` en ${myDisplayCode.toLowerCase()}`) || detailLower.includes(` de ${myDisplayCode.toLowerCase()}`) || detailLower.includes(` - ${myDisplayCode.toLowerCase()}`) ); return hasMySucName || hasMySucCode || hasMyDisplayCode; }); const [searchTerm, setSearchTerm] = React.useState(''); const [dateFrom, setDateFrom] = React.useState(''); const [dateTo, setDateTo] = React.useState(''); const filteredLogs = myLogs.filter(entry => { // 1. Text filter if (searchTerm.trim()) { const s = searchTerm.toLowerCase(); const matchesText = entry.action.toLowerCase().includes(s) || entry.detail.toLowerCase().includes(s) || entry.user.toLowerCase().includes(s); if (!matchesText) return false; } // 2. Date Range filter const entryDate = new Date(entry.timestamp); if (dateFrom) { const fromLimit = new Date(dateFrom + 'T00:00:00'); if (entryDate < fromLimit) return false; } if (dateTo) { const toLimit = new Date(dateTo + 'T23:59:59'); if (entryDate > toLimit) return false; } return true; }); const hs = { container: { padding: '24px 20px', maxWidth: 640, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 20 }, header: { display: 'flex', flexDirection: 'column', gap: 4 }, title: { fontSize: 20, fontWeight: 800, letterSpacing: '-0.02em', color: 'var(--text-primary)' }, subtitle: { fontSize: 13, color: 'var(--text-tertiary)' }, filterPanel: { background: 'var(--card-bg)', border: '1px solid var(--border-color)', borderRadius: 'var(--radius-lg)', padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 14, boxShadow: 'var(--shadow-sm)' }, searchField: { display: 'flex', flexDirection: 'column', gap: 4 }, dateFields: { display: 'flex', gap: 12, alignItems: 'flex-end', flexWrap: 'wrap' }, dateCol: { flex: '1 1 140px', display: 'flex', flexDirection: 'column', gap: 4 }, filterLabel: { fontSize: 11, fontWeight: 700, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }, clearBtn: { color: 'var(--status-danger)', border: '1px solid var(--border-subtle)', height: 38, padding: '0 16px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, logList: { display: 'flex', flexDirection: 'column', gap: 12 }, logCard: { background: 'var(--card-bg)', border: '1px solid var(--border-color)', borderRadius: 'var(--radius-lg)', padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 8, transition: 'all var(--transition-fast)' }, cardHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, actionBadge: (action) => { const isDelete = action.includes('Eliminó') || action.includes('Desactivó'); const isCreate = action.includes('Registró') || action.includes('Creó'); return { fontSize: 11, fontWeight: 700, padding: '4px 10px', borderRadius: 'var(--radius-sm)', background: isDelete ? 'rgba(239, 68, 68, 0.15)' : isCreate ? 'rgba(99, 102, 241, 0.15)' : 'rgba(59, 130, 246, 0.15)', color: isDelete ? '#ef4444' : isCreate ? 'var(--accent)' : '#3b82f6' }; }, dateText: { fontSize: 11, color: 'var(--text-tertiary)' }, detailText: { fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5 }, emptyState: { padding: '40px 20px', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14, background: 'var(--bg-secondary)', borderRadius: 'var(--radius-lg)', border: '1.5px dashed var(--border-color)' } }; return (

Historial de Actividades

Consulta la bitácora de tus acciones y registros en esta sucursal

setSearchTerm(e.target.value)} style={{ width: '100%' }} />
setDateFrom(e.target.value)} style={{ width: '100%', height: 38 }} />
setDateTo(e.target.value)} style={{ width: '100%', height: 38 }} />
{(searchTerm || dateFrom || dateTo) && ( )}
{filteredLogs.length > 0 ? ( filteredLogs.map(entry => (
{entry.action} {entry.date} · {entry.time}
{entry.detail}
)) ) : (
No se encontraron registros de actividad
)}
); }; window.SalaView = SalaView; window.SalaHistorialView = SalaHistorialView;