// ============================================================ // Módulo Rutas de Imágenes // En modo servidor, "Rutas por libro" consulta MySQL mediante booksService. // ============================================================ const Rutas = ({ libros: librosLocales = [], onUpdate, currentUser, onSessionExpired }) => { const serverMode = window.APP_CONFIG.AUTH.MODE === 'server'; const [librosServidor, setLibrosServidor] = useState([]); const [loadingBooks, setLoadingBooks] = useState(serverMode); const [booksError, setBooksError] = useState(''); const [rutasPorLibro, setRutasPorLibro] = useState({}); const [asignando, setAsignando] = useState(null); const [rutaBase, setRutaBase] = useState('\\\\SERVIDOR-DIG\\Digitalizacion\\FCE_2026'); const [modoLote, setModoLote] = useState(false); const [seleccionados, setSeleccionados] = useState(new Set()); const [filterSerie, setFilterSerie] = useState(''); const [search, setSearch] = useState(''); const [showRutaBase, setShowRutaBase] = useState(false); const [rutas, setRutas] = useState({ origen: '\\\\SERVIDOR-DIG\\Digitalizacion\\FCE_2026', edicion: '\\\\SERVIDOR-EDIT\\Trabajo\\FCE_2026', controlCalidad:'\\\\SERVIDOR-CC\\Revision\\FCE_2026', salidaFinal: '\\\\SERVIDOR-FIN\\Publicacion\\FCE_2026', backup: 'S:\\Backup\\FCE_2026', }); const mapServerStatus = (book) => { const stage = book.currentStage || book.current_stage || ''; const status = book.status || ''; if (stage === 'published' || stage === 'publication') return 'finalizado'; if (stage === 'conversion') return status === 'completed' ? 'finalizado' : 'en_conversion'; if (stage === 'quality_control') { if (status === 'approved') return 'aceptado'; if (status === 'rejected' || status === 'correction_required') return 'rechazado'; return 'en_cc'; } if (stage === 'editing') return status === 'completed' ? 'editado' : 'en_edicion'; if (stage === 'digitization') return status === 'completed' ? 'digitalizado' : 'en_digitalizacion'; return 'nuevo'; }; const normalizeServerBook = (book) => { const serverId = book.serverId || book.id; const consValue = book.catalogNumber ?? book.catalog_number ?? book.inventoryNumber ?? book.inventory_number ?? serverId; return { ...book, serverId, cons: String(consValue ?? '').padStart(4, '0'), serie: book.seriesCollection || book.series_collection || book.serie || 'Sin serie', titulo: book.title || book.titulo || 'Sin título', autor: book.author || book.autor || '', estatus: mapServerStatus(book), rutaImagenes: rutasPorLibro[serverId] || null, }; }; const loadAllBooks = async () => { if (!serverMode) return; setLoadingBooks(true); setBooksError(''); try { const first = await window.booksService.listBooks({ page: 1, per_page: 100, sort: 'inventory_number', direction: 'asc' }); const all = [...first.items]; const totalPages = Math.max(1, Number(first.pagination.total_pages) || 1); for (let page = 2; page <= totalPages; page += 1) { const result = await window.booksService.listBooks({ page, per_page: 100, sort: 'inventory_number', direction: 'asc' }); all.push(...result.items); } setLibrosServidor(all); } catch (error) { if (error && error.status === 401 && typeof onSessionExpired === 'function') { onSessionExpired(); return; } setBooksError(error?.message || 'No fue posible cargar los libros desde la base de datos.'); } finally { setLoadingBooks(false); } }; useEffect(() => { loadAllBooks(); }, [serverMode]); const libros = serverMode ? librosServidor.map(normalizeServerBook) : librosLocales; // En servidor se muestran todos los libros reales. En demo se conserva el comportamiento anterior. const librosDisponibles = serverMode ? libros : libros.filter(l => ['digitalizado','en_edicion','editado','en_cc','aceptado','rechazado','en_conversion','finalizado'].includes(l.estatus) ); const series = [...new Set(librosDisponibles.map(l => l.serie).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'es')); const normalizedSearch = search.trim().toLowerCase(); const filtrados = librosDisponibles.filter((libro) => { if (filterSerie && libro.serie !== filterSerie) return false; if (!normalizedSearch) return true; return [libro.cons, libro.serie, libro.titulo, libro.autor] .some(value => String(value || '').toLowerCase().includes(normalizedSearch)); }); const conRuta = librosDisponibles.filter(l => l.rutaImagenes).length; const sinRuta = librosDisponibles.length - conRuta; const generarRuta = (libro) => { const serie = String(libro.serie || 'Sin_serie').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-zA-Z0-9]/g, '_'); const titulo = String(libro.titulo || 'Sin_titulo').normalize('NFD').replace(/[\u0300-\u036f]/g, '').substring(0, 30).replace(/[^a-zA-Z0-9]/g, '_'); return `${rutaBase}\\${serie}\\${libro.cons}_${titulo}`; }; const applyRoute = (libro, ruta) => { if (serverMode) { setRutasPorLibro(prev => ({ ...prev, [libro.serverId]: ruta })); } else if (typeof onUpdate === 'function') { onUpdate(libro.cons, { rutaImagenes: ruta }); } }; const asignarLote = () => { seleccionados.forEach(key => { const libro = librosDisponibles.find(l => String(l.serverId || l.cons) === String(key)); if (libro) applyRoute(libro, generarRuta(libro)); }); setSeleccionados(new Set()); setModoLote(false); }; const toggleSel = (key) => { const nueva = new Set(seleccionados); if (nueva.has(key)) nueva.delete(key); else nueva.add(key); setSeleccionados(nueva); }; const selTodos = () => { if (seleccionados.size === filtrados.length) setSeleccionados(new Set()); else setSeleccionados(new Set(filtrados.map(l => String(l.serverId || l.cons)))); }; const guardarRuta = (libro, ruta) => { applyRoute(libro, ruta); setAsignando(null); }; return (
0 ? 'danger' : 'success'}/>
Rutas configuradas del proyecto
Ubicaciones de los servidores donde se almacenan las imágenes
{[ { key: 'origen', label: 'Origen (Digitalización)', color: 'var(--info)', icon: 'camera' }, { key: 'edicion', label: 'Trabajo (Edición)', color: '#7e22ce', icon: 'edit' }, { key: 'controlCalidad', label: 'Revisión (CC)', color: 'var(--warn)', icon: 'check_shield' }, { key: 'salidaFinal', label: 'Publicación (Final)', color: 'var(--success)', icon: 'convert' }, { key: 'backup', label: 'Backup', color: 'var(--text-2)', icon: 'library' }, ].map(r => (
{r.label}
{showRutaBase ? ( setRutas({ ...rutas, [r.key]: e.target.value })}/> ) : (
{rutas[r.key]}
)}
))}
Rutas por libro
{serverMode ? 'Libros reales consultados desde MySQL' : 'Asigna la ubicación específica de cada libro digitalizado'}
{serverMode && } {modoLote ? ( <> ) : ( )}
setRutaBase(e.target.value)} placeholder="\\SERVIDOR\Carpeta"/>
setSearch(e.target.value)} placeholder="Número, título o autor"/>
La ruta se genera como {'{RUTA_BASE}\\{SERIE}\\{NUM_CONS}_{TITULO}'}
{serverMode &&
La lista proviene del servidor. Las asignaciones de ruta hechas en esta pantalla se mantienen durante la sesión; la persistencia definitiva por libro requiere una tabla/API específica.
}
{booksError &&
{booksError}
}
{modoLote && } {loadingBooks && } {!loadingBooks && filtrados.length === 0 && } {!loadingBooks && filtrados.map(l => { const key = String(l.serverId || l.cons); const tieneRuta = !!l.rutaImagenes; return {modoLote && } ; })}
{seleccionados.size === filtrados.length && filtrados.length > 0 && }
Núm.Serie / TítuloEstatusRuta de imágenesAcción
Cargando libros reales desde MySQL…
toggleSel(key)}>{seleccionados.has(key) && }
#{l.cons}
{l.serie}
{l.titulo}
{l.autor &&
{l.autor}
}
{tieneRuta ?
{l.rutaImagenes}
: Sin asignar}
{!loadingBooks &&
Mostrando {filtrados.length} de {librosDisponibles.length} libros.
}
{asignando && setAsignando(null)} onSave={(libro, ruta) => guardarRuta(libro, ruta)} generarRuta={generarRuta}/>}
); }; const RutaModal = ({ libro, onClose, onSave, generarRuta }) => { const [ruta, setRuta] = useState(''); const [validando, setValidando] = useState(false); const [estadoRuta, setEstadoRuta] = useState(null); useEffect(() => { const initial = libro.rutaImagenes || generarRuta(libro); setRuta(initial); setEstadoRuta(libro.rutaImagenes ? 'valida' : null); }, [libro]); const validarRuta = () => { setValidando(true); setEstadoRuta(null); setTimeout(() => { setValidando(false); setEstadoRuta(ruta.trim() ? 'valida' : 'invalida'); }, 500); }; return }>
Libro
{libro.titulo}
{libro.serie}{libro.autor ? ` · ${libro.autor}` : ''}
{ setRuta(e.target.value); setEstadoRuta(null); }} style={{ flex: 1 }}/>
{estadoRuta === 'valida' &&
La ruta tiene un formato válido.
} {estadoRuta === 'invalida' &&
Escribe una ruta válida.
}
; };