// ============================================================ // Módulo Digitalización // ============================================================ const DIGITALIZATION_UPLOAD_BATCH_SIZE = 50; const DIGITALIZATION_ASSIGNMENT_STATUS = Object.freeze({ pending: 'nuevo', in_progress: 'en_digitalizacion', completed: 'digitalizado', cancelled: 'rechazado', reassigned: 'rechazado' }); const toIsoDate = (value) => { if (!value) return ''; if (/^\d{4}-\d{2}-\d{2}$/.test(String(value))) return String(value); const normalized = String(value).replace(' ', 'T'); const date = new Date(normalized); if (Number.isNaN(date.getTime())) return ''; return date.toISOString().slice(0, 10); }; const formatShortDate = (value) => { const iso = toIsoDate(value); if (!iso) return '—'; const [year, month, day] = iso.split('-'); return `${day}/${month}/${year}`; }; const mapCurrentBookStatus = (stage, status) => { switch (stage) { case 'digitization': return status === 'completed' ? 'digitalizado' : 'en_digitalizacion'; case 'editing': return status === 'completed' ? 'editado' : 'en_edicion'; case 'quality_control': if (status === 'rejected') return 'rechazado'; if (status === 'accepted' || status === 'completed') return 'aceptado'; return 'en_cc'; case 'conversion': return status === 'completed' ? 'finalizado' : 'en_conversion'; case 'publication': case 'published': return 'finalizado'; default: if (status === 'completed') return 'digitalizado'; if (status === 'in_progress') return 'en_digitalizacion'; return 'nuevo'; } }; const ServerDigitalizationDetailModal = ({ item, currentUser, onClose, onSessionExpired }) => { const [state, setState] = useState({ status: item ? 'loading' : 'idle', book: null, error: '' }); useEffect(() => { if (!item?.book?.id) return undefined; let active = true; const controller = new AbortController(); const loadBook = async () => { setState({ status: 'loading', book: null, error: '' }); try { const response = await window.apiRequest(`books/show.php?id=${encodeURIComponent(item.book.id)}`, { signal: controller.signal }); if (!active) return; setState({ status: 'loaded', book: response && response.data ? response.data.book : null, error: '' }); } catch (error) { if (!active) return; if (error && error.status === 401) { onClose(); onSessionExpired(); return; } setState({ status: 'error', book: null, error: error && error.message ? error.message : 'No fue posible cargar el detalle del libro.' }); } }; loadBook(); return () => { active = false; controller.abort(); }; }, [item?.book?.id, onClose, onSessionExpired]); if (!item) return null; const book = state.book || {}; const title = book.title || item.book?.title || 'Libro sin título'; const inventoryNumber = book.inventory_number || item.book?.inventory_number || item.book?.id || item.assignment_id; const series = book.publisher || item.book?.series_collection || 'Sin serie'; const totalPages = book.total_pages || item.book?.total_pages || 0; const barcode = book.barcode || '—'; const statusKey = mapCurrentBookStatus(item.book?.current_stage, item.book?.status); return ( {state.status === 'loading' && ( )} {state.status === 'error' && (
{state.error}
)} {state.status !== 'loading' && state.status !== 'error' && (
Datos del libro
Núm. Consecutivo
#{inventoryNumber}
Serie o Colección
{item.book?.series_collection || '—'}
Título
{title}
Autor
{book.author || item.book?.author || '—'}
Pie de imprenta
{book.publisher ? `${book.publisher}${book.publication_year ? `, ${book.publication_year}` : ''}` : '—'}
Páginas
{fmtNum(totalPages)}
Código de barras
{barcode}
Proceso de digitalización
Fecha
{formatShortDate(item.completed_at)}
Digitalizado por
{currentUser.nombre || currentUser.name || '—'}
Imágenes
{fmtNum(item.resulting_images || 0)}
Acumulado
{fmtNum(item.cumulative_images || 0)}
Proceso de edición
Fecha
Editado por
Control de calidad
Fecha
Revisado por
Estatus
Conversión
Fecha
Status final
)}
); }; const DigitalizacionLocal = ({ libros, currentUser, onUpdate, onSelectBook }) => { const [formState, setFormState] = useState({ open: false, mode: 'complete', book: null, images: '', date: '2026-08-02', error: '' }); const misLibros = libros.filter(l => (l.estatus === 'en_digitalizacion' && l.digitalizadoPor === currentUser.id) || l.estatus === 'nuevo' ); const misFinalizados = libros.filter(l => l.digitalizadoPor === currentUser.id && ['digitalizado', 'en_edicion', 'editado', 'en_cc', 'aceptado', 'rechazado', 'en_conversion', 'finalizado'].includes(l.estatus) ); const totalImagenesHoy = misFinalizados .filter(l => l.fechaDigitalizacion === '2026-08-02') .reduce((sum, l) => sum + (l.imagenesResultantes || 0), 0); const iniciarDigitalizacion = (libro) => { onUpdate(libro.cons, { estatus: 'en_digitalizacion', digitalizadoPor: currentUser.id }); }; const openCompleteModal = (libro) => { setFormState({ open: true, mode: 'complete', book: libro, images: '', date: '2026-08-02', error: '' }); }; const openEditModal = (libro) => { setFormState({ open: true, mode: 'edit', book: libro, images: libro.imagenesResultantes ? String(libro.imagenesResultantes) : '', date: libro.fechaDigitalizacion || '2026-08-02', error: '' }); }; const closeFormModal = () => { setFormState({ open: false, mode: 'complete', book: null, images: '', date: '2026-08-02', error: '' }); }; const submitLocalForm = (event) => { event.preventDefault(); if (!formState.book) return; const images = parseInt(formState.images, 10); if (isNaN(images) || images <= 0) { setFormState(previous => ({ ...previous, error: 'Ingresa una cantidad válida de imágenes.' })); return; } onUpdate(formState.book.cons, { estatus: formState.mode === 'complete' ? 'digitalizado' : formState.book.estatus, fechaDigitalizacion: formState.date, digitalizadoPor: currentUser.id, imagenesResultantes: images, acumuladoImagenes: formState.mode === 'edit' ? (formState.book.acumuladoImagenes ? formState.book.acumuladoImagenes - (formState.book.imagenesResultantes || 0) + images : images) : images, }); closeFormModal(); }; return (
Mi cola de digitalización
Obras no digitalizadas disponibles y trabajos tomados por ti · Los libros de otros usuarios no aparecen
{misLibros.length === 0 && ( )} {misLibros.map(libro => ( ))}
Núm. Serie / Título Autor Págs. Estatus Acción
#{libro.cons}
{libro.serie}
{libro.titulo}
{libro.autor} {libro.paginas} {libro.estatus === 'nuevo' ? ( ) : ( )}
Mis libros digitalizados
Últimos {Math.min(10, misFinalizados.length)} libros procesados
{misFinalizados.length === 0 && ( )} {misFinalizados.slice(-10).reverse().map(libro => ( ))}
Núm. Título Fecha Imágenes Acumulado Estatus actual Acciones
#{libro.cons}
{libro.titulo}
{fmtDate(libro.fechaDigitalizacion)} {fmtNum(libro.imagenesResultantes)} {fmtNum(libro.acumuladoImagenes)}
} > {formState.book && (
#{formState.book.cons}
{formState.book.titulo}
{formState.book.autor} · {formState.book.paginas} páginas
setFormState(previous => ({ ...previous, images: event.target.value, error: '' }))} autoFocus />
Debe coincidir con las páginas escaneadas más portadas y guardas
{currentUser.nombre}
{formState.error &&
{formState.error}
}
)}
); }; const DigitalizacionServer = ({ currentUser, onSessionExpired }) => { const [state, setState] = useState({ status: 'loading', error: '', stats: { queue_count: 0, completed_total: 0, images_today: 0 }, queue: [], queuePagination: { page: 1, per_page: 10, total: 0, total_pages: 0 }, recent: [] }); const [reloadToken, setReloadToken] = useState(0); const [queueSearch, setQueueSearch] = useState(''); const [queueSearchInput, setQueueSearchInput] = useState(''); const [queuePage, setQueuePage] = useState(1); const [queuePerPage, setQueuePerPage] = useState(10); const [feedback, setFeedback] = useState(null); const [operationById, setOperationById] = useState({}); const [detailItem, setDetailItem] = useState(null); const [successModal, setSuccessModal] = useState({ open: false, title: '', message: '', path: '', count: 0 }); const [uploadProgress, setUploadProgress] = useState({ active: false, uploaded: 0, total: 0, batch: 0, totalBatches: 0, percent: 0, message: '' }); const [formState, setFormState] = useState({ open: false, mode: 'complete', assignment: null, images: '', files: [], imageOperation: 'append', imageStatus: null, statusLoading: false, date: new Date().toISOString().slice(0, 10), error: '' }); useEffect(() => { let active = true; const controller = new AbortController(); const loadDashboard = async () => { setState(previous => ({ ...previous, status: 'loading', error: '' })); try { const query = new URLSearchParams({ page: String(queuePage), per_page: String(queuePerPage) }); if (queueSearch) query.set('search', queueSearch); const response = await window.apiRequest(`digitalization/dashboard.php?${query.toString()}`, { signal: controller.signal }); if (!active) return; const data = response && response.data ? response.data : {}; setState({ status: 'loaded', error: '', stats: data.stats || { queue_count: 0, completed_total: 0, images_today: 0 }, queue: Array.isArray(data.queue_items) ? data.queue_items : [], queuePagination: data.queue_pagination || { page: queuePage, per_page: queuePerPage, total: 0, total_pages: 0 }, recent: Array.isArray(data.recent_completed) ? data.recent_completed : [] }); } catch (error) { if (!active) return; if (error && error.status === 401) { onSessionExpired(); return; } setState(previous => ({ ...previous, status: 'error', error: error && error.message ? error.message : 'No fue posible obtener los datos de digitalización.' })); } }; loadDashboard(); return () => { active = false; controller.abort(); }; }, [reloadToken, queueSearch, queuePage, queuePerPage, onSessionExpired]); const submitQueueSearch = (event) => { event.preventDefault(); setQueuePage(1); setQueueSearch(queueSearchInput.trim()); }; const clearQueueSearch = () => { setQueueSearchInput(''); setQueueSearch(''); setQueuePage(1); }; const changeQueuePerPage = (value) => { setQueuePerPage(Number(value)); setQueuePage(1); }; const refreshData = () => setReloadToken(value => value + 1); const setOperation = (assignmentId, value) => { setOperationById(previous => ({ ...previous, [assignmentId]: value })); }; const clearOperation = (assignmentId) => { setOperationById(previous => { const next = { ...previous }; delete next[assignmentId]; return next; }); }; const startAssignment = async (assignment) => { const assignmentId = assignment.assignment_id; if (!assignmentId) return; setFeedback(null); setOperation(assignmentId, 'starting'); try { await window.assignmentsService.startAssignment(assignmentId); setFeedback({ tone: 'success', message: 'La digitalización se inició correctamente.' }); refreshData(); } catch (error) { if (error && error.status === 401) { onSessionExpired(); return; } setFeedback({ tone: 'error', message: error && error.message ? error.message : 'No fue posible iniciar la digitalización.' }); } finally { clearOperation(assignmentId); } }; const claimAvailableBook = async (item) => { const bookId = item?.book?.id; if (!bookId) return; const operationKey = `book-${bookId}`; setFeedback(null); setOperation(operationKey, 'claiming'); try { await window.apiRequest('digitalization/claim.php', { method: 'POST', body: { book_id: bookId } }); setFeedback({ tone: 'success', message: 'El libro se agregó a tu cola y la digitalización quedó iniciada.' }); refreshData(); } catch (error) { if (error && error.status === 401) { onSessionExpired(); return; } if (error && error.status === 409) { setFeedback({ tone: 'warning', message: error.message || 'Otro usuario ya tomó este libro.' }); refreshData(); return; } setFeedback({ tone: 'error', message: error && error.message ? error.message : 'No fue posible tomar el libro.' }); } finally { clearOperation(operationKey); } }; const openCompleteModal = (assignment) => { setFeedback(null); setUploadProgress({ active: false, uploaded: 0, total: 0, batch: 0, totalBatches: 0, percent: 0, message: '' }); setFormState({ open: true, mode: 'complete', assignment, images: '', files: [], imageOperation: 'append', imageStatus: null, statusLoading: false, date: new Date().toISOString().slice(0, 10), error: '' }); }; const openEditModal = async (assignment) => { setFeedback(null); setUploadProgress({ active: false, uploaded: 0, total: 0, batch: 0, totalBatches: 0, percent: 0, message: '' }); setFormState({ open: true, mode: 'edit', assignment, images: assignment.resulting_images ? String(assignment.resulting_images) : '', files: [], imageOperation: 'append', imageStatus: null, statusLoading: true, date: toIsoDate(assignment.completed_at) || new Date().toISOString().slice(0, 10), error: '' }); try { const response = await window.apiRequest( `digitalization/edit-status.php?assignment_id=${encodeURIComponent(assignment.assignment_id)}` ); const status = response && response.data ? response.data : null; setFormState(previous => { if (!previous.open || previous.assignment?.assignment_id !== assignment.assignment_id) return previous; return { ...previous, imageStatus: status, statusLoading: false, images: String(status?.database_image_count ?? assignment.resulting_images ?? 0), date: toIsoDate(status?.effective_at) || previous.date, error: '' }; }); } catch (error) { if (error && error.status === 401) { onSessionExpired(); return; } setFormState(previous => ({ ...previous, statusLoading: false, error: error && error.message ? error.message : 'No fue posible consultar las imágenes actuales.' })); } }; const closeFormModal = () => { const assignmentId = formState.assignment?.assignment_id; if (assignmentId && operationById[assignmentId]) return; setUploadProgress({ active: false, uploaded: 0, total: 0, batch: 0, totalBatches: 0, percent: 0, message: '' }); setFormState({ open: false, mode: 'complete', assignment: null, images: '', files: [], imageOperation: 'append', imageStatus: null, statusLoading: false, date: new Date().toISOString().slice(0, 10), error: '' }); }; const uploadImagesInBatches = async (assignmentId, operation, selectedFiles) => { const total = selectedFiles.length; const totalBatches = Math.ceil(total / DIGITALIZATION_UPLOAD_BATCH_SIZE); const uploadId = `${assignmentId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; let finalData = null; setUploadProgress({ active: true, uploaded: 0, total, batch: 0, totalBatches, percent: 0, message: `Preparando ${total} imágenes…` }); for (let index = 0; index < totalBatches; index += 1) { const start = index * DIGITALIZATION_UPLOAD_BATCH_SIZE; const batchFiles = selectedFiles.slice(start, start + DIGITALIZATION_UPLOAD_BATCH_SIZE); const currentBatch = index + 1; setUploadProgress(previous => ({ ...previous, active: true, batch: currentBatch, totalBatches, message: `Subiendo lote ${currentBatch} de ${totalBatches}…` })); const uploadBody = new FormData(); uploadBody.append('assignment_id', String(assignmentId)); uploadBody.append('mode', operation); uploadBody.append('upload_id', uploadId); uploadBody.append('batch_index', String(currentBatch)); uploadBody.append('total_batches', String(totalBatches)); uploadBody.append('total_files', String(total)); batchFiles.forEach(file => uploadBody.append('images[]', file, file.name)); const uploadResponse = await window.apiRequest('digitalization/upload-images.php', { method: 'POST', body: uploadBody, timeout: 180000 }); finalData = uploadResponse && uploadResponse.data ? uploadResponse.data : {}; const uploaded = Math.min(total, start + batchFiles.length); const percent = total > 0 ? Math.round((uploaded / total) * 100) : 100; setUploadProgress({ active: currentBatch < totalBatches, uploaded, total, batch: currentBatch, totalBatches, percent, message: currentBatch < totalBatches ? `${uploaded} de ${total} imágenes cargadas` : `Carga completa: ${uploaded} de ${total} imágenes` }); } return finalData || {}; }; const submitServerForm = async (event) => { event.preventDefault(); const assignment = formState.assignment; const assignmentId = assignment?.assignment_id; let images = parseInt(formState.images, 10); if (!assignmentId) { setFormState(previous => ({ ...previous, error: 'La asignación ya no está disponible.' })); return; } if (!formState.date) { setFormState(previous => ({ ...previous, error: 'La fecha de digitalización es obligatoria.' })); return; } const isComplete = formState.mode === 'complete'; const operation = isComplete ? 'replace' : formState.imageOperation; const requiresFiles = isComplete || ['append', 'replace'].includes(operation); if (requiresFiles && formState.files.length === 0) { setFormState(previous => ({ ...previous, error: 'Selecciona las imágenes JPG que se van a subir.' })); return; } if (operation === 'database_only') { if (isNaN(images) || images < 0) { setFormState(previous => ({ ...previous, error: 'Ingresa una cantidad válida para la base de datos.' })); return; } } else if (isComplete || operation === 'replace') { images = formState.files.length; } else if (operation === 'append') { images = Number(formState.imageStatus?.filesystem_image_count || 0) + formState.files.length; } const action = formState.mode === 'edit' ? 'editing-completion' : 'completing'; setOperation(assignmentId, action); setFeedback(null); setFormState(previous => ({ ...previous, error: '' })); try { let uploadData = null; if (operation !== 'database_only') { if (operation === 'replace' && formState.mode === 'edit') { const confirmed = window.confirm( `Se reemplazarán las ${formState.imageStatus?.filesystem_image_count || 0} imágenes actuales. ` + 'Los archivos existentes se moverán a Backup antes de continuar. ¿Deseas continuar?' ); if (!confirmed) { clearOperation(assignmentId); return; } } uploadData = await uploadImagesInBatches(assignmentId, operation, formState.files); images = Number(uploadData.final_count ?? images); } if (formState.mode === 'edit') { await window.apiRequest('digitalization/update-completion.php', { method: 'POST', body: { assignment_id: assignmentId, resulting_images: images, effective_at: formState.date } }); const successMessage = operation === 'database_only' ? 'La cantidad registrada en la base de datos se actualizó sin modificar archivos.' : `La digitalización se actualizó correctamente. La carpeta contiene ${images} imágenes.`; setFeedback({ tone: 'success', message: successMessage }); setSuccessModal({ open: true, title: 'Digitalización actualizada', message: successMessage, path: uploadData?.relative_path || formState.imageStatus?.relative_path || '', count: images }); } else { await window.assignmentsService.completeAssignment(assignmentId, { resulting_images: images, effective_at: formState.date, upload_relative_path: uploadData?.relative_path || null, uploaded_files: Array.isArray(uploadData?.files) ? uploadData.files : [] }); const successMessage = `La digitalización se registró correctamente. Se guardaron ${images} imágenes.`; setFeedback({ tone: 'success', message: successMessage }); setSuccessModal({ open: true, title: 'Carga completada', message: successMessage, path: uploadData?.relative_path || 'INPI 2026/DIGITALIZACION', count: images }); } setFormState({ open: false, mode: 'complete', assignment: null, images: '', files: [], imageOperation: 'append', imageStatus: null, statusLoading: false, date: new Date().toISOString().slice(0, 10), error: '' }); setUploadProgress(previous => ({ ...previous, active: false, percent: 100, message: 'Carga completada.' })); refreshData(); } catch (error) { setUploadProgress(previous => ({ ...previous, active: false, message: previous.total > 0 ? `La carga se detuvo después de ${previous.uploaded} de ${previous.total} imágenes.` : previous.message })); if (error && error.status === 401) { onSessionExpired(); return; } setFormState(previous => ({ ...previous, error: error && error.message ? error.message : (formState.mode === 'edit' ? 'No fue posible editar la digitalización.' : 'No fue posible finalizar la digitalización.') })); } finally { clearOperation(assignmentId); } }; const queueItems = state.queue; const queuePagination = state.queuePagination || { page: 1, per_page: queuePerPage, total: 0, total_pages: 0 }; const recentItems = state.recent; const isSavingForm = formState.assignment ? Boolean(operationById[formState.assignment.assignment_id]) : false; if (state.status === 'error') { return (
); } return (
{feedback && (
{feedback.message}
)}
Mi cola de digitalización
Obras no digitalizadas disponibles y trabajos tomados por ti · Los libros tomados por otros usuarios no aparecen
setQueueSearchInput(event.target.value)} /> {(queueSearch || queueSearchInput) && ( )}
{queueSearch && (
Resultados para: {queueSearch}
)}
{state.status === 'loading' && ( )} {state.status === 'loaded' && queueItems.length === 0 && ( )} {queueItems.map(item => { const assignmentStatus = item.status; const isAvailable = item.queue_type === 'available'; const localStatusKey = DIGITALIZATION_ASSIGNMENT_STATUS[assignmentStatus] || 'nuevo'; const operationKey = isAvailable ? `book-${item.book?.id}` : item.assignment_id; const operation = operationById[operationKey]; return ( ); })}
Núm. Serie / Título Autor Págs. Estatus Acción
#{item.book?.inventory_number || item.book?.id || item.assignment_id}
{item.book?.series_collection || 'Sin serie'}
{item.book?.title || 'Libro sin título'}
{item.book?.author || 'Sin autor'} {fmtNum(item.book?.total_pages || 0)} {isAvailable ? ( Disponible ) : } {isAvailable ? ( ) : assignmentStatus === 'pending' ? ( ) : ( )}
{queuePagination.total === 0 ? 'Sin resultados' : `Mostrando ${(queuePagination.page - 1) * queuePagination.per_page + 1}–${Math.min(queuePagination.page * queuePagination.per_page, queuePagination.total)} de ${queuePagination.total} obras`}
Página {queuePagination.total_pages ? queuePagination.page : 0} de {queuePagination.total_pages || 0}
Mis libros digitalizados
Últimos 6 libros procesados
{state.status === 'loading' && ( )} {state.status === 'loaded' && recentItems.length === 0 && ( )} {recentItems.map(item => { const currentStatusKey = mapCurrentBookStatus(item.book?.current_stage, item.book?.status); const operation = operationById[item.assignment_id]; return ( ); })}
Núm. Título Fecha Imágenes Acumulado Estatus actual Acciones
#{item.book?.inventory_number || item.book?.id || item.assignment_id}
{item.book?.title || 'Libro sin título'}
{formatShortDate(item.completed_at)} {fmtNum(item.resulting_images || 0)} {fmtNum(item.cumulative_images || 0)}
} > {formState.assignment && (
#{formState.assignment.book?.inventory_number || formState.assignment.book?.id}
{formState.assignment.book?.title || 'Libro sin título'}
{formState.assignment.book?.author || 'Sin autor'} · {fmtNum(formState.assignment.book?.total_pages || 0)} páginas
setFormState(previous => ({ ...previous, images: event.target.value, error: '' }))} autoFocus />
Debe coincidir con las páginas escaneadas más portadas y guardas
{formState.mode === 'edit' && (
Estado actual de las imágenes
{formState.statusLoading ? (
Consultando la base de datos y la carpeta del libro…
) : formState.imageStatus ? (
Cantidad registrada en la base de datos {fmtNum(formState.imageStatus.database_image_count)}
Imágenes encontradas en la carpeta {fmtNum(formState.imageStatus.filesystem_image_count)}
Carpeta: {formState.imageStatus.relative_path}
Primer archivo: {formState.imageStatus.relative_path}\1.jpg
{formState.imageStatus.images_match ? '✓ La cantidad registrada coincide con los archivos almacenados.' : `⚠ La base de datos registra ${formState.imageStatus.database_image_count}, pero se encontraron ${formState.imageStatus.filesystem_image_count} archivos.`}
) : null}
)} {formState.mode === 'edit' && (
)} {(formState.mode === 'complete' || formState.imageOperation !== 'database_only') && (
{ const selectedFiles = Array.from(event.target.files || []); setFormState(previous => { let nextImages = previous.images; if (previous.mode === 'complete' || previous.imageOperation === 'replace') { nextImages = selectedFiles.length ? String(selectedFiles.length) : previous.images; } else if (previous.imageOperation === 'append') { nextImages = String(Number(previous.imageStatus?.filesystem_image_count || 0) + selectedFiles.length); } return { ...previous, files: selectedFiles, images: nextImages, error: '' }; }); }} />
Se conservan correctamente los nombres originales, incluidos acentos y ñ. Si un nombre ya existe, el sistema evitará sobrescribirlo.
{formState.files.length > 0 && (
{formState.files.length} imagen(es) seleccionada(s)
Cantidad final estimada: {formState.mode === 'edit' && formState.imageOperation === 'append' ? Number(formState.imageStatus?.filesystem_image_count || 0) + formState.files.length : formState.files.length}
)}
)}
{currentUser.nombre || currentUser.name || 'Usuario'}
{(uploadProgress.total > 0 || uploadProgress.active) && (
{uploadProgress.message || 'Preparando carga…'} {uploadProgress.percent}%
Lote {uploadProgress.batch || 0} de {uploadProgress.totalBatches || 0} {' · '} {uploadProgress.uploaded || 0} de {uploadProgress.total || 0} imágenes {' · '} Máximo {DIGITALIZATION_UPLOAD_BATCH_SIZE} archivos por solicitud
)} {isSavingForm && (
{uploadProgress.active ? uploadProgress.message : (formState.mode === 'edit' ? 'Guardando cambios y verificando las imágenes…' : 'Registrando la digitalización…')}
)} {formState.error && (
{formState.error}
)} )} setDetailItem(null)} onSessionExpired={onSessionExpired} /> setSuccessModal({ open: false, title: '', message: '', path: '', count: 0 })} title={successModal.title || 'Operación completada'} maxWidth={520} footer={ } >
{successModal.message}
{successModal.path && (
Carpeta: {successModal.path}
Primer archivo: {successModal.path}\1.jpg
)}
); }; const Digitalizacion = (props) => { const serverMode = window.APP_CONFIG.AUTH.MODE === 'server'; if (serverMode) { return ; } return ; }; Object.assign(window, { Digitalizacion });