// ============================================================ // Módulo Edición - con cortes horarios 11am / 1pm / 4pm / 6pm // ============================================================ const CORTES = [ { key: 'c11', hora: '11:00', label: 'Corte AM' }, { key: 'c13', hora: '13:00', label: 'Pre-comida' }, { key: 'c16', hora: '16:00', label: 'Post-comida' }, { key: 'c18', hora: '18:00', label: 'Cierre' }, ]; const EdicionLocal = ({ libros, currentUser, onUpdate, onSelectBook }) => { const [editando, setEditando] = useState(null); const [imagenes, setImagenes] = useState(''); const [cortesForm, setCortesForm] = useState({ c11: '', c13: '', c16: '', c18: '' }); // Cola FIFO: libros digitalizados ordenados por fecha, asignados al editor o disponibles const misEnCurso = libros.filter(l => l.estatus === 'en_edicion' && l.editadoPor === currentUser.id); const disponibles = libros.filter(l => l.estatus === 'digitalizado') .sort((a, b) => a.fechaDigitalizacion.localeCompare(b.fechaDigitalizacion)); const misEditados = libros.filter(l => l.editadoPor === currentUser.id && ['editado','en_cc','aceptado','rechazado','en_conversion','finalizado'].includes(l.estatus) ); // Producción de hoy (cortes) const hoyEditados = misEditados.filter(l => l.fechaEdicion === '2026-08-02'); const cortesHoy = { c11: 0, c13: 0, c16: 0, c18: 0 }; hoyEditados.forEach(l => { if (l.cortes) { cortesHoy.c11 += l.cortes.c11 || 0; cortesHoy.c13 += l.cortes.c13 || 0; cortesHoy.c16 += l.cortes.c16 || 0; cortesHoy.c18 += l.cortes.c18 || 0; } }); const totalCortesHoy = cortesHoy.c11 + cortesHoy.c13 + cortesHoy.c16 + cortesHoy.c18; // Hora "actual" simulada const horaActual = 15; // 3 PM const cortesEsperados = [11, 13, 16, 18]; const cortesCompletados = cortesEsperados.filter(h => h <= horaActual).length; const tomarLibro = (libro) => { onUpdate(libro.cons, { estatus: 'en_edicion', editadoPor: currentUser.id }); }; const abrirRegistro = (libro) => { setEditando(libro); setImagenes(libro.imagenesResultantes?.toString() || ''); setCortesForm({ c11: '', c13: '', c16: '', c18: '' }); }; const finalizarEdicion = () => { if (!editando) return; const n = parseInt(imagenes, 10); if (isNaN(n) || n <= 0) return; onUpdate(editando.cons, { estatus: 'editado', fechaEdicion: '2026-08-02', editadoPor: currentUser.id, imagenesEditadas: n, cortes: { c11: parseInt(cortesForm.c11) || 0, c13: parseInt(cortesForm.c13) || 0, c16: parseInt(cortesForm.c16) || 0, c18: parseInt(cortesForm.c18) || 0, }, }); setEditando(null); setImagenes(''); }; return (
{/* Timeline de cortes de hoy */}
Registro de productividad de hoy · Sábado 02 Agosto
Jornada L-V 9:00-18:00 · Comida 14:00-15:00 · Cortes en 11:00, 13:00, 16:00 y 18:00
Hora simulada: 15:00
{CORTES.map((c, i) => { const val = cortesHoy[c.key]; const hora = parseInt(c.hora); const isDone = hora <= horaActual; const isActive = false; // ninguno activo en este demo return (
{isDone ? '✓' : i + 1}
{c.hora}
{val}
{c.label}
); })}
Total imágenes editadas hoy
{fmtNum(totalCortesHoy)}
Ritmo por hora (estimado)
{cortesCompletados > 0 ? Math.round(totalCortesHoy / (horaActual - 9)) : 0} img/hr
Libros terminados hoy
{hoyEditados.length}
{/* Kanban personal */}
En edición ({misEnCurso.length})
Libros que estoy editando ahora
{misEnCurso.length === 0 && }
{misEnCurso.map(l => (
abrirRegistro(l)}>
#{l.cons} · {l.serie}
{l.titulo}
{fmtNum(l.imagenesResultantes)} imágenes Digitalizado {fmtDate(l.fechaDigitalizacion)}
))}
Cola FIFO disponible ({disponibles.length})
Ordenados por fecha de digitalización · Primeros en entrar, primeros en salir
{disponibles.length === 0 && }
{disponibles.slice(0, 12).map((l, i) => { const dias = daysBetween(l.fechaDigitalizacion, '2026-08-02'); return (
5 ? 'urgent' : ''}`}>{i + 1}
#{l.cons} · {l.serie}
{l.titulo}
{fmtNum(l.imagenesResultantes)} imág · esperando {dias} día{dias === 1 ? '' : 's'}
{dias > 5 && Urgente}
); })}
{/* Historial */}
Mis libros editados
Últimos {Math.min(10, misEditados.length)} · click para ver detalle
{misEditados.slice(-10).reverse().map(l => ( onSelectBook(l)} style={{ cursor: 'pointer' }}> ))}
Núm. Título Fecha edición Imágs. Cortes 11/13/16/18 Estatus
#{l.cons}
{l.titulo}
{fmtDate(l.fechaEdicion)} {fmtNum(l.imagenesEditadas)} {l.cortes ? `${l.cortes.c11}/${l.cortes.c13}/${l.cortes.c16}/${l.cortes.c18}` : '—'}
{/* Modal finalizar */} setEditando(null)} title="Finalizar edición y registrar cortes" maxWidth={520} footer={<> } > {editando && (
#{editando.cons}
{editando.titulo}
{editando.autor} · {fmtNum(editando.imagenesResultantes)} imágenes originales
setImagenes(e.target.value)} autoFocus/>
Cortes horarios (imágenes procesadas en cada tramo)
{CORTES.map(c => (
{c.hora} · {c.label}
setCortesForm({ ...cortesForm, [c.key]: e.target.value })} />
))}
)}
); }; const EDITING_CUTS = [ { key: 'cut_11', label: '11:00 · Corte AM' }, { key: 'cut_13', label: '13:00 · Pre-comida' }, { key: 'cut_16', label: '16:00 · Post-comida' }, { key: 'cut_18', label: '18:00 · Cierre' }, ]; const editingIsoDate = (value) => { if (!value) return ''; const normalized = String(value).replace(' ', 'T'); const date = new Date(normalized); return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10); }; const editingFormatDate = (value) => { const iso = editingIsoDate(value); if (!iso) return '—'; const [y,m,d] = iso.split('-'); return `${d}/${m}/${y}`; }; const EdicionServer = ({ currentUser, onSessionExpired }) => { const [state, setState] = useState({status:'loading',error:'',available:[],active:[],progress:[],completed:[],today:{date:'',total_images:0,finished_books:0}}); const [reloadToken,setReloadToken]=useState(0); const [feedback,setFeedback]=useState(null); const [claimBook,setClaimBook]=useState(null); const [claimBusy,setClaimBusy]=useState(false); const [workBook,setWorkBook]=useState(null); const [workDate,setWorkDate]=useState(new Date().toISOString().slice(0,10)); const [cuts,setCuts]=useState({cut_11:'',cut_13:'',cut_16:'',cut_18:''}); const [images,setImages]=useState([]); const [finishMode,setFinishMode]=useState(false); const [saving,setSaving]=useState(false); const [downloadBusy,setDownloadBusy]=useState({}); const [search,setSearch]=useState(''); useEffect(()=>{ let active=true; const controller=new AbortController(); window.apiRequest('editing/dashboard.php',{signal:controller.signal}).then(response=>{ if(!active)return; const data=response?.data||{}; setState({status:'loaded',error:'',available:data.available||[],active:data.active||[],progress:data.progress||[],completed:data.completed||[],today:data.today||{date:'',total_images:0,finished_books:0}}); }).catch(error=>{ if(!active)return; if(error?.status===401){onSessionExpired();return;} setState(s=>({...s,status:'error',error:error?.message||'No fue posible cargar Edición.'})); }); return()=>{active=false;controller.abort();}; },[reloadToken,onSessionExpired]); const refresh=()=>setReloadToken(v=>v+1); const filteredAvailable=state.available.filter(book=>{ const q=search.trim().toLowerCase(); if(!q)return true; return [book.inventory_number,book.catalog_number,book.series_collection,book.title,book.author].some(v=>String(v||'').toLowerCase().includes(q)); }); const downloadEditingZip=async(book)=>{ const assignmentId=Number(book?.assignment_id||0); if(!assignmentId)return; setDownloadBusy(previous=>({...previous,[assignmentId]:true})); setFeedback(null); try{ const base=window.APP_CONFIG.API_BASE_URL.replace(/\/+$/,''); const response=await fetch(`${base}/editing/download.php?assignment_id=${encodeURIComponent(assignmentId)}`,{ method:'GET', credentials:'include', headers:{Accept:'application/zip,application/json'}, cache:'no-store' }); const contentType=response.headers.get('content-type')||''; if(!response.ok||!contentType.includes('application/zip')){ let message=`No fue posible descargar el ZIP. HTTP ${response.status}`; try{ const payload=await response.json(); if(payload?.message)message=payload.message; }catch(error){} throw Object.assign(new Error(message),{status:response.status}); } const blob=await response.blob(); if(!blob||blob.size<1)throw new Error('El ZIP generado está vacío.'); const disposition=response.headers.get('content-disposition')||''; const utf8Match=disposition.match(/filename\*=UTF-8''([^;]+)/i); const simpleMatch=disposition.match(/filename="?([^";]+)"?/i); const fileName=utf8Match ? decodeURIComponent(utf8Match[1]) : simpleMatch ? simpleMatch[1] : `INPI_2026_EDICION_${new Date().toISOString().replace(/[-:T]/g,'_').slice(0,19)}_ASIGNACION_${assignmentId}.zip`; const objectUrl=URL.createObjectURL(blob); const anchor=document.createElement('a'); anchor.href=objectUrl; anchor.download=fileName; document.body.appendChild(anchor); anchor.click(); anchor.remove(); window.setTimeout(()=>URL.revokeObjectURL(objectUrl),1500); setFeedback({tone:'success',message:`El ZIP de "${book.title||'la obra'}" se descargó nuevamente.`}); }catch(error){ if(error?.status===401){onSessionExpired();return;} setFeedback({tone:'error',message:error?.message||'No fue posible descargar nuevamente el ZIP.'}); }finally{ setDownloadBusy(previous=>{ const next={...previous}; delete next[assignmentId]; return next; }); } }; const confirmClaim=async()=>{ if(!claimBook)return; setClaimBusy(true); setFeedback(null); let claimedAssignmentId=null; try{ const response=await window.apiRequest('editing/claim.php',{method:'POST',body:{book_id:claimBook.id}}); const data=response?.data||{}; claimedAssignmentId=Number(data.assignment?.id)||null; const url=`${window.APP_CONFIG.API_BASE_URL.replace(/\/+$/,'')}/${data.download_url}`; const downloadResponse=await fetch(url,{method:'GET',credentials:'include',headers:{Accept:'application/zip,application/json'}}); const contentType=downloadResponse.headers.get('content-type')||''; if(!downloadResponse.ok||!contentType.includes('application/zip')){ let message=`No fue posible descargar el ZIP. HTTP ${downloadResponse.status}`; try{ const raw=await downloadResponse.text(); const payload=JSON.parse(raw); if(payload?.message)message=payload.message; }catch(error){} throw Object.assign(new Error(message),{status:downloadResponse.status}); } const blob=await downloadResponse.blob(); if(!blob||blob.size<1)throw new Error('El ZIP generado está vacío.'); const objectUrl=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=objectUrl; a.download=data.download_filename||`INPI_2026_${new Date().toISOString().slice(0,10).split('-').reverse().join('_')}.zip`; document.body.appendChild(a); a.click(); a.remove(); window.setTimeout(()=>URL.revokeObjectURL(objectUrl),1500); setClaimBook(null); setFeedback({tone:'success',message:`Libro tomado y ZIP descargado correctamente (${data.source_file_count||0} imágenes).`}); refresh(); }catch(error){ if(claimedAssignmentId){ try{ await window.apiRequest('editing/release.php',{method:'POST',body:{assignment_id:claimedAssignmentId}}); }catch(releaseError){} } if(error?.status===401){onSessionExpired();return;} setFeedback({tone:'error',message:error?.message||'No fue posible tomar o descargar el libro. El libro se liberó de la cola de Edición.'}); refresh(); }finally{ setClaimBusy(false); } }; const openWork=(book,finish=false)=>{ setWorkBook(book);setFinishMode(finish);setWorkDate(state.today.date||new Date().toISOString().slice(0,10)); const existing=state.progress.find(p=>Number(p.assignment_id)===Number(book.assignment_id)&&p.work_date===(state.today.date||new Date().toISOString().slice(0,10))); setCuts({cut_11:existing?.cut_11||'',cut_13:existing?.cut_13||'',cut_16:existing?.cut_16||'',cut_18:existing?.cut_18||''});setImages([]); }; const saveProgress=async()=>{ if(!workBook)return;setSaving(true);setFeedback(null); try{ if(finishMode){ if(images.length===0)throw new Error('Selecciona las imágenes editadas.'); const form=new FormData(); form.append('assignment_id',workBook.assignment_id); images.forEach(file=>form.append('images[]',file)); const response=await fetch(`${window.APP_CONFIG.API_BASE_URL.replace(/\/+$/,'')}/editing/complete.php`,{method:'POST',credentials:'include',body:form}); const raw=await response.text(); let payload=null; try{payload=raw?JSON.parse(raw):null;}catch(error){} if(!response.ok||payload?.success===false)throw Object.assign(new Error(payload?.message||`HTTP ${response.status}`),{status:response.status}); setFeedback({tone:'success',message:'Edición finalizada, imágenes guardadas y libro enviado a Control de Calidad.'}); }else{ await window.apiRequest('editing/save-progress.php',{method:'POST',body:{assignment_id:workBook.assignment_id,...cuts}}); setFeedback({tone:'success',message:'Avance del día guardado correctamente.'}); } setWorkBook(null);refresh(); }catch(error){if(error?.status===401){onSessionExpired();return;}setFeedback({tone:'error',message:error?.message||'No fue posible guardar.'});} finally{setSaving(false);} }; const todayProgress=state.progress.filter(p=>p.work_date===state.today.date); const todayCuts={cut_11:0,cut_13:0,cut_16:0,cut_18:0};todayProgress.forEach(p=>Object.keys(todayCuts).forEach(k=>todayCuts[k]+=Number(p[k]||0))); if(state.status==='error')return
; return
{feedback&&
{feedback.message}
}
Historial y productividad de hoy · {editingFormatDate(state.today.date)}
Cortes diarios conservados por fecha; si un libro continúa mañana, se crea un nuevo registro
{EDITING_CUTS.map((c,i)=>
0?'done':''}`}>{todayCuts[c.key]>0?'✓':i+1}
{c.label.split(' · ')[0]}
{fmtNum(todayCuts[c.key])}
{c.label.split(' · ')[1]}
)}
Total imágenes editadas hoy
{fmtNum(state.today.total_images)}
Libros terminados hoy
{state.today.finished_books}
{state.progress.length>0&&
{state.progress.slice(0,12).map(p=>)}
FechaLibro11:0013:0016:0018:00Total
{editingFormatDate(p.work_date)}{p.title}{p.cut_11}{p.cut_13}{p.cut_16}{p.cut_18}{p.total_images}
}
En edición ({state.active.length})
Libros que tomaste y están en proceso
{state.active.length===0&&}{state.active.map(book=>
#{book.inventory_number} · {book.series_collection}
{book.title}
{fmtNum(book.resulting_images)} imágenes originalesTomado {editingFormatDate(book.assigned_at)}
)}
Cola FIFO disponible ({filteredAvailable.length})
Digitalizados y sin asignación activa de Edición
setSearch(e.target.value)}/>
{filteredAvailable.length===0&&}{filteredAvailable.map((book,i)=>
{i+1}
#{book.inventory_number} · {book.series_collection}
{book.title}
{fmtNum(book.resulting_images)} imágenes · digitalizado {editingFormatDate(book.digitized_at)}
)}
Mis libros editados
Historial real de asignaciones completadas
{state.completed.map(row=>)}
Núm.TítuloFechaImágenesDías trabajadosEstatus
#{row.inventory_number}{row.title}{editingFormatDate(row.completed_at)}{row.edited_images}{row.work_days}
!claimBusy&&setClaimBook(null)} title="Preparar descarga para edición" maxWidth={560} footer={<>}> {claimBook&&
#{claimBook.inventory_number}
{claimBook.title}
Antes de descargar
Crea esta estructura para mantener el control:
Documentos\INPI 2026\EDICION\{editingFormatDate(state.today.date||new Date().toISOString().slice(0,10)).replaceAll('/','_')}\

Al aceptar, el libro se asignará exclusivamente a tu usuario y se descargará un ZIP con la estructura Serie\Título\1.JPG\.

}
!saving&&setWorkBook(null)} title={finishMode?'Finalizar edición y subir imágenes':'Registrar avance diario'} maxWidth={620} footer={<>}> {workBook&&
#{workBook.inventory_number} · {workBook.series_collection||'Sin serie'}
{workBook.title}
{workBook.author||'Sin autor'} · {fmtNum(workBook.resulting_images)} imágenes originales
La fecha se obtiene automáticamente del servidor y no puede modificarse.
{!finishMode&&<>
Cortes horarios del día
{EDITING_CUTS.map(c=>
{c.label}
setCuts({...cuts,[c.key]:e.target.value})}/>
)}
} {finishMode&&
setImages(Array.from(e.target.files||[]))}/>
Se guardarán en INPI 2026\EDICION\Serie\Título\1.JPG\ conservando el nombre original.
{images.length} archivos seleccionados
}
}
; }; const Edicion = (props) => { const serverMode = window.APP_CONFIG.AUTH.MODE === 'server'; if (serverMode) return ; return ; }; Object.assign(window, { Edicion });