// ============================================================ // Catálogo de libros: servidor de solo lectura y demo local explícita // ============================================================ const catalogValue = (value) => value === null || value === undefined || value === '' ? 'No especificado' : value; const catalogLabel = (catalog, value) => value ? (catalog[value] || value) : 'No especificado'; const CatalogBadge = ({ value, catalog }) => ( {catalogLabel(catalog, value)} ); const ServerBookDetail = ({ bookId, onClose, onSessionExpired }) => { const [state, setState] = useState('loading'); const [book, setBook] = useState(null); const [message, setMessage] = useState(''); const [retryToken, setRetryToken] = useState(0); const requestRef = useRef(0); useEffect(() => { if (!bookId) return undefined; const controller = new AbortController(); const requestId = ++requestRef.current; setState('loading'); setMessage(''); window.booksService.getBookById(bookId, { signal: controller.signal }) .then((loadedBook) => { if (requestId !== requestRef.current || controller.signal.aborted) return; setBook(loadedBook); setState(loadedBook ? 'loaded' : 'error'); if (!loadedBook) setMessage('No fue posible interpretar los datos del libro.'); }) .catch((error) => { if (controller.signal.aborted || requestId !== requestRef.current) return; if (error?.status === 401) { onClose(); onSessionExpired(); return; } setBook(null); setState(error?.status === 0 || error?.status >= 500 ? 'unavailable' : 'error'); setMessage( error?.status === 403 ? 'No tienes permiso para consultar este libro.' : error?.status === 404 ? 'El libro solicitado ya no está disponible.' : 'No fue posible cargar el detalle del libro.' ); }); return () => controller.abort(); }, [bookId, retryToken]); return ( {state === 'loading' && (
Cargando detalle…
)} {(state === 'error' || state === 'unavailable') && (
{message}
)} {state === 'loaded' && book && (
Vista de solo lectura · datos del servidor
Datos bibliográficos
Núm. consecutivo
{catalogValue(book.catalogNumber)}
Código de barras / inventario
{catalogValue(book.inventoryNumber)}
Serie o colección
{catalogValue(book.seriesCollection)}
Título
{catalogValue(book.title)}
Autor
{catalogValue(book.author)}
Mención de responsabilidad
{catalogValue(book.responsibilityMention)}
Pie de imprenta
{catalogValue(book.imprint)}
Clasificación
{catalogValue(book.classification)}
Año
{catalogValue(book.publicationYear)}
Idioma
{catalogValue(book.language)}
Páginas
{catalogValue(book.totalPages)}
Estado físico
{catalogValue(book.physicalCondition)}
Código de barras
{catalogValue(book.barcode)}
ISBN
{catalogValue(book.isbn)}
Estado del proceso
Etapa
Estado
Prioridad
Proyecto
{catalogValue(book.project?.name)}
Observaciones
{catalogValue(book.observations)}
Registro
Creado
{catalogValue(book.createdAt)}
Actualizado
{catalogValue(book.updatedAt)}
)}
); }; const ServerCatalogo = ({ onSessionExpired }) => { const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [currentStage, setCurrentStage] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [priority, setPriority] = useState(''); const [sort, setSort] = useState('updated_at'); const [direction, setDirection] = useState('desc'); const [page, setPage] = useState(1); const [catalogState, setCatalogState] = useState('loading'); const [items, setItems] = useState([]); const [pagination, setPagination] = useState({ page: 1, per_page: 20, total: 0, total_pages: 0 }); const [errorMessage, setErrorMessage] = useState(''); const [retryToken, setRetryToken] = useState(0); const [selectedBookId, setSelectedBookId] = useState(null); const requestRef = useRef(0); useEffect(() => { const timeoutId = window.setTimeout(() => { setPage(1); setSearch(searchInput.trim()); }, 300); return () => window.clearTimeout(timeoutId); }, [searchInput]); useEffect(() => { const controller = new AbortController(); const requestId = ++requestRef.current; setCatalogState('loading'); setErrorMessage(''); window.booksService.listBooks({ search, current_stage: currentStage, status: statusFilter, priority, page, per_page: 20, sort, direction, signal: controller.signal }).then((result) => { if (controller.signal.aborted || requestId !== requestRef.current) return; setItems(result.items); setPagination(result.pagination); setCatalogState(result.items.length === 0 ? 'empty' : 'loaded'); }).catch((error) => { if (controller.signal.aborted || requestId !== requestRef.current) return; setItems([]); if (error?.status === 401) { onSessionExpired(); return; } if (error?.status === 403) { setCatalogState('error'); setErrorMessage('No tienes permiso para consultar el catálogo.'); return; } setCatalogState(error?.status === 0 || error?.status >= 500 ? 'unavailable' : 'error'); setErrorMessage( error?.status === 0 || error?.status >= 500 ? 'No fue posible conectar con el servicio de libros.' : 'No fue posible obtener el catálogo.' ); }); return () => controller.abort(); }, [search, currentStage, statusFilter, priority, page, sort, direction, retryToken]); const updateFilter = (setter) => (event) => { setter(event.target.value); setPage(1); }; const clearFilters = () => { setSearchInput(''); setSearch(''); setCurrentStage(''); setStatusFilter(''); setPriority(''); setSort('updated_at'); setDirection('desc'); setPage(1); }; const canGoBack = page > 1 && catalogState !== 'loading'; const canGoForward = page < pagination.total_pages && catalogState !== 'loading'; return (
Catálogo de libros · Base de datos
{pagination.total} libros · servidor · solo lectura · click en fila para ver detalle
setSearchInput(event.target.value)} style={{ paddingLeft: 32 }} />
{(catalogState === 'error' || catalogState === 'unavailable') && (
{errorMessage}
)}
{catalogState === 'loading' && ( )} {catalogState === 'empty' && ( )} {catalogState === 'loaded' && items.map(book => ( book.serverId && setSelectedBookId(book.serverId)} style={{ cursor: book.serverId ? 'pointer' : 'default' }} > ))}
Núm. Serie / Título Autor Pie de imprenta Clasificación Código de barras Págs. Etapa Estado
Cargando catálogo…
No se encontraron libros con los filtros seleccionados.
#{catalogValue(book.catalogNumber)}
{catalogValue(book.seriesCollection)}
{catalogValue(book.title)}
{catalogValue(book.author)}
{catalogValue(book.imprint)}
{catalogValue(book.publicationYear)}
{catalogValue(book.classification)} {catalogValue(book.barcode)} {catalogValue(book.totalPages)}
Página {pagination.page} de {pagination.total_pages || 1}
setSelectedBookId(null)} onSessionExpired={onSessionExpired} />
); }; const LocalCatalogo = ({ libros, onSelectBook }) => { const [q, setQ] = useState(''); const [filterSerie, setFilterSerie] = useState(''); const [filterEstatus, setFilterEstatus] = useState(''); const filtrados = useMemo(() => libros.filter(book => { if (q && !(book.titulo.toLowerCase().includes(q.toLowerCase()) || book.autor.toLowerCase().includes(q.toLowerCase()) || book.cons.includes(q) || book.codigoBarras.includes(q))) return false; if (filterSerie && book.serie !== filterSerie) return false; if (filterEstatus && book.estatus !== filterEstatus) return false; return true; }), [libros, q, filterSerie, filterEstatus]); return (
Catálogo de libros · Base de datos
{filtrados.length} de {libros.length} libros · demo local
setQ(event.target.value)}/>
{filtrados.map(book => ( onSelectBook(book)} style={{ cursor: 'pointer' }}> ))}
Núm.SerieTítulo · AutorClasificaciónPágs.Estatus
#{book.cons} {book.serie}
{book.titulo}
{book.autor}
{book.clasificacion} {book.paginas}
); }; const Catalogo = ({ libros, onSelectBook, onSessionExpired }) => { const isServerMode = window.APP_CONFIG.AUTH.MODE === 'server'; if (isServerMode) return ; return ; }; // Detalle local heredado para los demás módulos y para local_demo. const BookDetail = ({ libro, onClose }) => { if (!libro) return null; const dig = getUser(libro.digitalizadoPor); const ed = getUser(libro.editadoPor); const cc = getUser(libro.ccPor); return (
Datos del libro
Núm. Consecutivo
#{libro.cons}
Serie o Colección
{libro.serie}
Título
{libro.titulo}
Autor
{libro.autor}
Mención de responsabilidad
{libro.mencion}
Pie de imprenta
{libro.pieImprenta}
Páginas
{libro.paginas}
Código de barras
{libro.codigoBarras}
Clasificación
{libro.clasificacion}
Proceso de digitalización
Fecha
{fmtDate(libro.fechaDigitalizacion)}
Digitalizado por
{dig?.nombre || '—'}
Imágenes
{fmtNum(libro.imagenesResultantes)}
Acumulado
{fmtNum(libro.acumuladoImagenes)}
Proceso de edición
Fecha
{fmtDate(libro.fechaEdicion)}
Editado por
{ed?.nombre || '—'}
{libro.cortes && (
Cortes horarios
11:00 → {libro.cortes.c11} · 13:00 → {libro.cortes.c13} · 16:00 → {libro.cortes.c16} · 18:00 → {libro.cortes.c18}
)}
Control de calidad
Fecha
{fmtDate(libro.fechaControlCalidad)}
Revisado por
{cc?.nombre || '—'}
Estatus
{libro.observaciones && (
Observaciones
{libro.observaciones}
)}
Conversión
Fecha
{fmtDate(libro.fechaConversion)}
Status final
{libro.statusFinal || '—'}
Formatos
{[ ['JPG', libro.edicionJpg], ['TIFF', libro.conversionTiff], ['PDF Color', libro.pdfColor], ['PDF B&N', libro.pdfBn], ['EPUB', libro.epub] ].map(([label, done]) => ( {done ? '✓' : '·'} {label} ))}
); }; Object.assign(window, { Catalogo, BookDetail });