// ============ AcademIA8 · Consola — Reglas de material + Vista de publicación ============ // El editor de unidad clásico (fragmentos/checkpoints separados) fue reemplazado // por el organizador (organizer.jsx), coherente con el mapeo a pasos del backend. // Este archivo conserva las reglas de material (compartidas con el organizador) y // la vista de publicación, ahora cableada a los endpoints reales del backend // (GET /diff, GET /disclaimer) — sin diff local ni copia publicada en el front. // ---------- Reglas por tipo de material (contenido de las ayudas contextuales) ---------- const MEDIA_RULES = { video: { types:['MP4'], help:(<> Subida directa (hasta 500 MB): se envía como archivo por WhatsApp. Si pesa más de 16 MB se comprime automáticamente al subir (podés ver el progreso en el editor) — el alumno siempre recibe un MP4 dentro del límite de WhatsApp. Video largo por link: alternativa si preferís pegar un link (YouTube, Vimeo) en vez de subir el archivo. El alumno recibe el link, no el archivo. ) }, imagen: { types:['JPEG','PNG'], help:(Hasta 5 MB. Formatos: JPEG, PNG.) }, audio: { types:['AAC','MP3','OGG','Opus'], help:(Hasta 16 MB. Formatos: AAC, MP3, OGG, Opus.) }, documento: { types:['PDF','DOC','DOCX'], help:(Hasta 100 MB. Formatos: PDF, DOC, DOCX, etc.) }, }; /* =================================================================== VISTA DE PUBLICACIÓN (diff borrador vs publicado — backend Camino A) - estado derivado: solo-borrador / publicado-limpio / publicado-con-cambios - diff: unidades `editadas` y `nuevas` (no hay movida ni borrada, Camino A) - disclaimer de alcance: por unidad editada, cuántos alumnos ya la cursaron =================================================================== */ function PublishView({ course, actions }){ const [diff, setDiff] = useState(null); const [disc, setDisc] = useState({}); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [publishing, setPublishing] = useState(false); const estado = course.derivedEstado; const firstPub = estado === 'solo-borrador'; const clean = estado === 'publicado-limpio'; // mapa unidad_id → {title, pos} desde el borrador cargado const unitById = {}; (course.draft?.units || []).forEach((u,i)=>{ unitById[u.id] = { title:u.title, pos:i+1, u }; }); useEffect(()=>{ let alive = true; (async ()=>{ setLoading(true); setErr(null); try { const d = await A8Api.diff(course.id); let dc = {}; if (d.editadas && d.editadas.length){ try { dc = await A8Api.disclaimer(course.id); } catch(e){ dc = {}; } } if (alive){ setDiff(d); setDisc(dc || {}); } } catch(e){ if (alive) setErr(e.message || String(e)); } finally { if (alive) setLoading(false); } })(); return ()=>{ alive = false; }; }, [course.id]); async function doPublish(){ setPublishing(true); const ok = await actions.publish(course.id); // recarga + toast; devuelve bool setPublishing(false); if (ok) actions.openCourse(course.id); } const nuevas = diff ? diff.nuevas : []; const editadas = diff ? diff.editadas : []; const metaCambiada = diff ? !!diff.meta_cambiada : false; const totalChanges = nuevas.length + editadas.length + (metaCambiada ? 1 : 0); const affectedTotal = Object.values(disc).reduce((s,n)=>s+n, 0); return (
Vista de publicación

{course.meta_draft?.name}

actions.openCourse(course.id)}>Volver al editor
{/* hero resumen */}
{firstPub ? 'Primera publicación' : clean ? 'Sin cambios' : 'Confirmá los cambios'}

{firstPub ? 'Vas a hacer visible este curso al alumno por primera vez.' : clean ? 'El borrador ya está publicado tal cual. No hay cambios para enviar.' : <>Al publicar, el alumno pasará a ver tu borrador actual.}

{!firstPub && !clean && (
+{nuevas.length}unidades nuevas
{editadas.length}unidades editadas
)}
{/* alcance del Camino A (informativo, no bloquea) */}
Republicar no reordena ni borra unidades ya publicadas: solo se editan las existentes y se agregan al final. El progreso de cada alumno se conserva.
{loading && (

Calculando diferencias…

)} {err && (
No se pudo calcular el diff: {err}
)} {!loading && !err && ( <> {/* diff por unidad */}
{firstPub ? 'Unidades a publicar' : 'Cambios respecto de lo publicado'}
{totalChanges===0 ? (

{firstPub ? 'El borrador está vacío' : 'Sin cambios'}

{firstPub ? 'Agregá unidades y contenido antes de publicar.' : 'El borrador coincide con lo publicado.'}

) : (
{metaCambiada && (
Metadata del curso editada
Nombre, descripción y/o mensaje de bienvenida
)} {nuevas.map(e=>{ const u = unitById[e.unidad_id]; return ; })} {editadas.map(e=>{ const u = unitById[e.unidad_id]; const n = disc[e.unidad_id] || 0; const note = n>0 ? `${n} ${n===1?'alumno ya cursó':'alumnos ya cursaron'} esta unidad y no ${n===1?'verá':'verán'} esta edición` : null; return ; })}
)} {/* disclaimer de alcance */} {!firstPub && affectedTotal>0 && ( <>
Hay {affectedTotal} {affectedTotal===1?'alumno':'alumnos'} que no {affectedTotal===1?'verá':'verán'} {editadas.length===1?'este cambio':'estos cambios'} porque ya {affectedTotal===1?'pasó':'pasaron'} por {editadas.length===1?'esa unidad':'esas unidades'}.
)} {/* barra publicar */}
{firstPub?'Publicar por primera vez':clean?'Nada para publicar':`Publicar ${totalChanges} ${totalChanges===1?'cambio':'cambios'}`}
Esto reemplaza lo que ve el alumno por tu borrador actual. La acción es inmediata.
actions.openCourse(course.id)}>Cancelar {publishing ? 'Publicando…' : firstPub?'Publicar curso':'Publicar cambios'}
)}
); } function DiffRow({ title, pos, items, tone, label, note }){ const cnt = A8.countUnitContent({ items: items || [] }); const cls = tone==='add'?'is-add':tone==='edit'?'is-edit':''; const tagCls = tone==='add'?'tag-new':tone==='edit'?'tag-edit':'tag-move'; return (
#{pos} {title} {label && {label}}
{cnt.frags} cont. {cnt.chks} cuest.
{note &&
{note}
}
); } Object.assign(window, { PublishView, DiffRow, MEDIA_RULES });