// ============================================================
// 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
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
{(catalogState === 'error' || catalogState === 'unavailable') && (
{errorMessage}
)}
| Núm. |
Serie / Título |
Autor |
Pie de imprenta |
Clasificación |
Código de barras |
Págs. |
Etapa |
Estado |
{catalogState === 'loading' && (
| Cargando catálogo… |
)}
{catalogState === 'empty' && (
| No se encontraron libros con los filtros seleccionados. |
)}
{catalogState === 'loaded' && items.map(book => (
book.serverId && setSelectedBookId(book.serverId)}
style={{ cursor: book.serverId ? 'pointer' : 'default' }}
>
| #{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
| Núm. | Serie | Título · Autor | Clasificación | Págs. | Estatus |
{filtrados.map(book => (
onSelectBook(book)} style={{ cursor: 'pointer' }}>
| #{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}
Mención de responsabilidad
{libro.mencion}
Pie de imprenta
{libro.pieImprenta}
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 || '—'}
{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 });