3.7.0: auditoría de Vía Sumapaz: «Calcular» ya no se queda gris, banco de material por temas, proxies sin original, y «Qué pesa» por dentro
- El botón «Calcular» (y «Reanudar» la cola, y «Volver a pintar») quedaba deshabilitado para siempre tras la primera vez: al cambiar de carpeta «Qué va a pasar» no calculaba nada. - «material» suelto ya no ignora una carpeta; «No tocar» es un banco de material: los temas se quedan y, dentro, las grabaciones van por fecha y cámara, todo sin marcar. - «Hacer de una vez» y la pasada de la noche ya no mueven basura ni proxies sin marcar; un proxy sin su original va sin marcar. - «Qué pesa» entra nivel a nivel con el detalle de cada carpeta; repetidos, plan e Inicio mucho más rápidos; aviso cuando el tipo de trabajo de la carpeta no es el del cliente. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
@@ -145,6 +145,19 @@
|
||||
.tm{position:relative;height:300px;border-radius:6px;overflow:hidden;background:var(--panel2)}
|
||||
.tm div{position:absolute;overflow:hidden;padding:4px 6px;font-size:11px;border:1px solid rgba(255,255,255,.6);color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5);cursor:default}
|
||||
.tm div b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.tm div.pick{cursor:pointer}.tm div.pick:hover{filter:brightness(1.15)}
|
||||
.tm .tmvacio{position:static;color:var(--mut);text-shadow:none;border:0;padding:14px;font-size:12.5px}
|
||||
.tmrow{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(250px,1fr);gap:18px;align-items:start}
|
||||
.tmrow .tm{height:380px}
|
||||
@media(max-width:1000px){.tmrow{grid-template-columns:1fr}}
|
||||
.migas{display:flex;flex-wrap:wrap;gap:4px 6px;align-items:center;margin:0 0 8px;font-size:12.5px}
|
||||
.migas .lnk{padding:0}
|
||||
.tmdet{font-size:12.5px}
|
||||
.tmdet h4{margin:0 0 2px;font-size:14px;word-break:break-word}
|
||||
.tmdet .tit{margin:10px 0 3px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--mut)}
|
||||
.tmdet .ln{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:1px 0}
|
||||
.tmdet .trato{margin:8px 0;padding:8px 10px;border-radius:8px;background:var(--panel2)}
|
||||
.tmdet .acts{margin-top:10px;display:flex;gap:14px}
|
||||
.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
|
||||
.toolbar label{color:var(--mut);font-size:12px;display:flex;align-items:center;gap:6px}
|
||||
.toolbar .sep{flex:1}
|
||||
@@ -545,16 +558,16 @@
|
||||
<section id="t-revisar">
|
||||
<h2 class="v">Revisar y ordenar</h2>
|
||||
<p class="ayuda">Todo se mueve <b>dentro de la misma carpeta</b>, y lo único que se borra son las carpetas que se quedan vacías, si las marcas tú. <span class="q" title="Más">?</span><span class="mas">Basura y proxies van a <span class="path">_Para borrar</span>. Los nombres de archivo no cambian, así Resolve y Premiere pueden re-enlazar. Cada aplicación es un cambio que se deshace desde Historial. Las carpetas que la app no reconoce salen en el plan como «dudoso» y no se marcan: nómbralas en «Por nombrar» y se ordenan solas, en cualquier disco.</span></p>
|
||||
<div id="mapaCargando" class="hint" style="display:none"><span class="spin"></span> Leyendo el inventario de la carpeta… En una carpeta grande tarda unos segundos.</div>
|
||||
<div id="mapaVacio" class="hint">Esta carpeta todavía no está escaneada. Vuelve a <b>Carpeta</b> y pulsa <b>Escanear</b> (solo lee nombres y tamaños; no toca nada).</div>
|
||||
<div id="revisarLleno" style="display:none">
|
||||
<div class="tabs" id="tabsRev"><a data-s="hay" class="on">Qué hay</a><a data-s="plan">Qué va a pasar</a><a data-s="nombrar">Por nombrar <span class="badge" id="badgeNombrar">…</span></a><a data-s="stock">Stock viejo</a><a data-s="vacias">Carpetas vacías</a></div>
|
||||
|
||||
<div class="sub on" id="s-hay">
|
||||
<div class="cards" id="cards"></div>
|
||||
<div class="row">
|
||||
<div class="box"><h3>Qué pesa (primer nivel)</h3><div class="tm" id="treemap"></div><div class="legend" id="legend" style="margin-top:8px"></div></div>
|
||||
<div class="box"><h3>Avisos</h3><div id="alertas"></div></div>
|
||||
</div>
|
||||
<div class="box" style="margin-bottom:18px"><h3>Qué pesa · toca una carpeta para entrar en ella</h3><div class="migas" id="tmMigas"></div>
|
||||
<div class="tmrow"><div><div class="tm" id="treemap"></div><div class="legend" id="legend" style="margin-top:8px"></div></div><div class="tmdet" id="tmDetalle"></div></div></div>
|
||||
<div class="box" style="margin-bottom:18px"><h3>Avisos</h3><div id="alertas"></div></div>
|
||||
<details class="mas"><summary>Más detalle: cámaras, meses, tipos de archivo, repetidos, dónde vive cada cliente</summary>
|
||||
<div class="row" style="margin-top:12px"><div class="box"><h3>Por fuente (de qué cámara viene)</h3><div id="fuentes"></div></div><div class="box"><h3>Material de cámara por mes</h3><div id="meses"></div></div></div>
|
||||
<div class="row"><div class="box"><h3>Por tipo de archivo</h3><div id="exts"></div></div><div class="box"><h3>Repetidos (mismo nombre y peso, el mismo clip en MP4 y MOV, o con dos calidades)</h3><div id="dups"></div><div class="toolbar" style="margin-top:10px"><button id="btnComparar">Comparar y ver todos</button><button class="p" id="btnJuntar" disabled>Juntar en Duplicados</button><span class="mut" id="dupsResumen"></span></div></div></div>
|
||||
@@ -735,6 +748,8 @@
|
||||
|
||||
<h3>Revisar y ordenar</h3>
|
||||
<dl>
|
||||
<dt>Qué pesa</dt><dd>Toca cualquier bloque para entrar en esa carpeta, nivel a nivel, hasta el fondo. Al lado dice qué guarda, de qué cámaras o grabadoras, de qué fechas, cuántos proxies y basura se pueden apartar, y qué hace la app con ella. <i>Cómo: sale del inventario del escaneo, que entra en todas las carpetas; en Inicio se ve cuántas leyó y hasta qué profundidad.</i></dd>
|
||||
<dt>Banco de material</dt><dd>Una carpeta de material ordenada por temas (Lugares, Social, Ambiental…). Los temas se quedan como están; dentro de cada uno, las grabaciones van por fecha de rodaje y por cámara: «Lugares\Peajes\Drone» pasa a «Lugares\Peajes\2024-03-12\DJI». Una carpeta cuyo nombre diga «No tocar» se trata así, y todo lo que la app propone dentro va sin marcar: lo marcas tú en «Qué va a pasar», donde el banco es un paso propio. Otra carpeta se puede marcar como banco en «Por nombrar».</dd>
|
||||
<dt>Qué hay</dt><dd>Qué pesa y de qué clase es, por cámara, por mes y por tipo de archivo, con avisos de lo raro: material suelto, meses mal escritos, dos sistemas de orden a la vez.</dd>
|
||||
<dt>Por nombrar</dt><dd>Las carpetas que no sabe qué son y que no moverá hasta que se lo digas. Propone qué es cada una y tú lo confirmas. No pregunta por las carpetas de la propia app, ni por lo de dentro de las piezas, ni por lo que ya ordena solo. <i>Cómo: mira la forma (si trae sus exportados, su portada y su música es una pieza; si guarda meses dentro, un cajón) y lo que tiene dentro contando archivos, no peso. Lo confirmado se guarda en un diccionario y vale para las carpetas que se llamen exactamente igual, en cualquier disco.</i></dd>
|
||||
<dt>Qué va a pasar</dt><dd>El plan: qué haría la app en la carpeta, sin mover nada todavía. Lo explica con una carpeta de ejemplo, que se ve pasar de donde está a donde va, y debajo lista lo demás. Son tres pasos (apartar basura y proxies, ordenar por año y mes, ordenar lo demás) y un solo clic aplica todo lo marcado, en orden: no hay que ir uno por uno. Lo que no va solo queda aparte y contado: los cambios cosméticos (solo el nombre, «01 - Celular» → «01- Celular») y los dudosos. Mientras aplica, una barra dice por dónde va, archivo a archivo, y al acabar, qué quedó sin hacer. <i>Cómo: usa la plantilla de la clase de trabajo y la fecha de cada archivo, la más antigua entre creación y modificación; un render hereda la fecha de su original. Cada cambio va como seguro, probable o dudoso, y solo los dos primeros se marcan solos.</i></dd>
|
||||
@@ -851,8 +866,8 @@ const fechaCorta = ts => ts ? new Date(ts*1000).toLocaleDateString('es-CO', {day
|
||||
const hora = ts => ts ? new Date(ts*1000).toLocaleTimeString('es-CO', {hour:'2-digit', minute:'2-digit'}) : '';
|
||||
const num = n => (n || 0).toLocaleString('es-CO');
|
||||
const dur = s => { s = Math.round(s||0); return s >= 3600 ? `${Math.floor(s/3600)}h ${Math.floor(s%3600/60)}m` : `${Math.floor(s/60)}m ${s%60}s`; };
|
||||
const COLORES = {ANIO:'#4f8cff',GRABACION:'#e0574f',PIEZA:'#7c5cff',CAMPANA:'#ff8c42',EXPORTADOS:'#3ecf8e',PORTADAS:'#f5b84b',APOYOS:'#d96fd9',MARCA:'#39b8c9',PROYECTOS:'#8fa1b8',MUSICA:'#c9b339',SFX:'#c9b339',VOZ:'#c9b339',IA:'#a78bfa',STOCK:'#5aa8e0',CONTENEDOR:'#6b7280',REVISAR:'#ff6b6b',PROXY:'#555c6b',BASURA:'#3a3f4a',APARTADO:'#9aa3ad','':'#4b5563'};
|
||||
const NOMBRE_CUBETA = {ANIO:'año',GRABACION:'grabación',PIEZA:'pieza',CAMPANA:'campaña',EXPORTADOS:'exportados',PORTADAS:'portadas',APOYOS:'apoyos',MARCA:'marca',PROYECTOS:'proyectos',MUSICA:'música',SFX:'sonidos',VOZ:'voz',IA:'IA',STOCK:'stock',CONTENEDOR:'contenedor',REVISAR:'por revisar',PROXY:'proxies',BASURA:'basura',CELULAR:'celular',FOTOS:'fotos',APARTADO:'apartado por la app'};
|
||||
const COLORES = {BANCO:'#b07d4f',ANIO:'#4f8cff',GRABACION:'#e0574f',PIEZA:'#7c5cff',CAMPANA:'#ff8c42',EXPORTADOS:'#3ecf8e',PORTADAS:'#f5b84b',APOYOS:'#d96fd9',MARCA:'#39b8c9',PROYECTOS:'#8fa1b8',MUSICA:'#c9b339',SFX:'#c9b339',VOZ:'#c9b339',IA:'#a78bfa',STOCK:'#5aa8e0',CONTENEDOR:'#6b7280',REVISAR:'#ff6b6b',PROXY:'#555c6b',BASURA:'#3a3f4a',APARTADO:'#9aa3ad','':'#4b5563'};
|
||||
const NOMBRE_CUBETA = {BANCO:'banco de material (temas)',DOCUMENTOS:'documentos',GUION:'guion',IGNORAR:'se deja como está',ANIO:'año',GRABACION:'grabación',PIEZA:'pieza',CAMPANA:'campaña',EXPORTADOS:'exportados',PORTADAS:'portadas',APOYOS:'apoyos',MARCA:'marca',PROYECTOS:'proyectos',MUSICA:'música',SFX:'sonidos',VOZ:'voz',IA:'IA',STOCK:'stock',CONTENEDOR:'contenedor',REVISAR:'por revisar',PROXY:'proxies',BASURA:'basura',CELULAR:'celular',FOTOS:'fotos',APARTADO:'apartado por la app'};
|
||||
const SEGURIDAD = {alta:'seguro', media:'revisar', baja:'dudoso'};
|
||||
let ESTADO = null, MAPA = null, PLAN = null, CAND = [], VOC = [], STOCK = [], RESP = null, CARPETA = null;
|
||||
|
||||
@@ -885,15 +900,23 @@ async function accion(btn, fn){
|
||||
// se marca lo que pusimos nosotros: si la acción deja OTRA etiqueta (porque recalculó el plan, vació la
|
||||
// lista o volvió a bloquear el botón), esa manda. Antes el finally la pisaba y quedaban botones
|
||||
// habilitados con la cuenta vieja: «Aplicar todo lo marcado (23)» sobre un plan que ya no tenía 23.
|
||||
const antes = btn.innerHTML, marca = '<span class="spin"></span>' + antes;
|
||||
btn.disabled = true; btn.innerHTML = marca;
|
||||
const antes = btn.innerHTML, marca = '<span class="spin"></span>' + antes, ficha = {};
|
||||
btn.disabled = true; btn.innerHTML = marca; btn._accion = ficha;
|
||||
try { await fn(); }
|
||||
catch (e) { console.error(e); aviso(e instanceof ApiError ? e.message : 'Algo falló: ' + (e.message || e), 'bad', null, detalleDe(e)); }
|
||||
finally { if (btn.innerHTML === marca) { btn.disabled = false; btn.innerHTML = antes; } }
|
||||
finally {
|
||||
if (btn.innerHTML === marca) { btn.disabled = false; btn.innerHTML = antes; }
|
||||
// La etiqueta nueva manda, pero si nadie decidió bloquear el botón (con `bloquear`), se vuelve a poder
|
||||
// pulsar. Medido el 14-oct-2026: tras el primer cálculo «Calcular» se quedaba gris para siempre (su
|
||||
// etiqueta pasa a «Calcular de nuevo»), y al cambiar de carpeta «Qué va a pasar» ya no calculaba nada;
|
||||
// lo mismo «Reanudar» la cola después de «Detener», y «Volver a pintar».
|
||||
else if (btn._accion === ficha) btn.disabled = false;
|
||||
if (btn._accion === ficha) btn._accion = null;
|
||||
}
|
||||
}
|
||||
// qué se guarda como detalle de un error: la traza del servidor si la mandó, o la pila del navegador si fue aquí
|
||||
const detalleDe = e => (e && e.datos && e.datos.traza) ? String(e.datos.traza) : (e instanceof ApiError ? '' : String((e && e.stack) || ''));
|
||||
function bloquear(id, motivo){ const b = $('#' + id); if (!b) return; b.disabled = !!motivo; const p = $('#' + id + 'PorQue'); if (p) p.textContent = motivo && motivo !== 'x' ? motivo : ''; }
|
||||
function bloquear(id, motivo){ const b = $('#' + id); if (!b) return; b._accion = null; b.disabled = !!motivo; const p = $('#' + id + 'PorQue'); if (p) p.textContent = motivo && motivo !== 'x' ? motivo : ''; }
|
||||
const seguro = fn => async (...a) => { try { await fn(...a); } catch (e) { console.error(e); aviso(e instanceof ApiError ? e.message : 'Algo falló: ' + (e.message || e), 'bad', null, detalleDe(e)); } };
|
||||
$$('.ayuda .q').forEach(q => q.onclick = () => q.closest('.ayuda').classList.toggle('abierta'));
|
||||
|
||||
@@ -978,7 +1001,7 @@ async function sondear(){ if (SONDEO) return; SONDEO = cargarEstado().catch(e =>
|
||||
setInterval(sondear, 2500);
|
||||
$('#raiz').onchange = () => { PLAN = null; CAND = []; STOCK = []; RESP = null; MAPA = null; CARPETA = null; VOC = []; limpiarTablas(); cargarEstado().then(() => cargarVista()).catch(e => aviso(e.message, 'bad')); };
|
||||
function limpiarTablas(){
|
||||
$('#planPasos').innerHTML = ''; $('#planAvisos').innerHTML = ''; PLAN_ABIERTOS = new Set();
|
||||
$('#planPasos').innerHTML = ''; $('#planAvisos').innerHTML = ''; $('#planLista').innerHTML = ''; $('#planEjemplo').innerHTML = ''; $('#planAvance').innerHTML = ''; PLAN_ABIERTOS = new Set(); TM_RUTA = null;
|
||||
$('#planIntro').innerHTML = 'Pulsa <b>Calcular</b> para ver, paso a paso, qué haría la app en esta carpeta.'; estadoPlan();
|
||||
$('#optCards').innerHTML = ''; $('#anaEstado').textContent = ''; $('#convLista').innerHTML = '<span class="mut">Pulsa «Buscar clips» para ver qué se puede convertir en esta carpeta.</span>'; bloquear('btnEncolar', 'x'); $('#btnEncolar').textContent = 'Convertir';
|
||||
$('#tablaStock tbody').innerHTML = ''; $('#stockCaja').style.display = 'none'; $('#stockResumen').textContent = ''; bloquear('btnStockPapelera', 'Busca primero');
|
||||
@@ -1165,7 +1188,10 @@ async function cargarCarpeta(){
|
||||
$('#inicioLleno').style.display = raiz ? '' : 'none';
|
||||
if (!raiz) { pintarPortada(); return; }
|
||||
const r = cfgRaiz();
|
||||
// en una carpeta grande la primera lectura tarda: mejor decirlo que dejar la de antes, o nada
|
||||
if ($('#acciones').dataset.raiz !== raiz) { $('#acciones').dataset.raiz = ''; $('#siguiente').innerHTML = ''; $('#acciones').innerHTML = '<div class="cabsec">Leyendo la carpeta</div><div class="mut"><span class="spin"></span> En una carpeta grande tarda unos segundos.</div>'; }
|
||||
CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raiz));
|
||||
if (raizActual() !== raiz) return;
|
||||
$('#cabTitulo').textContent = r.cliente;
|
||||
const e = CARPETA.escaneo;
|
||||
$('#cabSub').innerHTML = `<span class="path">${esc(r.ruta)}</span> · ${esc(ESTADO.tipos[r.tipo] || r.tipo)}${e ? ` · ${num(e.n)} archivos · ${gb(e.bytes)} · escaneada ${fecha(e.ts)}` : ' · sin escanear'}`;
|
||||
@@ -1177,6 +1203,10 @@ async function cargarCarpeta(){
|
||||
$('#pesoBarra').innerHTML = d.grupos.map(g => `<i style="width:${Math.max(g.pct, 0.4)}%;background:${COLOR[g.clave] || '#ccc'}" title="${esc(g.titulo)} · ${gb(g.bytes)} · ${num(g.n)} archivos"></i>`).join('');
|
||||
$('#pesoLinea').innerHTML = d.grupos.slice(0, 5).map(g => `<b>${gb(g.bytes)}</b> ${esc(NOMBRE_CORTO[g.clave] || g.titulo)}`).join(' · ');
|
||||
$('#inicioDiag').innerHTML = d.frases.slice(0, 3).map(f => `<div>${esc(f)}</div>`).join('') + (d.frases.length > 3 ? `<div class="mut">Más en Revisar y ordenar.</div>` : '');
|
||||
const inv = CARPETA.inventario;
|
||||
// que se vea que el escaneo entró en todas: cuántas carpetas y hasta qué profundidad
|
||||
if (inv && e) $('#inicioDiag').insertAdjacentHTML('afterbegin', `<div class="mut" style="margin-bottom:4px">Escaneo completo: ${num(e.n)} archivos en ${num(inv.carpetas)} carpetas, hasta ${inv.niveles} nivel${inv.niveles !== 1 ? 'es' : ''} de profundidad. <button class="lnk" id="verDentro" style="padding:0">Ver qué hay dentro</button></div>`);
|
||||
const vd = $('#verDentro'); if (vd) vd.onclick = () => { irA('revisar'); subRev('hay'); };
|
||||
}
|
||||
const lotes = CARPETA.lotes || [];
|
||||
$('#inicioLotes').innerHTML = lotes.length ? lotes.map(l => `<div class="ev"><span class="h">${esc(fechaCorta(l.ts))}</span><span class="q"><b>${esc(queSeHizo(l.lote))}</b> · ${cuentaLote(l, true)}${l.errores ? ` <span class="bad">· ${l.errores} avisos</span>` : ''}</span>${l.lote && (l.lote.startsWith('optimizar') || l.lote.startsWith('entrada-') || l.lote.startsWith('tanda-')) ? '' : `<button data-l="${esc(l.lote)}">Deshacer</button>`}</div>`).join('') : '<span class="mut">Todavía no se ha movido nada en esta carpeta.</span>';
|
||||
@@ -1186,7 +1216,19 @@ async function cargarCarpeta(){
|
||||
// Una sola lista, en orden, con la cifra de cada cosa y un botón. Sustituye a repartir el trabajo
|
||||
// entre pestañas dentro de pestañas: el usuario ve de un vistazo qué falta y cuánto pesa.
|
||||
const IR_ACCION = {
|
||||
plan: p => { irA('revisar'); if (PLAN && p) { PLAN_PASO = p; PLAN_EJ = 0; pintarPlan(); } subRev('plan'); if (!PLAN) $('#btnPlan').click(); },
|
||||
plan: p => { if (p) PLAN_PASO = p; irA('revisar'); if (PLAN && p) { PLAN_EJ = 0; pintarPlan(); } subRev('plan'); if (!PLAN) $('#btnPlan').click(); },
|
||||
tipo: (_, a) => {
|
||||
const [actual, delCliente] = a.tipos || [];
|
||||
modal('Cómo se ordena esta carpeta', `<p>${esc(a.que)}</p><div class="tipos" id="mTipo">${tarjetasTipo(delCliente || actual)}</div><p class="mut" style="font-size:12.5px">El plan se vuelve a calcular con el que elijas. No se mueve nada hasta que apliques.</p>`,
|
||||
seguro(async () => {
|
||||
const t = (document.querySelector('#mTipo input:checked') || {}).value; if (!t) return;
|
||||
await api('/api/raiz/tipo', {raiz: raizActual(), tipo: t});
|
||||
PLAN = null; MAPAE = null;
|
||||
aviso(`Listo: la carpeta y el cliente se ordenan como «${(ESTADO.tipos[t] || t).split(' · ')[0]}».`, 'ok');
|
||||
await cargarEstado(); cargarVista();
|
||||
}));
|
||||
$('#modalSi').textContent = 'Usar este';
|
||||
},
|
||||
nombrar: () => { irA('revisar'); subRev('nombrar'); },
|
||||
stock: () => { irA('revisar'); subRev('stock'); if (!STOCK.length) $('#btnStock').click(); },
|
||||
vacias: () => { irA('revisar'); subRev('vacias'); if (!VACIAS || VACIAS.raiz !== raizActual()) $('#btnVacias').click(); },
|
||||
@@ -1210,6 +1252,7 @@ async function pintarAcciones(){
|
||||
const d = await api('/api/carpeta/acciones?raiz=' + encodeURIComponent(raiz));
|
||||
if (raizActual() !== raiz) return; // cambió de carpeta mientras llegaba
|
||||
ACCIONES = d.acciones || [];
|
||||
$('#acciones').dataset.raiz = raiz; // de qué carpeta es lo que se ve: así se sabe cuándo decir «leyendo»
|
||||
const quedan = ACCIONES.filter(a => a.estado === 'toca').length;
|
||||
const auto = d.automatico || [], asis = d.asistente;
|
||||
const corriendo = asis && asis.activo;
|
||||
@@ -1218,7 +1261,7 @@ async function pintarAcciones(){
|
||||
: `<div class="cabsec">${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día' + (d.ultimo ? ` · último cambio ${esc(fechaCorta(d.ultimo.ts))}` : '')}${auto.length > 1 ? '<span class="sep"></span><button class="p" id="btnHacerTodo" style="text-transform:none;letter-spacing:0">Hacer las ' + auto.length + ' de una vez</button>' : ''}</div>`;
|
||||
$('#acciones').innerHTML = cab +
|
||||
ACCIONES.map((a, i) => `<div class="ac ${a.estado}"><span class="ico">${a.calculando ? '<span class="spin"></span>' : a.estado === 'hecho' ? '✓' : a.estado === 'espera' ? '·' : i + 1}</span><div class="tx"><b>${esc(a.titulo)}</b><div class="q">${esc(a.que)}</div>${a.detalle ? `<div class="d">${esc(a.detalle)}</div>` : ''}</div><div class="der">${a.cifra ? `<span class="cifra">${esc(a.cifra)}</span>` : ''}${a.boton ? `<button class="${a.estado === 'espera' ? 'lnk' : 'p'}" data-a="${i}">${esc(a.boton)}</button>` : ''}</div></div>`).join('');
|
||||
$$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso); });
|
||||
$$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso, a); });
|
||||
// se acaba de mover algo y el programa está comprobando qué queda: se vuelve a preguntar en un momento
|
||||
if (d.calculando) setTimeout(() => { if (raizActual() === raiz && vistaActual() === 'carpeta') cargarCarpeta().catch(() => {}); }, 1500);
|
||||
const bt = $('#btnHacerTodo');
|
||||
@@ -1316,7 +1359,12 @@ async function deshacer(lote){
|
||||
// ---------------- revisar: qué hay ----------------
|
||||
async function cargarMapa(){
|
||||
if (!raizActual()) { $('#mapaVacio').style.display = 'block'; $('#revisarLleno').style.display = 'none'; return; }
|
||||
MAPA = await api('/api/mapa?raiz=' + encodeURIComponent(raizActual()));
|
||||
const raiz = raizActual();
|
||||
if (!MAPA || MAPA.raiz !== raiz) { $('#mapaVacio').style.display = 'none'; $('#revisarLleno').style.display = 'none'; $('#mapaCargando').style.display = 'block'; }
|
||||
let m0;
|
||||
try { m0 = await api('/api/mapa?raiz=' + encodeURIComponent(raiz)); } finally { $('#mapaCargando').style.display = 'none'; }
|
||||
if (raizActual() !== raiz) return;
|
||||
MAPA = m0;
|
||||
$('#mapaVacio').style.display = MAPA.escaneado ? 'none' : 'block';
|
||||
$('#revisarLleno').style.display = MAPA.escaneado ? 'block' : 'none';
|
||||
cargarDiscos().catch(() => {});
|
||||
@@ -1329,9 +1377,7 @@ async function cargarMapa(){
|
||||
[gb(m.duplicados.bytes), `repetidos (${m.duplicados.grupos} grupos)`], [gb(m.proxies.bytes), `proxies (${m.proxies.n})`],
|
||||
[num(m.basura.n), `archivos basura · ${gb(m.basura.bytes)}`], [m.sueltos_raiz.n, `sueltos en la raíz · ${gb(m.sueltos_raiz.bytes)}`],
|
||||
].map(([b,s]) => `<div class="card"><b>${b}</b><small>${s}</small></div>`).join('');
|
||||
treemap($('#treemap'), m.arbol.map(h => ({nombre:h.nombre, bytes:h.bytes, n:h.n, color: COLORES[h.cubeta] || COLORES[''], hijos:h.hijos, cubeta:h.cubeta})));
|
||||
const usados = [...new Set(m.arbol.map(h => h.cubeta || ''))];
|
||||
$('#legend').innerHTML = usados.map(c => `<span><i style="background:${COLORES[c]||COLORES['']}"></i>${NOMBRE_CUBETA[c] || c || 'no reconocida'}</span>`).join('');
|
||||
verCarpetaTm(TM_RUTA);
|
||||
const al = [];
|
||||
if (m.basura.n) al.push(`<li><b>${num(m.basura.n)} archivos basura</b> (${gb(m.basura.bytes)}): .DS_Store, Thumbs.db, .tmp, autoguardados de Adobe. Al ordenar van a _Para borrar.</li>`);
|
||||
if (m.proxies.n) al.push(`<li><b>${gb(m.proxies.bytes)} en proxies</b> (${m.proxies.n}). Se regeneran desde Resolve/Premiere: sobran en el disco.</li>`);
|
||||
@@ -1366,13 +1412,47 @@ $('#btnJuntar').onclick = () => { if (!DUPS || DUPS.raiz !== raizActual()) retur
|
||||
await cargarEstado(); cargarVista();
|
||||
})); $('#modalSi').textContent = 'Juntar'; };
|
||||
|
||||
function treemap(el, items){
|
||||
// «Qué pesa», nivel a nivel: se entra en cualquier carpeta y al lado dice qué guarda, de qué cámaras, de
|
||||
// qué fechas y qué hace la app con ella. Antes solo había el primer nivel, y en Vía Sumapaz una carpeta de
|
||||
// 3,6 TB era un bloque gris sin nada dentro: parecía que el escaneo no había entrado.
|
||||
let TM_RUTA = null;
|
||||
const mesCorto = ym => { const [a, m] = (ym || '').split('-'); return m ? `${MESES_UI[+m - 1].slice(0, 3).toLowerCase()} ${a}` : ''; };
|
||||
const verCarpetaTm = seguro(async ruta => {
|
||||
const raiz = raizActual(); if (!raiz) return;
|
||||
const d = await api(`/api/mapa/carpeta?raiz=${encodeURIComponent(raiz)}${ruta ? '&ruta=' + encodeURIComponent(ruta) : ''}`);
|
||||
if (raizActual() !== raiz) return;
|
||||
TM_RUTA = d.rel ? d.ruta : null;
|
||||
const items = d.hijos.map(h => ({nombre: h.nombre, bytes: h.bytes, n: h.n, color: COLORES[h.cubeta] || h.color || COLORES[''], cubeta: h.cubeta, ruta: h.ruta}));
|
||||
if (d.sueltos.n && d.hijos.length) items.push({nombre: 'archivos sueltos aquí', bytes: d.sueltos.bytes, n: d.sueltos.n, color: '#9aa3ad', cubeta: ''});
|
||||
treemap($('#treemap'), items, r => verCarpetaTm(r));
|
||||
if (!d.hijos.length) $('#treemap').innerHTML = `<div class="tmvacio">Aquí ya no hay más carpetas: ${num(d.n)} archivo${d.n !== 1 ? 's' : ''} sueltos, ${gb(d.bytes)}.</div>`;
|
||||
const usados = [...new Set(items.map(i => i.cubeta || ''))];
|
||||
$('#legend').innerHTML = usados.map(c => `<span><i style="background:${COLORES[c] || COLORES['']}"></i>${NOMBRE_CUBETA[c] || c || 'no reconocida'}</span>`).join('');
|
||||
let acum = d.raiz.replace(/[\\/]+$/, '');
|
||||
const segs = d.rel ? d.rel.split(/[\\/]/) : [];
|
||||
$('#tmMigas').innerHTML = [`<button class="lnk" data-r="">${esc((cfgRaiz() || {}).cliente || d.raiz)}</button>`]
|
||||
.concat(segs.map(s => { acum += '\\' + s; return `<button class="lnk" data-r="${esc(acum)}">${esc(s)}</button>`; })).join('<span class="mut">›</span>');
|
||||
$$('#tmMigas [data-r]').forEach(b => b.onclick = () => verCarpetaTm(b.dataset.r || null));
|
||||
const fila = (a, b, c) => `<div class="ln"><span>${esc(a)}</span><b>${b}</b><span class="mut">${c}</span></div>`;
|
||||
$('#tmDetalle').innerHTML = `<h4>${esc(d.rel ? d.nombre : ((cfgRaiz() || {}).cliente || d.nombre))}</h4>
|
||||
<div><b>${gb(d.bytes)}</b> · ${num(d.n)} archivo${d.n !== 1 ? 's' : ''}${d.carpetas ? ` · ${num(d.carpetas)} carpetas dentro, hasta ${d.niveles} nivel${d.niveles !== 1 ? 'es' : ''} más abajo` : ''}</div>
|
||||
${d.trato.texto ? `<p class="trato">${esc(d.trato.texto)}</p>` : ''}
|
||||
${d.en_plan ? `<div class="mut">En el plan: ${d.en_plan.filas ? `${num(d.en_plan.filas)} cambio${d.en_plan.filas !== 1 ? 's' : ''} (${gb(d.en_plan.bytes)}), ${num(d.en_plan.marcadas)} marcado${d.en_plan.marcadas !== 1 ? 's' : ''}` : 'nada que cambiar'}.</div>` : ''}
|
||||
<div class="tit">Qué guarda</div>${d.roles.slice(0, 6).map(r => fila(r.que, gb(r.bytes), num(r.n))).join('') || '<div class="mut">Nada.</div>'}
|
||||
${d.fuentes.length ? '<div class="tit">De qué cámara o grabadora</div>' + d.fuentes.slice(0, 6).map(f => fila(f.que, gb(f.bytes), num(f.n))).join('') : ''}
|
||||
${d.fechas.desde ? `<div class="tit">Cuándo se grabó</div><div>De ${mesCorto(d.fechas.desde)} a ${mesCorto(d.fechas.hasta)} · ${d.fechas.meses} mes${d.fechas.meses !== 1 ? 'es' : ''} distintos</div>` : ''}
|
||||
${d.apartable.proxies || d.apartable.basura ? `<div class="mut" style="margin-top:6px">Se puede apartar: ${gb(d.apartable.proxies)} en proxies y ${gb(d.apartable.basura)} de basura.</div>` : ''}
|
||||
<div class="acts"><button class="lnk" id="tmAbrir">Abrir en el Explorador</button>${d.rel ? '<button class="lnk" id="tmSubir">Subir un nivel</button>' : ''}</div>`;
|
||||
$('#tmAbrir').onclick = seguro(async () => { await api('/api/abrir', {ruta: d.ruta}); });
|
||||
const sb = $('#tmSubir'); if (sb) sb.onclick = () => { const p = d.ruta.replace(/[\\/][^\\/]+$/, ''); verCarpetaTm(p.toLowerCase() === d.raiz.toLowerCase() ? null : p); };
|
||||
});
|
||||
function treemap(el, items, alPulsar){
|
||||
el.innerHTML = ''; const W = el.clientWidth || 600, H = el.clientHeight || 300;
|
||||
const total = items.reduce((s,i) => s + i.bytes, 0) || 1;
|
||||
const nodos = items.filter(i => i.bytes > 0).sort((a,b) => b.bytes - a.bytes).map(i => ({...i, area: i.bytes/total*W*H}));
|
||||
let x = 0, y = 0, w = W, h = H, fila = [], resto = nodos.slice();
|
||||
const peor = (fila, lado) => { const s = fila.reduce((a,b)=>a+b.area,0); const mx = Math.max(...fila.map(f=>f.area)), mn = Math.min(...fila.map(f=>f.area)); return Math.max(lado*lado*mx/(s*s), s*s/(lado*lado*mn)); };
|
||||
const pinta = (fila, lado, horizontal) => { const s = fila.reduce((a,b)=>a+b.area,0); const grosor = s/lado; let off = 0; fila.forEach(f => { const largo = f.area/grosor; const d = document.createElement('div'); const rx = horizontal ? x+off : x, ry = horizontal ? y : y+off, rw = horizontal ? largo : grosor, rh = horizontal ? grosor : largo; d.style.left = rx+'px'; d.style.top = ry+'px'; d.style.width = rw+'px'; d.style.height = rh+'px'; d.style.background = f.color; d.title = `${f.nombre}\n${gb(f.bytes)} · ${f.n} archivos${f.cubeta ? '\n' + (NOMBRE_CUBETA[f.cubeta] || f.cubeta) : ''}` + (f.hijos && f.hijos.length ? '\n\n' + f.hijos.slice(0,12).map(h => ` ${h.nombre}: ${gb(h.bytes)}`).join('\n') : ''); d.innerHTML = rw > 40 && rh > 18 ? `<b>${esc(f.nombre)}</b>${rh > 34 ? `<span>${gb(f.bytes)}</span>` : ''}` : ''; el.appendChild(d); off += largo; }); if (horizontal) { y += grosor; h -= grosor; } else { x += grosor; w -= grosor; } };
|
||||
const pinta = (fila, lado, horizontal) => { const s = fila.reduce((a,b)=>a+b.area,0); const grosor = s/lado; let off = 0; fila.forEach(f => { const largo = f.area/grosor; const d = document.createElement('div'); const rx = horizontal ? x+off : x, ry = horizontal ? y : y+off, rw = horizontal ? largo : grosor, rh = horizontal ? grosor : largo; d.style.left = rx+'px'; d.style.top = ry+'px'; d.style.width = rw+'px'; d.style.height = rh+'px'; d.style.background = f.color; d.title = `${f.nombre}\n${gb(f.bytes)} · ${f.n} archivos${f.cubeta ? '\n' + (NOMBRE_CUBETA[f.cubeta] || f.cubeta) : ''}` + (f.hijos && f.hijos.length ? '\n\n' + f.hijos.slice(0,12).map(h => ` ${h.nombre}: ${gb(h.bytes)}`).join('\n') : ''); d.innerHTML = rw > 40 && rh > 18 ? `<b>${esc(f.nombre)}</b>${rh > 34 ? `<span>${gb(f.bytes)}</span>` : ''}` : ''; if (f.ruta && alPulsar) { d.classList.add('pick'); d.onclick = () => alPulsar(f.ruta); } el.appendChild(d); off += largo; }); if (horizontal) { y += grosor; h -= grosor; } else { x += grosor; w -= grosor; } };
|
||||
while (resto.length) { const horizontal = w >= h; const lado = horizontal ? w : h; const n = resto.shift(); if (!fila.length || peor(fila.concat(n), lado) <= peor(fila, lado)) fila.push(n); else { pinta(fila, lado, !horizontal); fila = [n]; } }
|
||||
if (fila.length) pinta(fila, w >= h ? w : h, !(w >= h));
|
||||
}
|
||||
@@ -1429,7 +1509,10 @@ const PASOS_PLAN = [
|
||||
{k: 'limpiar', t: 'Apartar basura y proxies', q: 'Archivos de sistema y proxies van a «_Para borrar». Nada se borra.', vacio: 'No hay basura ni proxies que apartar.'},
|
||||
{k: 'fechas', t: 'Ordenar por año y mes', q: 'Grabaciones y piezas, en la carpeta del mes en que se grabaron.', vacio: 'Las fechas ya están en orden.'},
|
||||
{k: 'resto', t: 'Ordenar lo demás', q: 'Exportados, proyectos, música y marca, cada cosa a su carpeta.', vacio: 'Nada más que ordenar.'},
|
||||
{k: 'banco', t: 'Banco de material', q: 'Los temas se quedan; dentro de cada uno, las grabaciones por fecha de rodaje y cámara.', vacio: 'Las grabaciones del banco ya están por fecha y cámara.'},
|
||||
];
|
||||
// el banco de material solo sale si la carpeta tiene uno
|
||||
const pasosVisibles = () => PASOS_PLAN.filter(p => p.k !== 'banco' || filasPaso('banco').length);
|
||||
const SEG_TXT = {alta: 'seguro', media: 'probable', baja: 'dudoso'};
|
||||
let PLAN_PASO = null, PLAN_EJ = 0, PLAN_VER_OPC = false, PLAN_POR_QUE = false, PLAN_AVISOS = false, ACCIONES = [];
|
||||
const pasoDe = r => r.paso || 'resto';
|
||||
@@ -1450,7 +1533,10 @@ function estadoPlan(){
|
||||
}
|
||||
async function calcularPlan(){
|
||||
$('#planIntro').innerHTML = '<span class="spin"></span> mirando la carpeta…';
|
||||
PLAN = await api('/api/plan?raiz=' + encodeURIComponent(raizActual()));
|
||||
const raiz = raizActual();
|
||||
const p = await api('/api/plan?raiz=' + encodeURIComponent(raiz));
|
||||
if (raizActual() !== raiz) return; // se cambió de carpeta mientras calculaba
|
||||
PLAN = p;
|
||||
PLAN_EJ = 0; PLAN_VER_OPC = false;
|
||||
pintarPlan();
|
||||
}
|
||||
@@ -1472,8 +1558,9 @@ function pasoPorDefecto(){
|
||||
}
|
||||
// «43 cosméticos y 5 dudosos»: por qué hay cambios sin marcar
|
||||
function resumenOpc(rs){
|
||||
const cos = rs.filter(r => r.cosmetico).length, dud = rs.filter(r => !r.cosmetico && r.confianza === 'baja').length, otros = rs.length - cos - dud;
|
||||
return [cos ? `${cos} cosmético${cos > 1 ? 's' : ''}` : '', dud ? `${dud} dudoso${dud > 1 ? 's' : ''}` : '', otros ? `${otros} que desmarcaste` : ''].filter(Boolean).join(', ').replace(/, ([^,]*)$/, ' y $1');
|
||||
const nt = rs.filter(r => r.no_tocar).length, cos = rs.filter(r => r.cosmetico && !r.no_tocar).length,
|
||||
dud = rs.filter(r => !r.cosmetico && !r.no_tocar && r.confianza === 'baja').length, otros = rs.length - nt - cos - dud;
|
||||
return [nt ? `${nt} dentro de «No tocar»` : '', cos ? `${cos} cosmético${cos > 1 ? 's' : ''}` : '', dud ? `${dud} dudoso${dud > 1 ? 's' : ''}` : '', otros ? `${otros} que desmarcaste` : ''].filter(Boolean).join(', ').replace(/, ([^,]*)$/, ' y $1');
|
||||
}
|
||||
function pintarPlan(){
|
||||
if (!PLAN) { estadoPlan(); return; }
|
||||
@@ -1492,7 +1579,7 @@ function pintarCabPlan(){
|
||||
$('#planIntro').innerHTML = !tot ? `<b class="grande">La carpeta ya está en orden.</b> No hay nada que apartar ni mover.`
|
||||
: !marc.length ? `<b class="grande">Lo seguro ya está hecho.</b> Quedan ${sin.length} cambio${sin.length > 1 ? 's' : ''} que la app no hace sola (${resumenOpc(sin)}). Están abajo, por si quieres marcarlos.`
|
||||
: `<b class="grande">La app va a hacer ${marc.length} cambio${marc.length > 1 ? 's' : ''}</b> (${gb(marc.reduce((a, r) => a + r.bytes, 0))}) dentro de <span class="path">${esc(PLAN.raiz)}</span>. Nada se borra, y se deshace desde Historial.${sin.length ? ` Otros ${sin.length} quedan sin marcar (${resumenOpc(sin)}): están abajo, por si los quieres.` : ''}`;
|
||||
$('#planPasos').innerHTML = PASOS_PLAN.map((p, i) => {
|
||||
$('#planPasos').innerHTML = pasosVisibles().map((p, i) => {
|
||||
const rs = filasPaso(p.k), m = rs.filter(r => r.marcado), vacio = !rs.length, on = PLAN_PASO === p.k;
|
||||
return `<div class="ficha ${vacio ? 'vacio' : ''} ${on ? 'on' : ''}" data-paso="${p.k}" ${vacio ? '' : `tabindex="0" role="button" aria-pressed="${on}"`}><span class="num">${vacio ? '✓' : i + 1}</span><div class="tx"><b>${p.t}</b><div class="q">${vacio ? p.vacio : p.q}</div>${vacio ? '' : `<div class="cifra">${m.length ? `<b>${m.length}</b> cambio${m.length > 1 ? 's' : ''} · ${gb(m.reduce((a, r) => a + r.bytes, 0))}` : '<b>0</b> marcados'}${rs.length > m.length ? ` <span class="mut">· ${rs.length - m.length} sin marcar</span>` : ''}</div>`}</div>${vacio || !m.length ? '' : `<button class="lnk" data-aplicar="${p.k}" title="Aplicar solo lo marcado de este paso">solo este paso</button>`}</div>`;
|
||||
}).join('');
|
||||
@@ -1563,12 +1650,13 @@ function pintarLista(){
|
||||
const peso = {alta: 0, media: 1, baja: 2}, orden = (a, b) => (peso[a.confianza] - peso[b.confianza]) || b.bytes - a.bytes;
|
||||
const todas = filasPaso(PLAN_PASO).filter(pasa).sort(orden), marc = todas.filter(r => r.marcado), sin = todas.filter(r => !r.marcado), ej = ejemploActual();
|
||||
const grupos = rs => { const g = {}; rs.forEach(r => (g[r.cubeta] = g[r.cubeta] || []).push(r)); return Object.keys(g).map(c => `<div class="grp">${esc(NOMBRE_CUBETA[c] || c)} · ${g[c].length}</div>` + g[c].map(r => filaHtml(r, ej)).join('')).join(''); };
|
||||
const cos = sin.filter(r => r.cosmetico), dud = sin.filter(r => !r.cosmetico);
|
||||
const explica = [cos.length ? `${cos.length === 1 ? 'uno solo cambia' : cos.length + ' solo cambian'} el nombre a la forma «NN- Título» (como «01 - Celular» → «01- Celular»): da igual hacerlo o no` : '', dud.length ? `${dud.length} ${dud.length === 1 ? 'es dudoso' : 'son dudosos'}: la app no está segura y no ${dud.length === 1 ? 'lo' : 'los'} mueve sin que lo marques tú` : ''].filter(Boolean).join('; ');
|
||||
caja.innerHTML = `<div class="lista ${PLAN_POR_QUE ? 'porque' : ''}"><div class="lcab"><b>${marc.length ? `Lo que va a hacer este paso · ${marc.length}` : f ? 'Nada marcado con ese filtro' : 'Este paso no tiene nada marcado'}</b><span class="sep"></span><label class="mut" style="font-size:12.5px"><input type="checkbox" id="chkPorQue" ${PLAN_POR_QUE ? 'checked' : ''}> ver por qué</label></div>${grupos(marc)}${sin.length ? `<div class="opc"><div class="lcab"><div><b>Sin marcar · ${sin.length}</b> <span class="mut">${explica}.</span></div><span class="sep"></span>${cos.length ? `<button class="lnk" id="btnMarcarCosm">marcar los cosméticos</button>` : ''}<button class="lnk" id="btnVerOpc">${PLAN_VER_OPC ? 'ocultar la lista' : 'ver la lista'}</button></div>${PLAN_VER_OPC ? grupos(sin) : ''}</div>` : ''}</div>`;
|
||||
const nt = sin.filter(r => r.no_tocar), cos = sin.filter(r => r.cosmetico && !r.no_tocar), dud = sin.filter(r => !r.cosmetico && !r.no_tocar);
|
||||
const explica = [nt.length ? `${nt.length === 1 ? 'uno está' : nt.length + ' están'} dentro de una carpeta que dice «No tocar»: no se mueve${nt.length === 1 ? '' : 'n'} hasta que ${nt.length === 1 ? 'lo' : 'los'} marques tú` : '', cos.length ? `${cos.length === 1 ? 'uno solo cambia' : cos.length + ' solo cambian'} el nombre a la forma «NN- Título» (como «01 - Celular» → «01- Celular»): da igual hacerlo o no` : '', dud.length ? `${dud.length} ${dud.length === 1 ? 'es dudoso' : 'son dudosos'}: la app no está segura y no ${dud.length === 1 ? 'lo' : 'los'} mueve sin que lo marques tú` : ''].filter(Boolean).join('; ');
|
||||
caja.innerHTML = `<div class="lista ${PLAN_POR_QUE ? 'porque' : ''}"><div class="lcab"><b>${marc.length ? `Lo que va a hacer este paso · ${marc.length}` : f ? 'Nada marcado con ese filtro' : 'Este paso no tiene nada marcado'}</b><span class="sep"></span><label class="mut" style="font-size:12.5px"><input type="checkbox" id="chkPorQue" ${PLAN_POR_QUE ? 'checked' : ''}> ver por qué</label></div>${grupos(marc)}${sin.length ? `<div class="opc"><div class="lcab"><div><b>Sin marcar · ${sin.length}</b> <span class="mut">${explica}.</span></div><span class="sep"></span>${cos.length ? `<button class="lnk" id="btnMarcarCosm">marcar los cosméticos</button>` : ''}${PLAN_PASO === 'banco' && sin.length > 1 ? `<button class="lnk" id="btnMarcarBanco">marcar los ${sin.length}</button>` : ''}<button class="lnk" id="btnVerOpc">${PLAN_VER_OPC ? 'ocultar la lista' : 'ver la lista'}</button></div>${PLAN_VER_OPC ? grupos(sin) : ''}</div>` : ''}</div>`;
|
||||
$('#chkPorQue').onchange = e => { PLAN_POR_QUE = e.target.checked; caja.querySelector('.lista').classList.toggle('porque', PLAN_POR_QUE); };
|
||||
const bo = $('#btnVerOpc'); if (bo) bo.onclick = () => { PLAN_VER_OPC = !PLAN_VER_OPC; pintarLista(); };
|
||||
const bc = $('#btnMarcarCosm'); if (bc) bc.onclick = () => { cos.forEach(r => r.marcado = true); pintarPlan(); };
|
||||
const bb = $('#btnMarcarBanco'); if (bb) bb.onclick = () => { sin.forEach(r => r.marcado = true); pintarPlan(); };
|
||||
// marcar o desmarcar una fila cambia las cifras, no la lista: así el foco se queda donde está
|
||||
$$('#planLista .fila').forEach(el => { const r = PLAN.filas.find(x => x.id === +el.dataset.id); el.querySelector('input[type=checkbox]').onchange = e => { r.marcado = e.target.checked; el.classList.toggle('sinmarcar', !r.marcado); pintarCabPlan(); estadoPlan(); }; });
|
||||
$$('#planLista [data-ej]').forEach(b => b.onclick = () => { const i = ejemplosDe(PLAN_PASO).findIndex(x => x.id === +b.dataset.ej); if (i >= 0) { PLAN_EJ = i; pintarEjemplo(); pintarLista(); $('#ejCaja').scrollIntoView({behavior: 'smooth', block: 'nearest'}); } });
|
||||
|
||||
Referencia en una nueva incidencia
Block a user