Pendientes: elegir uno de tus comentarios como el «¿por qué?» al mover
Pedido de Leo (28-oct, 18:00): Michelle comenta antes de mover y luego tenía que escribir lo mismo otra vez. El «¿por qué?» lista tus últimos comentarios del pendiente; si comentaste después del último cambio de columna, ya viene elegido. PATCH /api/tarea acepta _nota_de (el «cuando» de un comentario tuyo; 400 si no existe o es de otra persona): la entrada guarda «ref», no repite el texto en el hilo y lleva un extracto para la Actividad. En el hilo, el comentario elegido lleva «→ Hecho»; un porqué pegado igual que un comentario anterior también se ve una sola vez. «Comentar» revisa el texto al hacer clic. Pruebas: tests-comentarios.sh (26) y las demás; caso de Michelle en el navegador. DESPLIEGUE A14; lote 2026-10-28_comentarios-elegir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SfRSvDd335NPYEbBrDFGE
This commit is contained in:
parent
62c037f198
commit
e86b0a59fb
@ -597,6 +597,22 @@ sobre todo cuando algo pasó a Hecho sin estar terminado.
|
|||||||
Desplegado el 28-oct: páginas con rsync después de la comparación con el servidor, backend con `kill -HUP` al maestro de
|
Desplegado el 28-oct: páginas con rsync después de la comparación con el servidor, backend con `kill -HUP` al maestro de
|
||||||
gunicorn. Para deshacer: `git revert` del commit, `./scripts/deploy.sh` y `ssh -t leo@git.helgen.online 'sudo systemctl restart actas-hbox'`.
|
gunicorn. Para deshacer: `git revert` del commit, `./scripts/deploy.sh` y `ssh -t leo@git.helgen.online 'sudo systemctl restart actas-hbox'`.
|
||||||
|
|
||||||
|
## A14 · 28-oct: elegir uno de tus comentarios como el porqué
|
||||||
|
|
||||||
|
Lo pidió Leo (28-oct, 18:00): Michelle comenta antes de mover y luego el «¿por qué?» la hacía escribir lo mismo otra vez.
|
||||||
|
|
||||||
|
- `backend/app.py` (PATCH `/api/tarea`): `_nota_de` = el `cuando` de un comentario **de la misma persona** en ese
|
||||||
|
pendiente (si no existe o es de otra persona: 400). La entrada del movimiento guarda `ref` (sin `nota`, así no se
|
||||||
|
repite) y su texto para la Actividad lleva «por su comentario del dd/mm hh:mm: «extracto»». Con `_nota` además, guarda
|
||||||
|
las dos cosas.
|
||||||
|
- `docs/tableros/kanban.js` + `tablero.css`: el «¿por qué?» lista tus últimos 5 comentarios del pendiente; si comentaste
|
||||||
|
después del último cambio de columna, ese ya viene elegido (un clic en «Mover»). «Escribir otro motivo» sigue ahí. En el
|
||||||
|
hilo, el comentario elegido lleva la etiqueta «→ Hecho». El porqué pegado igual que un comentario anterior de la misma
|
||||||
|
persona también se muestra una sola vez. El orden se toma del historial, no de la hora (hay horas en UTC y en hora local).
|
||||||
|
- Pruebas: `bash backend/tests-comentarios.sh .` (26) y las demás; en el navegador, el caso de Michelle de punta a punta.
|
||||||
|
|
||||||
|
Desplegado igual que A13 (rsync de páginas tras la comparación; `kill -HUP` al maestro de gunicorn).
|
||||||
|
|
||||||
## B · Verificación después de desplegar
|
## B · Verificación después de desplegar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -1535,6 +1535,16 @@ def tarea_modificar():
|
|||||||
# comentario (28-oct): el «¿por qué?» al mover y los comentarios de la tarjeta; hasta 1000 caracteres. Va en el
|
# comentario (28-oct): el «¿por qué?» al mover y los comentarios de la tarjeta; hasta 1000 caracteres. Va en el
|
||||||
# texto del historial (la Actividad lo lee así) y aparte en «nota», para el hilo de comentarios del detalle.
|
# texto del historial (la Actividad lo lee así) y aparte en «nota», para el hilo de comentarios del detalle.
|
||||||
nota = _texto(d.get("_nota"), 1000)
|
nota = _texto(d.get("_nota"), 1000)
|
||||||
|
# «_nota_de» (28-oct, Leo): el porqué es un comentario que la misma persona ya dejó en el pendiente (su «cuando»).
|
||||||
|
# No se repite el texto en el hilo: la entrada guarda «ref» y la Actividad lleva un extracto.
|
||||||
|
ref = _texto(d.get("_nota_de"), 40)
|
||||||
|
ref_txt = ""
|
||||||
|
if ref:
|
||||||
|
previo = next((h for h in reversed(base.get("historial") or []) if h.get("cuando") == ref and h.get("quien") == u), None)
|
||||||
|
if not previo:
|
||||||
|
return jsonify(ok=False, error="No encontré ese comentario tuyo en el pendiente; escribe el porqué."), 400
|
||||||
|
citado = str(previo.get("nota") or previo.get("cambio") or "").strip()
|
||||||
|
ref_txt = "por su comentario del " + ref[8:10] + "/" + ref[5:7] + " " + ref[11:16] + ": «" + (citado[:140] + "…" if len(citado) > 140 else citado) + "»"
|
||||||
if not cambios and not nota:
|
if not cambios and not nota:
|
||||||
if t.get("orden") != base.get("orden") or t.get("responsable_ids") != base.get("responsable_ids"):
|
if t.get("orden") != base.get("orden") or t.get("responsable_ids") != base.get("responsable_ids"):
|
||||||
# solo cambió la posición (o se completaron los ids): se guarda sin entrada en el historial
|
# solo cambió la posición (o se completaron los ids): se guarda sin entrada en el historial
|
||||||
@ -1545,9 +1555,12 @@ def tarea_modificar():
|
|||||||
return jsonify(ok=True, tarea=t, aviso=aviso)
|
return jsonify(ok=True, tarea=t, aviso=aviso)
|
||||||
t["vencida"] = _vencida(t)
|
t["vencida"] = _vencida(t)
|
||||||
return jsonify(ok=True, tarea=t, sin_cambios=True, aviso=aviso)
|
return jsonify(ok=True, tarea=t, sin_cambios=True, aviso=aviso)
|
||||||
_hist(t, u, ("; ".join(cambios) + (" — " + nota if nota else "")) if cambios else nota)
|
extra = " — ".join(x for x in (nota, ref_txt if cambios else "") if x)
|
||||||
|
_hist(t, u, ("; ".join(cambios) + (" — " + extra if extra else "")) if cambios else nota)
|
||||||
if nota:
|
if nota:
|
||||||
t["historial"][-1]["nota"] = nota
|
t["historial"][-1]["nota"] = nota
|
||||||
|
if ref_txt and cambios:
|
||||||
|
t["historial"][-1]["ref"] = ref
|
||||||
_guardar_tarea(t)
|
_guardar_tarea(t)
|
||||||
t["vencida"] = _vencida(t)
|
t["vencida"] = _vencida(t)
|
||||||
return jsonify(ok=True, tarea=t, aviso=aviso)
|
return jsonify(ok=True, tarea=t, aviso=aviso)
|
||||||
|
|||||||
11
backend/lotes/2026-10-28_comentarios-elegir.json
Normal file
11
backend/lotes/2026-10-28_comentarios-elegir.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"lote": "2026-10-28_comentarios-elegir",
|
||||||
|
"autor": "claude",
|
||||||
|
"motivo": "28-oct, 18:00 (Leo): Michelle comenta antes de mover y el «¿por qué?» la hacía escribir lo mismo. Ahora elige uno de sus comentarios.",
|
||||||
|
"fuente": "/tableros/pendientes/",
|
||||||
|
"cambios": [
|
||||||
|
{ "id": "t-pendientes-comentarios",
|
||||||
|
"si_estado": ["hecho"],
|
||||||
|
"nota": "28-oct 18:00 (Leo): al mover se puede elegir uno de tus comentarios como el porqué; si comentaste después del último movimiento ya viene elegido y no se repite en el hilo. Pruebas: 26 del servidor y el caso de Michelle en el navegador." }
|
||||||
|
]
|
||||||
|
}
|
||||||
19
backend/tests-comentarios.sh
Executable file → Normal file
19
backend/tests-comentarios.sh
Executable file → Normal file
@ -12,6 +12,7 @@ for u in leo adriana jesus michelle claude; do
|
|||||||
echo "{\"id\":\"$u\",\"nombre\":\"$u\",\"area\":\"ingenieria\",\"activo\":true}" > $T/equipo/$u.json
|
echo "{\"id\":\"$u\",\"nombre\":\"$u\",\"area\":\"ingenieria\",\"activo\":true}" > $T/equipo/$u.json
|
||||||
done
|
done
|
||||||
echo '{"id":"t-a","titulo":"Cotizar gabinetes","responsable":"adriana, jesus","campeon":"adriana","estado":"en-curso","pista":"compras","sitios":["qro"],"orden":0,"historial":[]}' > $T/tareas/t-a.json
|
echo '{"id":"t-a","titulo":"Cotizar gabinetes","responsable":"adriana, jesus","campeon":"adriana","estado":"en-curso","pista":"compras","sitios":["qro"],"orden":0,"historial":[]}' > $T/tareas/t-a.json
|
||||||
|
echo '{"id":"t-m","titulo":"v6 bloque E","responsable":"michelle","campeon":"michelle","estado":"en-curso","pista":"ingenieria","sitios":["qro"],"orden":1,"historial":[]}' > $T/tareas/t-m.json
|
||||||
python3 $REPO/backend/app.py > $T/app.log 2>&1 &
|
python3 $REPO/backend/app.py > $T/app.log 2>&1 &
|
||||||
APP=$!; trap "kill $APP 2>/dev/null" EXIT; sleep 1.8
|
APP=$!; trap "kill $APP 2>/dev/null" EXIT; sleep 1.8
|
||||||
U="http://127.0.0.1:$PORT"
|
U="http://127.0.0.1:$PORT"
|
||||||
@ -45,6 +46,24 @@ R=$(as adriana -X PATCH -d '{"estado":"bloqueado"}' "$U/api/tarea?id=t-a")
|
|||||||
check "mover sin nota sigue funcionando (lotes, standup)" '"estado":"bloqueado"' "$R"
|
check "mover sin nota sigue funcionando (lotes, standup)" '"estado":"bloqueado"' "$R"
|
||||||
check "y sin nota no hay «nota»" 'None' "$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin)['tarea']['historial'][-1].get('nota'))")"
|
check "y sin nota no hay «nota»" 'None' "$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin)['tarea']['historial'][-1].get('nota'))")"
|
||||||
|
|
||||||
|
echo "## elegir uno de sus comentarios como el porqué (28-oct)"
|
||||||
|
R=$(as michelle -X PATCH -d '{"_nota":"D12 quedó a 3.44 mm de U7.1 (se pedían 2 mm); L2 a 5 mm."}' "$U/api/tarea?id=t-m")
|
||||||
|
CUANDO=$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin)['tarea']['historial'][-1]['cuando'])")
|
||||||
|
sleep 1.1; as jesus -X PATCH -d '{"_nota":"Comentario de Jesús en el mismo pendiente."}' "$U/api/tarea?id=t-m" > /dev/null
|
||||||
|
OTRO=$(as leo "$U/api/tareas" | python3 -c "import json,sys; t=[x for x in json.load(sys.stdin)['tareas'] if x['id']=='t-m'][0]; print(t['historial'][-1]['cuando'])")
|
||||||
|
check "con el comentario de otra persona no" 'comentario tuyo' "$(as michelle -X PATCH -d "{\"estado\":\"hecho\",\"_nota_de\":\"$OTRO\"}" "$U/api/tarea?id=t-m")"
|
||||||
|
check "con un comentario que no existe no" 'comentario tuyo' "$(as michelle -X PATCH -d '{"estado":"hecho","_nota_de":"2020-01-01T00:00:00"}' "$U/api/tarea?id=t-m")"
|
||||||
|
check "…y no se movió" '"estado": "en-curso"' "$(as leo "$U/api/tareas" | python3 -c "import json,sys; t=[x for x in json.load(sys.stdin)['tareas'] if x['id']=='t-m'][0]; print(json.dumps({'estado': t['estado']}))")"
|
||||||
|
R=$(as michelle -X PATCH -d "{\"estado\":\"hecho\",\"_nota_de\":\"$CUANDO\"}" "$U/api/tarea?id=t-m")
|
||||||
|
check "con su propio comentario se mueve" '"estado":"hecho"' "$R"
|
||||||
|
check "la entrada guarda «ref» con su «cuando»" "\"ref\": \"$CUANDO\"" "$(echo "$R" | ultimo)"
|
||||||
|
check "y no repite el texto como comentario (sin «nota»)" 'None' "$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin)['tarea']['historial'][-1].get('nota'))")"
|
||||||
|
check "la Actividad lleva el extracto" 'por su comentario del' "$(echo "$R" | ultimo)"
|
||||||
|
R=$(as michelle -X PATCH -d "{\"estado\":\"en-curso\",\"_nota_de\":\"$CUANDO\",\"_nota\":\"Leo pidió revisar L2 otra vez.\"}" "$U/api/tarea?id=t-m")
|
||||||
|
check "citar y además escribir: guarda las dos cosas" '"nota": "Leo pidió revisar L2 otra vez."' "$(echo "$R" | ultimo)"
|
||||||
|
check "…con su «ref»" "\"ref\": \"$CUANDO\"" "$(echo "$R" | ultimo)"
|
||||||
|
check "citar sin cambiar nada no deja rastro" '"sin_cambios":true' "$(as michelle -X PATCH -d "{\"_nota_de\":\"$CUANDO\"}" "$U/api/tarea?id=t-m")"
|
||||||
|
|
||||||
echo "## la Actividad sigue leyendo el historial"
|
echo "## la Actividad sigue leyendo el historial"
|
||||||
check "la Actividad muestra el comentario" 'a dos proveedores' "$(as leo "$U/api/actividad?dias=2")"
|
check "la Actividad muestra el comentario" 'a dos proveedores' "$(as leo "$U/api/actividad?dias=2")"
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,10 @@
|
|||||||
Comentarios (28-oct, Leo): el detalle tiene «Comentarios» (hilo + caja; PATCH solo con «_nota», cualquiera con
|
Comentarios (28-oct, Leo): el detalle tiene «Comentarios» (hilo + caja; PATCH solo con «_nota», cualquiera con
|
||||||
sesión) y la tarjeta muestra cuántos tiene. Al mover se pide «¿por qué?» cuando hace falta (reglaMover): a Hecho
|
sesión) y la tarjeta muestra cuántos tiene. Al mover se pide «¿por qué?» cuando hace falta (reglaMover): a Hecho
|
||||||
(la evidencia, D-27-6), al reabrir algo hecho, a Bloqueado y de En curso a Por hacer. «Cancelar» la deja donde estaba.
|
(la evidencia, D-27-6), al reabrir algo hecho, a Bloqueado y de En curso a Por hacer. «Cancelar» la deja donde estaba.
|
||||||
La nota va en «_nota» del mismo PATCH y queda en el historial («nota»). */
|
La nota va en «_nota» del mismo PATCH y queda en el historial («nota»). Si la persona ya había comentado, puede
|
||||||
|
elegir uno de sus comentarios como el porqué («_nota_de» = su «cuando»; la entrada guarda «ref»): no se repite en el
|
||||||
|
hilo, el comentario elegido lleva la etiqueta «→ Hecho». Si comentó después del último cambio de columna, ese viene
|
||||||
|
ya elegido. El orden es el del historial, no la hora (hay horas en UTC y en hora local). */
|
||||||
(function () {
|
(function () {
|
||||||
const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo', gestion: 'Gestión' };
|
const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo', gestion: 'Gestión' };
|
||||||
const ESTADOS = [['por-hacer', 'Por hacer'], ['en-curso', 'En curso'], ['bloqueado', 'Bloqueado'], ['hecho', 'Hecho']];
|
const ESTADOS = [['por-hacer', 'Por hacer'], ['en-curso', 'En curso'], ['bloqueado', 'Bloqueado'], ['hecho', 'Hecho']];
|
||||||
@ -186,10 +189,16 @@
|
|||||||
const NOMBRE_ESTADO = Object.fromEntries(ESTADOS);
|
const NOMBRE_ESTADO = Object.fromEntries(ESTADOS);
|
||||||
const NO_ES_COMENTARIO = /^(creado en |borró el pendiente|restaur)/i;
|
const NO_ES_COMENTARIO = /^(creado en |borró el pendiente|restaur)/i;
|
||||||
function comentarios(t) {
|
function comentarios(t) {
|
||||||
const out = [];
|
const out = [], porClave = {};
|
||||||
(t.historial || []).forEach(x => {
|
(t.historial || []).forEach((x, i) => {
|
||||||
const c = String(x.cambio || '');
|
const c = String(x.cambio || '');
|
||||||
const mv = c.match(/estado: ([a-z-]+) → ([a-z-]+)/);
|
const mv = c.match(/estado: ([a-z-]+) → ([a-z-]+)/);
|
||||||
|
// movió citando uno de sus comentarios: la etiqueta va en ese comentario, sin repetir el texto
|
||||||
|
if (x.ref && !x.nota) {
|
||||||
|
const o = porClave[x.ref + '|' + x.quien];
|
||||||
|
if (o && mv) (o.movs = o.movs || []).push({ a: mv[2], quien: x.quien, cuando: x.cuando });
|
||||||
|
return;
|
||||||
|
}
|
||||||
let texto = x.nota || '';
|
let texto = x.nota || '';
|
||||||
if (!texto) {
|
if (!texto) {
|
||||||
if (!c || NO_ES_COMENTARIO.test(c)) return;
|
if (!c || NO_ES_COMENTARIO.test(c)) return;
|
||||||
@ -197,10 +206,20 @@
|
|||||||
if (flecha < 0) texto = c;
|
if (flecha < 0) texto = c;
|
||||||
else { const k = c.indexOf(' — ', flecha); if (k < 0) return; texto = c.slice(k + 3); }
|
else { const k = c.indexOf(' — ', flecha); if (k < 0) return; texto = c.slice(k + 3); }
|
||||||
}
|
}
|
||||||
if (texto.trim()) out.push({ quien: x.quien, cuando: x.cuando, texto: texto.trim(), de: mv && mv[1], a: mv && mv[2] });
|
// el porqué pegado igual que un comentario suyo de antes (como se hacía antes del 28-oct en la tarde): una sola vez
|
||||||
|
if (mv && texto.trim()) {
|
||||||
|
const igual = out.slice().reverse().find(o => o.quien === x.quien && !o.a && o.texto === texto.trim());
|
||||||
|
if (igual) { (igual.movs = igual.movs || []).push({ a: mv[2], quien: x.quien, cuando: x.cuando }); return; }
|
||||||
|
}
|
||||||
|
if (texto.trim()) {
|
||||||
|
const o = { quien: x.quien, cuando: x.cuando, texto: texto.trim(), de: mv && mv[1], a: mv && mv[2], i };
|
||||||
|
out.push(o); porClave[x.cuando + '|' + x.quien] = o;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
// posición en el historial del último cambio de columna (-1 si nunca se movió)
|
||||||
|
const ultimoMovimiento = t => { const h = t.historial || []; for (let i = h.length - 1; i >= 0; i--) if (/estado: [a-z-]+ → /.test(String(h[i].cambio || ''))) return i; return -1; };
|
||||||
// cuándo pide el «¿por qué?» un cambio de columna (null = se mueve sin preguntar)
|
// cuándo pide el «¿por qué?» un cambio de columna (null = se mueve sin preguntar)
|
||||||
function reglaMover(desde, hacia) {
|
function reglaMover(desde, hacia) {
|
||||||
if (desde === hacia) return null;
|
if (desde === hacia) return null;
|
||||||
@ -624,12 +643,15 @@
|
|||||||
}
|
}
|
||||||
const mismaCol = t.estado === estado;
|
const mismaCol = t.estado === estado;
|
||||||
// 28-oct: algunos cambios de columna piden «¿por qué?» antes de moverse; «Cancelar» la deja donde estaba
|
// 28-oct: algunos cambios de columna piden «¿por qué?» antes de moverse; «Cancelar» la deja donde estaba
|
||||||
let nota = '';
|
let pq = null;
|
||||||
const regla = mismaCol ? null : reglaMover(t.estado, estado);
|
const regla = mismaCol ? null : reglaMover(t.estado, estado);
|
||||||
if (regla) {
|
if (regla) {
|
||||||
nota = await porQue(t, estado, regla);
|
pq = await porQue(t, estado, regla);
|
||||||
if (nota === null) { pintar(); return; }
|
if (pq === null) { pintar(); return; }
|
||||||
}
|
}
|
||||||
|
const extra = {};
|
||||||
|
if (pq && pq.nota) extra._nota = pq.nota;
|
||||||
|
if (pq && pq.ref) extra._nota_de = pq.ref;
|
||||||
const columna = tareas.filter(x => x.estado === estado && x.id !== id).sort((a, b) => a.orden - b.orden);
|
const columna = tareas.filter(x => x.estado === estado && x.id !== id).sort((a, b) => a.orden - b.orden);
|
||||||
columna.splice(Math.max(0, Math.min(idx, columna.length)), 0, t);
|
columna.splice(Math.max(0, Math.min(idx, columna.length)), 0, t);
|
||||||
const ids = columna.map(x => x.id);
|
const ids = columna.map(x => x.id);
|
||||||
@ -637,9 +659,9 @@
|
|||||||
const prev = { estado: t.estado, orden: t.orden };
|
const prev = { estado: t.estado, orden: t.orden };
|
||||||
t.estado = estado; columna.forEach((x, i) => x.orden = i); pintar();
|
t.estado = estado; columna.forEach((x, i) => x.orden = i); pintar();
|
||||||
try {
|
try {
|
||||||
if (!mismaCol) await api('/api/tarea?id=' + encodeURIComponent(id), json('PATCH', Object.assign({ estado, orden: ids.indexOf(id) }, nota ? { _nota: nota } : {})));
|
if (!mismaCol) await api('/api/tarea?id=' + encodeURIComponent(id), json('PATCH', Object.assign({ estado, orden: ids.indexOf(id) }, extra)));
|
||||||
await api('/api/tareas/orden', json('POST', { estado, ids }));
|
await api('/api/tareas/orden', json('POST', { estado, ids }));
|
||||||
if (!mismaCol) mensaje('«' + corto(t.titulo, 41) + '» pasó a ' + ESTADOS.find(e => e[0] === estado)[1] + '.' + (nota ? ' El porqué quedó en sus comentarios.' : ''), true);
|
if (!mismaCol) mensaje('«' + corto(t.titulo, 41) + '» pasó a ' + ESTADOS.find(e => e[0] === estado)[1] + '.' + (pq ? ' El porqué quedó en sus comentarios.' : ''), true);
|
||||||
await recargar();
|
await recargar();
|
||||||
} catch (e) { t.estado = prev.estado; t.orden = prev.orden; pintar(); mensaje('No se pudo mover: ' + e.message, false); }
|
} catch (e) { t.estado = prev.estado; t.orden = prev.orden; pintar(); mensaje('No se pudo mover: ' + e.message, false); }
|
||||||
}
|
}
|
||||||
@ -661,20 +683,51 @@
|
|||||||
const lab = el('label', { text: regla.pregunta });
|
const lab = el('label', { text: regla.pregunta });
|
||||||
const txt = el('textarea', { rows: '3', maxlength: '1000', placeholder: regla.ph, 'aria-required': 'true' });
|
const txt = el('textarea', { rows: '3', maxlength: '1000', placeholder: regla.ph, 'aria-required': 'true' });
|
||||||
lab.setAttribute('for', 'kb-pq-' + t.id); txt.id = 'kb-pq-' + t.id;
|
lab.setAttribute('for', 'kb-pq-' + t.id); txt.id = 'kb-pq-' + t.id;
|
||||||
const ayuda = el('div', { class: 'd-nota', text: 'Queda en los comentarios del pendiente, con tu nombre.' });
|
// 28-oct (Leo): si ya comentaste en este pendiente, elige uno de tus comentarios en vez de repetirlo
|
||||||
cuerpo.append(lab, txt, ayuda); dlgPq.append(cuerpo);
|
const yo = norm(user), desde = ultimoMovimiento(t);
|
||||||
|
const mios = comentarios(t).filter(c => norm(c.quien) === yo).slice(-5).reverse();
|
||||||
|
let elegido = null;
|
||||||
|
const ops = el('div', { class: 'pq-ops', role: 'radiogroup', 'aria-label': 'Tus comentarios' });
|
||||||
|
const radios = [];
|
||||||
|
function elegir(c) {
|
||||||
|
elegido = c;
|
||||||
|
radios.forEach(r => { r.inp.checked = r.c === c; r.lab.classList.toggle('on', r.c === c); });
|
||||||
|
txt.placeholder = c ? 'Agrega algo más (opcional)' : regla.ph;
|
||||||
|
txt.setAttribute('aria-required', c ? 'false' : 'true');
|
||||||
|
listoPara();
|
||||||
|
}
|
||||||
|
if (mios.length) {
|
||||||
|
cuerpo.append(lab);
|
||||||
|
mios.forEach(c => {
|
||||||
|
const l = el('label', { class: 'pq-op' });
|
||||||
|
const inp = el('input', { type: 'radio', name: 'kb-pq-op-' + t.id });
|
||||||
|
l.append(inp, el('span', { class: 'pq-op-c' }, el('span', { class: 'pq-op-f', text: 'Tu comentario · ' + fmt(c.cuando) }), el('span', { class: 'pq-op-t', text: c.texto })));
|
||||||
|
inp.addEventListener('change', () => elegir(c));
|
||||||
|
radios.push({ inp, lab: l, c }); ops.append(l);
|
||||||
|
});
|
||||||
|
const lOtro = el('label', { class: 'pq-op' });
|
||||||
|
const inpOtro = el('input', { type: 'radio', name: 'kb-pq-op-' + t.id });
|
||||||
|
lOtro.append(inpOtro, el('span', { class: 'pq-op-c' }, el('span', { class: 'pq-op-t', text: 'Escribir otro motivo' })));
|
||||||
|
inpOtro.addEventListener('change', () => { elegir(null); txt.focus(); });
|
||||||
|
radios.push({ inp: inpOtro, lab: lOtro, c: null }); ops.append(lOtro);
|
||||||
|
cuerpo.append(ops, txt);
|
||||||
|
} else cuerpo.append(lab, txt);
|
||||||
|
const ayuda = el('div', { class: 'd-nota', text: mios.length ? 'El comentario que elijas queda marcado con este cambio; no se repite.' : 'Queda en los comentarios del pendiente, con tu nombre.' });
|
||||||
|
cuerpo.append(ayuda); dlgPq.append(cuerpo);
|
||||||
const pie = el('div', { class: 'dpie' });
|
const pie = el('div', { class: 'dpie' });
|
||||||
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' });
|
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' });
|
||||||
const ok = el('button', { type: 'button', text: 'Mover a ' + (NOMBRE_ESTADO[estado] || estado) });
|
const ok = el('button', { type: 'button', text: 'Mover a ' + (NOMBRE_ESTADO[estado] || estado) });
|
||||||
const listoPara = () => { ok.disabled = txt.value.trim().length < 5; };
|
function listoPara() { ok.disabled = !elegido && txt.value.trim().length < 5; }
|
||||||
txt.addEventListener('input', listoPara); listoPara();
|
txt.addEventListener('input', listoPara);
|
||||||
|
// si comentaste después del último cambio de columna, ese comentario ya viene elegido
|
||||||
|
if (mios.length && mios[0].i > desde) elegir(mios[0]); else if (mios.length) elegir(null); else listoPara();
|
||||||
txt.addEventListener('keydown', e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && !ok.disabled) ok.click(); });
|
txt.addEventListener('keydown', e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && !ok.disabled) ok.click(); });
|
||||||
cancelar.addEventListener('click', () => dlgPq.close());
|
cancelar.addEventListener('click', () => dlgPq.close());
|
||||||
ok.addEventListener('click', () => { valor = txt.value.trim(); dlgPq.close(); });
|
ok.addEventListener('click', () => { valor = { nota: txt.value.trim(), ref: elegido ? elegido.cuando : '' }; dlgPq.close(); });
|
||||||
pie.append(cancelar, ok); dlgPq.append(pie);
|
pie.append(cancelar, ok); dlgPq.append(pie);
|
||||||
dlgPq.addEventListener('close', function fin() { dlgPq.removeEventListener('close', fin); listo(valor); });
|
dlgPq.addEventListener('close', function fin() { dlgPq.removeEventListener('close', fin); listo(valor); });
|
||||||
if (typeof dlgPq.showModal === 'function') dlgPq.showModal(); else dlgPq.setAttribute('open', '');
|
if (typeof dlgPq.showModal === 'function') dlgPq.showModal(); else dlgPq.setAttribute('open', '');
|
||||||
setTimeout(() => txt.focus(), 50);
|
setTimeout(() => (elegido ? ok : txt).focus(), 50);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -693,6 +746,8 @@
|
|||||||
const cab = el('div', { class: 'cm-cab' });
|
const cab = el('div', { class: 'cm-cab' });
|
||||||
cab.append(el('b', { text: EQUIPO[id] ? nombreCorto(id) : (c.quien || '') }), el('span', { class: 'cm-f', text: fmt(c.cuando) }));
|
cab.append(el('b', { text: EQUIPO[id] ? nombreCorto(id) : (c.quien || '') }), el('span', { class: 'cm-f', text: fmt(c.cuando) }));
|
||||||
if (c.a) cab.append(el('span', { class: 'cm-mv e-' + c.a, text: '→ ' + (NOMBRE_ESTADO[c.a] || c.a) }));
|
if (c.a) cab.append(el('span', { class: 'cm-mv e-' + c.a, text: '→ ' + (NOMBRE_ESTADO[c.a] || c.a) }));
|
||||||
|
(c.movs || []).forEach(m => cab.append(el('span', { class: 'cm-mv e-' + m.a, title: 'Movido con este comentario como porqué · ' + fmt(m.cuando),
|
||||||
|
text: '→ ' + (NOMBRE_ESTADO[m.a] || m.a) + (norm(m.quien) !== norm(c.quien) ? ' · ' + nombreCorto(norm(m.quien)) : '') })));
|
||||||
cuerpo.append(cab, el('div', { class: 'cm-t', text: c.texto }));
|
cuerpo.append(cab, el('div', { class: 'cm-t', text: c.texto }));
|
||||||
d.append(cuerpo);
|
d.append(cuerpo);
|
||||||
return d;
|
return d;
|
||||||
@ -714,10 +769,12 @@
|
|||||||
'aria-label': 'Nuevo comentario' });
|
'aria-label': 'Nuevo comentario' });
|
||||||
const btn = el('button', { type: 'button', text: 'Comentar' });
|
const btn = el('button', { type: 'button', text: 'Comentar' });
|
||||||
const msg = el('div', { class: 'msg' });
|
const msg = el('div', { class: 'msg' });
|
||||||
const listoPara = () => { btn.disabled = !user || !txt.value.trim(); };
|
// el botón no depende de los eventos de teclado (autocompletado, dictado, pegar): revisa el texto al hacer clic
|
||||||
txt.disabled = !user; txt.addEventListener('input', listoPara); listoPara();
|
const listoPara = () => { btn.disabled = !user; };
|
||||||
|
txt.disabled = !user; listoPara();
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const nota = txt.value.trim(); if (!nota) return;
|
const nota = txt.value.trim();
|
||||||
|
if (!nota) { msg.className = 'msg err'; msg.textContent = 'Escribe el comentario primero.'; txt.focus(); return; }
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const r = await api('/api/tarea?id=' + encodeURIComponent(t.id), json('PATCH', { _nota: nota }));
|
const r = await api('/api/tarea?id=' + encodeURIComponent(t.id), json('PATCH', { _nota: nota }));
|
||||||
|
|||||||
@ -18,7 +18,7 @@ hide:
|
|||||||
<li>Cada tarjeta del tablero es un <b>pendiente</b> (no confundir con la tarjeta EinarBoard). Léelo antes de empezar el trabajo.</li>
|
<li>Cada tarjeta del tablero es un <b>pendiente</b> (no confundir con la tarjeta EinarBoard). Léelo antes de empezar el trabajo.</li>
|
||||||
<li>Para cambiarlo de columna: arrástralo (arriba aparece una franja con las cuatro columnas, suéltalo ahí y no tienes que bajar), usa las flechas <b>← →</b> de la tarjeta o los botones de <b>Columna</b> en el detalle; en el teléfono, <b>Mover a…</b>. El tablero anota quién lo movió y cuándo.</li>
|
<li>Para cambiarlo de columna: arrástralo (arriba aparece una franja con las cuatro columnas, suéltalo ahí y no tienes que bajar), usa las flechas <b>← →</b> de la tarjeta o los botones de <b>Columna</b> en el detalle; en el teléfono, <b>Mover a…</b>. El tablero anota quién lo movió y cuándo.</li>
|
||||||
<li><b>Campeón:</b> aunque un pendiente tenga varios responsables, uno solo lo mueve entre columnas y responde por él (su avatar va primero, con anillo). Se elige en el detalle; sin elegir, es el primero de la lista. Los demás responsables editan todo lo demás; para moverlo, pídele al campeón que te lo pase. Leo (administrador) puede mover cualquiera.</li>
|
<li><b>Campeón:</b> aunque un pendiente tenga varios responsables, uno solo lo mueve entre columnas y responde por él (su avatar va primero, con anillo). Se elige en el detalle; sin elegir, es el primero de la lista. Los demás responsables editan todo lo demás; para moverlo, pídele al campeón que te lo pase. Leo (administrador) puede mover cualquiera.</li>
|
||||||
<li><b>¿Por qué?</b> (28-oct): algunos cambios de columna preguntan antes de moverse y lo que escribas queda en los comentarios con tu nombre. A <b>Hecho</b>: la evidencia (el acta, la liga o el archivo). Al <b>reabrir</b> algo que estaba en Hecho: qué faltó. A <b>Bloqueado</b>: qué lo bloquea y quién lo destraba. De En curso a Por hacer: por qué regresa. «Cancelar» lo deja donde estaba.</li>
|
<li><b>¿Por qué?</b> (28-oct): algunos cambios de columna preguntan antes de moverse y lo que escribas queda en los comentarios con tu nombre. A <b>Hecho</b>: la evidencia (el acta, la liga o el archivo). Al <b>reabrir</b> algo que estaba en Hecho: qué faltó. A <b>Bloqueado</b>: qué lo bloquea y quién lo destraba. De En curso a Por hacer: por qué regresa. Si ya lo explicaste en un comentario, <b>elige tu comentario</b> en vez de escribirlo otra vez (si comentaste después del último movimiento, ya viene elegido): queda marcado con el cambio y no se repite. «Cancelar» lo deja donde estaba.</li>
|
||||||
<li>Toca el título para ver el detalle: <b>comentarios</b>, descripción, enlaces, de qué depende y su historial. Ahí mismo edita y guarda. Cualquiera puede comentar, aunque no sea el campeón; el número junto a la fecha de la tarjeta dice cuántos comentarios tiene (tócalo para abrirlos).</li>
|
<li>Toca el título para ver el detalle: <b>comentarios</b>, descripción, enlaces, de qué depende y su historial. Ahí mismo edita y guarda. Cualquiera puede comentar, aunque no sea el campeón; el número junto a la fecha de la tarjeta dice cuántos comentarios tiene (tócalo para abrirlos).</li>
|
||||||
<li><b>Nuevo pendiente</b>: título corto, pista, responsable y fecha límite. Si depende de otro, márcalo en «Bloqueado por».</li>
|
<li><b>Nuevo pendiente</b>: título corto, pista, responsable y fecha límite. Si depende de otro, márcalo en «Bloqueado por».</li>
|
||||||
<li>Columna <b>Bloqueado</b>: solo si no puedes avanzar; al moverlo te pregunta qué falta y quién lo destraba.</li>
|
<li>Columna <b>Bloqueado</b>: solo si no puedes avanzar; al moverlo te pregunta qué falta y quién lo destraba.</li>
|
||||||
|
|||||||
@ -777,3 +777,16 @@
|
|||||||
.tb dialog .dcuerpo { max-height: none; flex: 1 1 auto; min-height: 0; }
|
.tb dialog .dcuerpo { max-height: none; flex: 1 1 auto; min-height: 0; }
|
||||||
.tb dialog .dpie { flex: none; }
|
.tb dialog .dpie { flex: none; }
|
||||||
}
|
}
|
||||||
|
/* «¿por qué?»: elegir uno de tus comentarios (28-oct) */
|
||||||
|
.tb dialog.kb-porque .pq-ops { display: grid; gap: 6px; margin: 0 0 8px; max-height: 240px; overflow: auto; }
|
||||||
|
.tb dialog.kb-porque .pq-op {
|
||||||
|
display: flex; gap: 9px; align-items: flex-start; margin: 0; padding: 8px 10px; cursor: pointer;
|
||||||
|
border: 1px solid var(--hg-linea-2); border-radius: 9px; background: var(--hg-plano); color: var(--hg-texto); font-weight: 500;
|
||||||
|
}
|
||||||
|
.tb dialog.kb-porque .pq-op input { width: auto; margin-top: 3px; flex: none; accent-color: var(--hg-curso); }
|
||||||
|
.tb dialog.kb-porque .pq-op.on { border-color: color-mix(in srgb, var(--hg-curso) 55%, transparent); background: var(--hg-curso-bg); }
|
||||||
|
.tb dialog.kb-porque .pq-op:focus-within { outline: 2px solid var(--hg-foco); outline-offset: 1px; }
|
||||||
|
.tb dialog.kb-porque .pq-op-c { display: grid; gap: 2px; min-width: 0; }
|
||||||
|
.tb dialog.kb-porque .pq-op-f { font-size: 12px; color: var(--hg-texto-3); font-weight: 600; }
|
||||||
|
.tb dialog.kb-porque .pq-op-t { font-size: 13.5px; line-height: 1.4; color: var(--hg-texto); white-space: pre-wrap; overflow-wrap: anywhere;
|
||||||
|
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user