Tablero: Adriana y Gibran pueden borrar pendientes; lo borrado va a la papelera
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
This commit is contained in:
parent
4ba4c712d5
commit
119eee5f70
@ -21,7 +21,7 @@ Adriana, Michelle, Jesús and the field technicians work from it; Claude closes
|
|||||||
|
|
||||||
| On the site | Source | Who edits | How |
|
| On the site | Source | Who edits | How |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Pendientes** (kanban, «Por persona», Ingeniería/Producción views) | server `/srv/projects/drogheda/tareas` | everyone in the UI; **Claude only through lotes** | `backend/lotes/AAAA-MM-DD_tema.json` → Leo runs `./scripts/tablero.sh revisar` then `aplicar`. Format and rules: `backend/lotes/README.md` |
|
| **Pendientes** (kanban, «Por persona», Ingeniería/Producción views) | server `/srv/projects/drogheda/tareas` | everyone in the UI (deleting: admins + `TAREAS_BORRAN`, by default Adriana and Gibran; soft delete to `tareas/_borrados/`); **Claude only through lotes** | `backend/lotes/AAAA-MM-DD_tema.json` → Leo runs `./scripts/tablero.sh revisar` then `aplicar`. Format and rules: `backend/lotes/README.md` |
|
||||||
| **Actividad** (home, Bitácora) | automatic: task history + actas | — | nothing to write; lotes show up as «Claude terminó / creó / comentó en …» |
|
| **Actividad** (home, Bitácora) | automatic: task history + actas | — | nothing to write; lotes show up as «Claude terminó / creó / comentó en …» |
|
||||||
| **Semáforo** (home, 22-oct) and per-site color | `docs/datos/proyecto.json` → `semaforo` (color rojo/amarillo/verde, titulo, texto, causas with **bold**/[links](/…), destraba, actualizado) + `estado`/`nota`/`meta` on each `sitios[]` item and on `oficina` | Leo decides the color; Claude or Leo edit | update it in every crisis or junta hand-off; map pins and the site list link to `/tableros/pendientes/?sitio=<id>` |
|
| **Semáforo** (home, 22-oct) and per-site color | `docs/datos/proyecto.json` → `semaforo` (color rojo/amarillo/verde, titulo, texto, causas with **bold**/[links](/…), destraba, actualizado) + `estado`/`nota`/`meta` on each `sitios[]` item and on `oficina` | Leo decides the color; Claude or Leo edit | update it in every crisis or junta hand-off; map pins and the site list link to `/tableros/pendientes/?sitio=<id>` |
|
||||||
| **Sitio of each task** (22-oct) | task field `sitios` = list of icave · timsa · eit · lzc · qro (no field = qro, Querétaro: fabricación e ingeniería) | everyone in the UI (site checkboxes); Claude through lotes (`"sitios": ["eit"]`) | filter `?sitio=eit,timsa`, view «Por sitio» (`?vista=sitio`); `GET /api/tareas` advertises the list; a lote with `sitios` needs the restarted backend |
|
| **Sitio of each task** (22-oct) | task field `sitios` = list of icave · timsa · eit · lzc · qro (no field = qro, Querétaro: fabricación e ingeniería) | everyone in the UI (site checkboxes); Claude through lotes (`"sitios": ["eit"]`) | filter `?sitio=eit,timsa`, view «Por sitio» (`?vista=sitio`); `GET /api/tareas` advertises the list; a lote with `sitios` needs the restarted backend |
|
||||||
|
|||||||
@ -385,6 +385,40 @@ curl -s -u leo:PASS 'https://drogheda.helgen.online/api/instalaciones?sitio=icav
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## A8 · 22-oct: Adriana y Gibran pueden borrar pendientes
|
||||||
|
|
||||||
|
Qué cambia: **borrar un pendiente** ya no es solo de los administradores (`ACTAS_ADMINS`): también pueden los usuarios de
|
||||||
|
`TAREAS_BORRAN`, que por omisión son `adriana` y `gibranlp` (no hace falta tocar el servicio). No cambia ningún otro
|
||||||
|
permiso: actas, usuarios, sitios y plan siguen siendo solo de administradores.
|
||||||
|
|
||||||
|
Borrar ya no destruye: el pendiente pasa a `/srv/projects/drogheda/tareas/_borrados/<id>__<AAAAMMDDhhmmss>.json` con su
|
||||||
|
historial, sigue en la Actividad («Adriana borró …», sin liga) y quien lo esperaba («Espera a que terminen») deja de
|
||||||
|
esperarlo, con una nota en su historial.
|
||||||
|
|
||||||
|
**Cambió `backend/app.py`: reinicia el servicio.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/deploy.sh
|
||||||
|
ssh leo@git.helgen.online 'sudo systemctl restart actas-hbox'
|
||||||
|
```
|
||||||
|
|
||||||
|
Para dar o quitar el permiso: en `/etc/systemd/system/actas-hbox.service`, la línea
|
||||||
|
`Environment=TAREAS_BORRAN=adriana,gibranlp` con los usuarios que sí (los administradores siempre pueden); después
|
||||||
|
`sudo systemctl daemon-reload && sudo systemctl restart actas-hbox`.
|
||||||
|
|
||||||
|
Recuperar un pendiente borrado (en el servidor):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/projects/drogheda/tareas
|
||||||
|
ls _borrados/ # <id>__AAAAMMDDhhmmss.json
|
||||||
|
mv _borrados/<id>__<fecha>.json <id>.json # vuelve al tablero como estaba; las dependencias que se quitaron no regresan solas
|
||||||
|
```
|
||||||
|
|
||||||
|
| Síntoma | Causa | Arreglo |
|
||||||
|
|---|---|---|
|
||||||
|
| Adriana o Gibran no ven «Borrar» en el detalle | servicio sin reiniciar | el `restart` de arriba |
|
||||||
|
| «Solo … pueden borrar pendientes» | ese usuario no es administrador ni está en `TAREAS_BORRAN` | así debe ser; si debe poder, agrégalo a `TAREAS_BORRAN` |
|
||||||
|
|
||||||
## B · Verificación después de desplegar
|
## B · Verificación después de desplegar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -75,10 +75,12 @@ cada cambio. Carpetas (variables de entorno, con su valor por omisión junto a `
|
|||||||
| `PLAN_FILE` | `/srv/projects/drogheda/plan-produccion.json` | el plan del pedido (570 → 113/327/130) |
|
| `PLAN_FILE` | `/srv/projects/drogheda/plan-produccion.json` | el plan del pedido (570 → 113/327/130) |
|
||||||
| `EQUIPO_DIR` | `/srv/projects/drogheda/equipo` | miembros del equipo (14-oct) + `_accesos.csv` |
|
| `EQUIPO_DIR` | `/srv/projects/drogheda/equipo` | miembros del equipo (14-oct) + `_accesos.csv` |
|
||||||
| `HTPASSWD_LOCK` | `/srv/projects/drogheda/.htpasswd.lock` | candado para escribir el htpasswd desde Equipo |
|
| `HTPASSWD_LOCK` | `/srv/projects/drogheda/.htpasswd.lock` | candado para escribir el htpasswd desde Equipo |
|
||||||
|
| `TAREAS_BORRAN` | `adriana,gibranlp` | quién borra pendientes además de `ACTAS_ADMINS` (22-oct); lo borrado va a `tareas/_borrados/` |
|
||||||
|
|
||||||
Reglas comunes: toda escritura exige el usuario autenticado (`X-Remote-User`; sin él, 403);
|
Reglas comunes: toda escritura exige el usuario autenticado (`X-Remote-User`; sin él, 403);
|
||||||
cada registro guarda `creado_por/creado_en/modificado_por/modificado_en` y un `historial`
|
cada registro guarda `creado_por/creado_en/modificado_por/modificado_en` y un `historial`
|
||||||
de `{quien, cuando, cambio}`; **solo un administrador borra** (`ACTAS_ADMINS`). Un `POST`
|
de `{quien, cuando, cambio}`; **solo un administrador borra** (`ACTAS_ADMINS`), salvo los pendientes, que también
|
||||||
|
borran los de `TAREAS_BORRAN` y pasan a `tareas/_borrados/` en vez de desaparecer (22-oct). Un `POST`
|
||||||
puede traer su propio `id` (lo usa `seed_1013.py` para ser idempotente): si ya existe → 409.
|
puede traer su propio `id` (lo usa `seed_1013.py` para ser idempotente): si ya existe → 409.
|
||||||
|
|
||||||
Siembra inicial (plan, 4 sitios, ítems de inventario, pendientes del 13-oct), idempotente:
|
Siembra inicial (plan, 4 sitios, ítems de inventario, pendientes del 13-oct), idempotente:
|
||||||
|
|||||||
@ -23,6 +23,8 @@ Environment=ACTAS_ADMINS=leo
|
|||||||
# Página de inicio (21-oct): quién escribe el enfoque de la semana y dónde se guarda.
|
# Página de inicio (21-oct): quién escribe el enfoque de la semana y dónde se guarda.
|
||||||
#Environment=HOY_EDITORES=leo,adriana
|
#Environment=HOY_EDITORES=leo,adriana
|
||||||
#Environment=HOY_FILE=/srv/projects/drogheda/enfoque.json
|
#Environment=HOY_FILE=/srv/projects/drogheda/enfoque.json
|
||||||
|
# Quién borra pendientes además de ACTAS_ADMINS (22-oct; por omisión adriana y gibranlp). Lo borrado va a tareas/_borrados/.
|
||||||
|
#Environment=TAREAS_BORRAN=adriana,gibranlp
|
||||||
Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd
|
Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd
|
||||||
Environment=PASS_MIN=8
|
Environment=PASS_MIN=8
|
||||||
Environment=PORT=8081
|
Environment=PORT=8081
|
||||||
|
|||||||
@ -30,6 +30,7 @@ Colecciones agregadas el 13-oct-2026 (tableros, inventario, sitios) — un JSON
|
|||||||
campos de auditoría creado_por/creado_en/modificado_por/modificado_en/historial; cualquier usuario
|
campos de auditoría creado_por/creado_en/modificado_por/modificado_en/historial; cualquier usuario
|
||||||
autenticado crea y modifica, solo un administrador borra:
|
autenticado crea y modifica, solo un administrador borra:
|
||||||
GET/POST /api/tareas (?sitio=eit,timsa filtra por sitio; 22-oct) · PATCH/DELETE /api/tarea?id= · GET /api/tareas/resumen · POST /api/tareas/orden
|
GET/POST /api/tareas (?sitio=eit,timsa filtra por sitio; 22-oct) · PATCH/DELETE /api/tarea?id= · GET /api/tareas/resumen · POST /api/tareas/orden
|
||||||
|
(DELETE /api/tarea: administradores y TAREAS_BORRAN, por omisión adriana y gibranlp; el pendiente pasa a tareas/_borrados/)
|
||||||
GET /api/plan · PUT /api/plan (admin)
|
GET /api/plan · PUT /api/plan (admin)
|
||||||
GET/POST /api/inventario · PATCH /api/inventario/item?id= · POST /api/inventario/movimiento
|
GET/POST /api/inventario · PATCH /api/inventario/item?id= · POST /api/inventario/movimiento
|
||||||
GET /api/inventario/movimientos?item_id= · GET /api/inventario/export.csv
|
GET /api/inventario/movimientos?item_id= · GET /api/inventario/export.csv
|
||||||
@ -1351,7 +1352,8 @@ def tareas_listar():
|
|||||||
equipo = {k: {"nombre": m.get("nombre"), "activo": m.get("activo", True), "area": m.get("area")} for k, m in miembros.items()}
|
equipo = {k: {"nombre": m.get("nombre"), "activo": m.get("activo", True), "area": m.get("area")} for k, m in miembros.items()}
|
||||||
return jsonify(ok=True, total=len(out), tareas=out, pistas=PISTAS, estados=ESTADOS, equipo=equipo,
|
return jsonify(ok=True, total=len(out), tareas=out, pistas=PISTAS, estados=ESTADOS, equipo=equipo,
|
||||||
versiones=VERSIONES_TARJETA, sitios=[{"id": i, "nombre": n, "corto": c} for i, n, c in SITIOS_TAREA],
|
versiones=VERSIONES_TARJETA, sitios=[{"id": i, "nombre": n, "corto": c} for i, n, c in SITIOS_TAREA],
|
||||||
admin=is_admin(), user=current_user())
|
admin=is_admin(), user=current_user(), puede_borrar=_puede_borrar_tarea(),
|
||||||
|
borran=sorted(ADMINS | TAREAS_BORRAN))
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/tareas")
|
@app.post("/api/tareas")
|
||||||
@ -1462,20 +1464,54 @@ def tareas_orden():
|
|||||||
return jsonify(ok=True, actualizadas=n)
|
return jsonify(ok=True, actualizadas=n)
|
||||||
|
|
||||||
|
|
||||||
|
# Quién borra pendientes (22-oct, Leo): los administradores (ACTAS_ADMINS) y TAREAS_BORRAN, por omisión
|
||||||
|
# adriana y gibranlp. Borrar no destruye: el pendiente pasa a tareas/_borrados/ con su historial (sigue en la
|
||||||
|
# Actividad) y se recupera moviendo el archivo de vuelta. Quien lo esperaba («bloqueada_por») deja de esperarlo.
|
||||||
|
TAREAS_BORRAN = {u.strip().lower() for u in os.environ.get("TAREAS_BORRAN", "adriana,gibranlp").split(",") if u.strip()}
|
||||||
|
TAREAS_BORRADOS = os.path.join(TAREAS_DIR, "_borrados")
|
||||||
|
|
||||||
|
|
||||||
|
def _puede_borrar_tarea():
|
||||||
|
u = current_user().lower()
|
||||||
|
return bool(u) and (u in ADMINS or u in TAREAS_BORRAN)
|
||||||
|
|
||||||
|
|
||||||
|
def _y_lista(xs):
|
||||||
|
xs = [str(x) for x in xs if x]
|
||||||
|
return " y ".join([", ".join(xs[:-1]), xs[-1]]) if len(xs) > 1 else (xs[0] if xs else "")
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/tarea")
|
@app.delete("/api/tarea")
|
||||||
def tarea_borrar():
|
def tarea_borrar():
|
||||||
if not is_admin():
|
u = quien()
|
||||||
return jsonify(ok=False, error="Solo un administrador puede borrar pendientes. "
|
if not u:
|
||||||
"Si ya no aplica, muévelo a Hecho."), 403
|
return _sin_usuario()
|
||||||
|
if not _puede_borrar_tarea():
|
||||||
|
nombres = {k: {"Leopoldo": "Leo"}.get((m.get("nombre") or k).split(" ")[0], (m.get("nombre") or k).split(" ")[0])
|
||||||
|
for k, m in _miembros().items()}
|
||||||
|
return jsonify(ok=False, error="Solo " + _y_lista([nombres.get(x, x) for x in sorted(ADMINS | TAREAS_BORRAN)])
|
||||||
|
+ " pueden borrar pendientes. Si ya no aplica, muévelo a Hecho."), 403
|
||||||
rid = slug(request.args.get("id"), 60)
|
rid = slug(request.args.get("id"), 60)
|
||||||
if not rid:
|
if not rid:
|
||||||
return jsonify(ok=False, error="falta id"), 400
|
return jsonify(ok=False, error="falta id"), 400
|
||||||
ruta = _ruta_registro(TAREAS_DIR, rid)
|
ruta = _ruta_registro(TAREAS_DIR, rid)
|
||||||
if not os.path.isfile(ruta):
|
t = _leer_json(ruta) if os.path.isfile(ruta) else None
|
||||||
|
if not isinstance(t, dict):
|
||||||
return jsonify(ok=False, error=f"No existe el pendiente {rid}."), 404
|
return jsonify(ok=False, error=f"No existe el pendiente {rid}."), 404
|
||||||
|
_hist(t, u, "borró el pendiente")
|
||||||
|
t["borrado_por"], t["borrado_en"] = u, t["modificado_en"]
|
||||||
|
destino = os.path.join(TAREAS_BORRADOS, rid + "__" + re.sub(r"\D", "", t["borrado_en"]) + ".json")
|
||||||
|
_escribir_json(destino, t) # primero la copia en la papelera; después se quita del tablero
|
||||||
os.remove(ruta)
|
os.remove(ruta)
|
||||||
|
sueltos = []
|
||||||
|
for o in _listar_json(TAREAS_DIR):
|
||||||
|
if rid in (o.get("bloqueada_por") or []):
|
||||||
|
o["bloqueada_por"] = [x for x in o["bloqueada_por"] if x != rid]
|
||||||
|
_hist(o, u, f"Ya no espera a «{_corto(t.get('titulo'))}»: se borró ese pendiente.")
|
||||||
|
_escribir_json(_ruta_registro(TAREAS_DIR, o["id"]), o)
|
||||||
|
sueltos.append(o["id"])
|
||||||
_reescribir_indice(TAREAS_DIR, _listar_json(TAREAS_DIR), COLS_TAREAS)
|
_reescribir_indice(TAREAS_DIR, _listar_json(TAREAS_DIR), COLS_TAREAS)
|
||||||
return jsonify(ok=True, borrado=rid)
|
return jsonify(ok=True, borrado=rid, papelera=os.path.basename(destino), dependencias=sueltos)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/tareas/resumen")
|
@app.get("/api/tareas/resumen")
|
||||||
@ -2051,13 +2087,17 @@ def actividad():
|
|||||||
limite = max(1, min(_entero(request.args.get("limite"), 30), 200))
|
limite = max(1, min(_entero(request.args.get("limite"), 30), 200))
|
||||||
desde = (datetime.datetime.now() - datetime.timedelta(days=dias)).replace(microsecond=0).isoformat()
|
desde = (datetime.datetime.now() - datetime.timedelta(days=dias)).replace(microsecond=0).isoformat()
|
||||||
eventos = []
|
eventos = []
|
||||||
for t in _listar_json(TAREAS_DIR):
|
for borrados, lista in ((False, _listar_json(TAREAS_DIR)), (True, _listar_json(TAREAS_BORRADOS))):
|
||||||
|
for t in lista:
|
||||||
for h in (t.get("historial") or []):
|
for h in (t.get("historial") or []):
|
||||||
cuando = str(h.get("cuando") or "")
|
cuando = str(h.get("cuando") or "")
|
||||||
if cuando >= desde:
|
if cuando >= desde:
|
||||||
eventos.append({"tipo": "pendiente", "cuando": cuando, "quien": h.get("quien") or "",
|
ev = {"tipo": "pendiente", "cuando": cuando, "quien": h.get("quien") or "",
|
||||||
"cambio": _texto(h.get("cambio"), 300), "id": t.get("id"),
|
"cambio": _texto(h.get("cambio"), 300), "id": t.get("id"),
|
||||||
"titulo": t.get("titulo"), "pista": t.get("pista"), "estado": t.get("estado")})
|
"titulo": t.get("titulo"), "pista": t.get("pista"), "estado": t.get("estado")}
|
||||||
|
if borrados:
|
||||||
|
ev["borrado"] = True # ya no está en el tablero: la Actividad no lo liga
|
||||||
|
eventos.append(ev)
|
||||||
if os.path.isdir(BASE):
|
if os.path.isdir(BASE):
|
||||||
for tipo in sorted(os.listdir(BASE)):
|
for tipo in sorted(os.listdir(BASE)):
|
||||||
carpeta = os.path.join(BASE, tipo)
|
carpeta = os.path.join(BASE, tipo)
|
||||||
|
|||||||
@ -36,8 +36,13 @@ check "PATCH inexistente → 404" "No existe" "$(curl "${ALAN[@]}" -X PATCH "$U/
|
|||||||
check "POST orden (renumera columna: t-b cambia de columna, t-a de posición)" '"actualizadas":2' "$(curl "${ALAN[@]}" -X POST $U/api/tareas/orden -d '{"estado":"en-curso","ids":["t-b","t-a"]}')"
|
check "POST orden (renumera columna: t-b cambia de columna, t-a de posición)" '"actualizadas":2' "$(curl "${ALAN[@]}" -X POST $U/api/tareas/orden -d '{"estado":"en-curso","ids":["t-b","t-a"]}')"
|
||||||
check "resumen" '"en-curso": 1' "$(curl -s $U/api/tareas/resumen | python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin)["por_pista"]["ingenieria"]))')"
|
check "resumen" '"en-curso": 1' "$(curl -s $U/api/tareas/resumen | python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin)["por_pista"]["ingenieria"]))')"
|
||||||
check "resumen vencidas" '"id":"t-a"' "$(curl -s $U/api/tareas/resumen)"
|
check "resumen vencidas" '"id":"t-a"' "$(curl -s $U/api/tareas/resumen)"
|
||||||
check "DELETE alan → 403" "Solo un administrador" "$(curl "${ALAN[@]}" -X DELETE "$U/api/tarea?id=t-b")"
|
check "DELETE alan → 403" "pueden borrar pendientes" "$(curl "${ALAN[@]}" -X DELETE "$U/api/tarea?id=t-b")"
|
||||||
check "DELETE leo → ok" '"borrado":"t-b"' "$(curl "${LEO[@]}" -X DELETE "$U/api/tarea?id=t-b")"
|
check "DELETE leo → ok" '"borrado":"t-b"' "$(curl "${LEO[@]}" -X DELETE "$U/api/tarea?id=t-b")"
|
||||||
|
# 22-oct: lo borrado va a la papelera y sigue en la Actividad; adriana y gibranlp también borran (TAREAS_BORRAN)
|
||||||
|
check "papelera" "t-b__" "$(ls $T/tareas/_borrados/ 2>/dev/null)"
|
||||||
|
check "Actividad: borró, sin liga" '"borrado":true' "$(curl "${LEO[@]}" "$U/api/actividad?dias=1&limite=200")"
|
||||||
|
check "adriana puede borrar" '"puede_borrar":true' "$(curl -s -H "X-Remote-User: adriana" $U/api/tareas)"
|
||||||
|
check "alan no" '"puede_borrar":false' "$(curl "${ALAN[@]}" $U/api/tareas)"
|
||||||
check "índice CSV escrito" "t-a" "$(cat $T/tareas/_indice.csv)"
|
check "índice CSV escrito" "t-a" "$(cat $T/tareas/_indice.csv)"
|
||||||
|
|
||||||
echo "## plan"
|
echo "## plan"
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
// Lo que dice el historial («estado: por-hacer → en-curso; fecha_limite: … → …») en una frase.
|
// Lo que dice el historial («estado: por-hacer → en-curso; fecha_limite: … → …») en una frase.
|
||||||
function frasePendiente(e) {
|
function frasePendiente(e) {
|
||||||
const c = String(e.cambio || '');
|
const c = String(e.cambio || '');
|
||||||
|
if (/^borró el pendiente/i.test(c)) return { verbo: 'borró', fin: '' };
|
||||||
const m = c.match(/^(?:creado en|creó el pendiente)\s*([a-z-]*)/i);
|
const m = c.match(/^(?:creado en|creó el pendiente)\s*([a-z-]*)/i);
|
||||||
if (m) return { verbo: 'creó', fin: m[1] && m[1] !== 'por-hacer' && ESTADOS[m[1]] ? ' en ' + ESTADOS[m[1]] : '' };
|
if (m) return { verbo: 'creó', fin: m[1] && m[1] !== 'por-hacer' && ESTADOS[m[1]] ? ' en ' + ESTADOS[m[1]] : '' };
|
||||||
const partes = c.split(/;\s*/).map(p => p.match(/^([a-z_]+):\s*(.*?)\s*→\s*(.*)$/)).filter(Boolean);
|
const partes = c.split(/;\s*/).map(p => p.match(/^([a-z_]+):\s*(.*?)\s*→\s*(.*)$/)).filter(Boolean);
|
||||||
@ -93,7 +94,9 @@
|
|||||||
const quien = el('b', { text: q.nombre });
|
const quien = el('b', { text: q.nombre });
|
||||||
if (e.tipo === 'pendiente') {
|
if (e.tipo === 'pendiente') {
|
||||||
const f = frasePendiente(e);
|
const f = frasePendiente(e);
|
||||||
const a = el('a', { href: '/tableros/pendientes/?id=' + encodeURIComponent(e.id || ''), text: '«' + corto(e.titulo, opts.corto || 70) + '»' });
|
// un pendiente borrado ya no está en el tablero: su título va sin liga
|
||||||
|
const a = e.borrado ? el('span', { class: 'act-borrado', title: 'Pendiente borrado', text: '«' + corto(e.titulo, opts.corto || 70) + '»' })
|
||||||
|
: el('a', { href: '/tableros/pendientes/?id=' + encodeURIComponent(e.id || ''), text: '«' + corto(e.titulo, opts.corto || 70) + '»' });
|
||||||
t.append(quien, ' ' + f.verbo + ' ', a);
|
t.append(quien, ' ' + f.verbo + ' ', a);
|
||||||
if (f.fin) t.append(f.fin);
|
if (f.fin) t.append(f.fin);
|
||||||
if (f.nota) t.append(el('span', { class: 'act-nota', text: corto(f.nota, 160) }));
|
if (f.nota) t.append(el('span', { class: 'act-nota', text: corto(f.nota, 160) }));
|
||||||
|
|||||||
@ -24,7 +24,7 @@ Las páginas que abres para saber **dónde está el proyecto hoy**: qué pendien
|
|||||||
|
|
||||||
## Cómo se usan
|
## Cómo se usan
|
||||||
|
|
||||||
1. Los pendientes se mueven arrastrando entre columnas (en el teléfono, con «Mover a…»). Cualquier usuario autenticado crea y mueve; **borrar y reabrir** es de administradores (`ACTAS_ADMINS`).
|
1. Los pendientes se mueven arrastrando entre columnas (en el teléfono, con «Mover a…»). Cualquier usuario autenticado crea y mueve. **Borrar** un pendiente lo pueden hacer Leo, Adriana y Gibran: sale del tablero, queda en la Actividad y Leo lo puede recuperar; si solo ya no aplica, mejor muévelo a Hecho. Borrar o reabrir **actas** sigue siendo de administradores (`ACTAS_ADMINS`).
|
||||||
2. Cada pendiente lleva **pista**, **responsable** (uno o varios del [Equipo](equipo.md)), **fecha límite** y ligas. Si depende de otro, márcalo en «Espera a que terminen» y, si no puedes avanzar, déjalo en *Bloqueado*.
|
2. Cada pendiente lleva **pista**, **responsable** (uno o varios del [Equipo](equipo.md)), **fecha límite** y ligas. Si depende de otro, márcalo en «Espera a que terminen» y, si no puedes avanzar, déjalo en *Bloqueado*.
|
||||||
3. Los tableros de Ingeniería y Producción son **vistas** de los mismos pendientes filtrados por pista, más sus bloques fijos (versiones, cantidades).
|
3. Los tableros de Ingeniería y Producción son **vistas** de los mismos pendientes filtrados por pista, más sus bloques fijos (versiones, cantidades).
|
||||||
4. Las cantidades del plan de producción (versión → objetivo → fecha) las edita un administrador en el propio tablero; los conteos reales salen de las actas.
|
4. Las cantidades del plan de producción (versión → objetivo → fecha) las edita un administrador en el propio tablero; los conteos reales salen de las actas.
|
||||||
|
|||||||
@ -21,7 +21,8 @@
|
|||||||
filtros: true = barra completa · 'min' = solo vistas, «Solo los míos» y «Nuevo» · false = sin barra.
|
filtros: true = barra completa · 'min' = solo vistas, «Solo los míos» y «Nuevo» · false = sin barra.
|
||||||
|
|
||||||
Servidor: GET /api/tareas · POST /api/tareas · PATCH /api/tarea?id= · POST /api/tareas/orden ·
|
Servidor: GET /api/tareas · POST /api/tareas · PATCH /api/tarea?id= · POST /api/tareas/orden ·
|
||||||
DELETE /api/tarea?id= (solo admin). Cada cambio guarda quién y cuándo en el historial.
|
DELETE /api/tarea?id= (administradores y TAREAS_BORRAN: «puede_borrar» y «borran» en GET /api/tareas;
|
||||||
|
el pendiente pasa a la papelera del servidor y sigue en la Actividad). Cada cambio guarda quién y cuándo.
|
||||||
Equipo (14-oct): los responsables son miembros de /api/equipo (GET /api/tareas ya trae el mapa
|
Equipo (14-oct): los responsables son miembros de /api/equipo (GET /api/tareas ya trae el mapa
|
||||||
id → nombre). El pendiente exige responsable; se puede escribir texto libre si la persona aún no
|
id → nombre). El pendiente exige responsable; se puede escribir texto libre si la persona aún no
|
||||||
está dada de alta (el servidor avisa). Kanban.nombresResp(t, equipo) sirve a otras páginas.
|
está dada de alta (el servidor avisa). Kanban.nombresResp(t, equipo) sirve a otras páginas.
|
||||||
@ -164,7 +165,7 @@
|
|||||||
|
|
||||||
function montar(root, opts) {
|
function montar(root, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
let tareas = [], admin = false, user = '', porId = {};
|
let tareas = [], admin = false, user = '', porId = {}, puedeBorrar = false, borran = [];
|
||||||
const q0 = new URLSearchParams(location.search);
|
const q0 = new URLSearchParams(location.search);
|
||||||
const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes', mios: q0.get('mios') === '1',
|
const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes', mios: q0.get('mios') === '1',
|
||||||
sitios: new Set((q0.get('sitio') || '').split(',').map(x => x.trim().toLowerCase()).filter(Boolean).map(x => ALIAS_SITIO[x] || x)) };
|
sitios: new Set((q0.get('sitio') || '').split(',').map(x => x.trim().toLowerCase()).filter(Boolean).map(x => ALIAS_SITIO[x] || x)) };
|
||||||
@ -495,6 +496,8 @@
|
|||||||
try {
|
try {
|
||||||
const j = await api('/api/tareas');
|
const j = await api('/api/tareas');
|
||||||
tareas = j.tareas || []; admin = !!j.admin; user = j.user || '';
|
tareas = j.tareas || []; admin = !!j.admin; user = j.user || '';
|
||||||
|
puedeBorrar = j.puede_borrar === undefined ? admin : !!j.puede_borrar; // servidor de antes del 22-oct: solo admin
|
||||||
|
borran = Array.isArray(j.borran) ? j.borran : [];
|
||||||
if (Array.isArray(j.sitios) && j.sitios.length) SITIOS = j.sitios.map(x => [x.id, x.corto || x.id, x.id === 'qro' ? 'Querétaro' : (x.corto || x.id), x.nombre || x.id]);
|
if (Array.isArray(j.sitios) && j.sitios.length) SITIOS = j.sitios.map(x => [x.id, x.corto || x.id, x.id === 'qro' ? 'Querétaro' : (x.corto || x.id), x.nombre || x.id]);
|
||||||
usarEquipo(j.equipo);
|
usarEquipo(j.equipo);
|
||||||
pintar();
|
pintar();
|
||||||
@ -598,15 +601,23 @@
|
|||||||
const m = el('div', { class: 'msg' }); cuerpo.append(m);
|
const m = el('div', { class: 'msg' }); cuerpo.append(m);
|
||||||
|
|
||||||
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
||||||
if (!nuevo && admin) {
|
if (!nuevo && puedeBorrar) {
|
||||||
const del = el('button', { type: 'button', class: 'peligro izq', text: 'Borrar' });
|
const del = el('button', { type: 'button', class: 'peligro izq', text: 'Borrar' });
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
if (!confirm('¿Borrar el pendiente «' + t.titulo + '»? No se puede deshacer. Si ya no aplica, mejor muévelo a Hecho.')) return;
|
if (!confirm('¿Borrar el pendiente «' + t.titulo + '»? Sale del tablero; queda en la Actividad y Leo lo puede recuperar. Si ya no aplica, mejor muévelo a Hecho.')) return;
|
||||||
try { await api('/api/tarea?id=' + encodeURIComponent(t.id), { method: 'DELETE' }); dlg.close(); mensaje('Pendiente borrado.', true); recargar(); }
|
try {
|
||||||
catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
const r = await api('/api/tarea?id=' + encodeURIComponent(t.id), { method: 'DELETE' });
|
||||||
|
const n = (r.dependencias || []).length;
|
||||||
|
dlg.close();
|
||||||
|
mensaje('Pendiente borrado.' + (n === 1 ? ' Un pendiente ya no lo espera.' : n > 1 ? ' ' + n + ' pendientes ya no lo esperan.' : ''), true);
|
||||||
|
recargar();
|
||||||
|
} catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
||||||
});
|
});
|
||||||
pie.append(del);
|
pie.append(del);
|
||||||
} else if (!nuevo) pie.append(el('span', { class: 'izq vacio', text: 'Solo el administrador borra; si ya no aplica, muévelo a Hecho.' }));
|
} else if (!nuevo) {
|
||||||
|
const nombres = borran.map(nombreCorto), quienes = nombres.length > 1 ? nombres.slice(0, -1).join(', ') + ' y ' + nombres[nombres.length - 1] : (nombres[0] || 'un administrador');
|
||||||
|
pie.append(el('span', { class: 'izq vacio', text: 'Solo ' + quienes + ' borran pendientes; si ya no aplica, muévelo a Hecho.' }));
|
||||||
|
}
|
||||||
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' }); cancelar.addEventListener('click', () => dlg.close());
|
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' }); cancelar.addEventListener('click', () => dlg.close());
|
||||||
const guardar = el('button', { type: 'button', text: nuevo ? 'Crear pendiente' : 'Guardar cambios' });
|
const guardar = el('button', { type: 'button', text: nuevo ? 'Crear pendiente' : 'Guardar cambios' });
|
||||||
guardar.addEventListener('click', async () => {
|
guardar.addEventListener('click', async () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user