feat: PWA instalable, cambio de contraseña y dashboards por rol

- POST /api/auth/cambiar-password (bcrypt 12, política ≥8+1 número) +
  modal en las 6 apps con auth
- PWA: manifest + íconos de marca en las 7 apps (sin service worker a
  propósito, documentado); instalable en celular
- Dashboards: 'Mi tablero' para vendedor (sus KPIs y funnel) y sección
  Equipo para coordinador/corporativo/admin
- mi-dia: equipo.porVendedor gana leadsNuevosSemana
- Tests: api 271, cotizador 88, alta 88, cobranza 81, agente-ia 19,
  dashboards 15, mi-dia 11, landing 4
This commit is contained in:
2026-08-28 08:37:41 +00:00
parent aa27e89c75
commit cb86a5190a
80 changed files with 2533 additions and 323 deletions

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Genera los íconos PWA de las 7 apps del ecosistema FRASA.
Ícono: cuadrado #1B4332 (verde marca) con la inicial del módulo en blanco,
en 192×192 y 512×512 PNG, en apps/<app>/public/icons/.
Uso: /root/.venv-tools/bin/python scripts/generar-iconos-pwa.py
"""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
VERDE = (0x1B, 0x43, 0x32) # #1B4332, --color-accent StudioBlank
BLANCO = (0xFF, 0xFF, 0xFF)
TAMANOS = (192, 512)
FUENTE = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
# app → letra del ícono
APPS = {
"landing": "F",
"cotizador": "C",
"alta-clientes": "A",
"cobranza": "$",
"agente-ia": "I",
"dashboards": "D",
"mi-dia": "H",
}
RAIZ = Path(__file__).resolve().parent.parent
def generar(letra: str, tamano: int, destino: Path) -> None:
imagen = Image.new("RGB", (tamano, tamano), VERDE)
dibujo = ImageDraw.Draw(imagen)
fuente = ImageFont.truetype(FUENTE, size=int(tamano * 0.55))
caja = dibujo.textbbox((0, 0), letra, font=fuente)
ancho, alto = caja[2] - caja[0], caja[3] - caja[1]
x = (tamano - ancho) / 2 - caja[0]
y = (tamano - alto) / 2 - caja[1]
dibujo.text((x, y), letra, font=fuente, fill=BLANCO)
imagen.save(destino, "PNG")
def main() -> None:
for app, letra in APPS.items():
directorio = RAIZ / "apps" / app / "public" / "icons"
directorio.mkdir(parents=True, exist_ok=True)
for tamano in TAMANOS:
destino = directorio / f"icon-{tamano}.png"
generar(letra, tamano, destino)
print(f"{app}: {destino.relative_to(RAIZ)}")
if __name__ == "__main__":
main()