Files
HoruxDespachos/docs/plans/2026-04-25-despacho-tareas-papeleria.md
2026-04-27 22:09:36 -06:00

20 KiB

Sesión 2026-04-25 — Módulo Despacho, Tareas y Papelería

Sesión enfocada en features nuevos del fork Horux_despacho. Los fixes fiscales del día anterior están en docs/plans/2026-04-24-session-fixes-and-features.md.


Índice

  1. Banner "CSD recién tramitado"
  2. Preferencias de notificación por contribuyente
  3. Tareas operativas (recurrentes)
  4. Papelería de Trabajo
  5. Módulo "Despacho" — 3 páginas
  6. Selector de periodo global
  7. Asignación de supervisor desde /usuarios
  8. Ajustes de UI
  9. Migraciones aplicadas
  10. Pendientes

1. Banner "CSD recién tramitado"

Problema

El SAT tarda 24-72h en propagar un CSD nuevo a la Lista de Contribuyentes Obligados (LCO). Durante esa ventana, intentos de emisión vía Facturapi fallan con mensajes tipo "RFC no encontrado en LCO". El user pierde tiempo intentando emitir y abre tickets innecesarios.

Solución

Banner contextual en /facturacion que aparece SOLO si hubo un rechazo del SAT con patrón LCO en las últimas 24h.

Archivos

  • Migración 033: facturapi_orgs.last_lco_rejection_at timestamptz.
  • apps/api/src/controllers/facturacion.controller.ts:
    • Helper isLcoRejection(message) con regex contra patrones SAT.
    • Helper markLcoRejection(pool, contribuyenteId) upsertea timestamp.
    • En el catch de emitir() se dispara la marca cuando aplica.
    • Endpoint getLcoStatus retorna { hasRecentLcoRejection, rejectedAt }.
  • apps/api/src/routes/facturacion.routes.ts: GET /facturacion/lco-status.
  • apps/web/app/(dashboard)/facturacion/page.tsx: banner amber al inicio del form, polling cada 15min.

Mensaje

CSD aún en proceso de validación — espera 24 horas antes de emitir tu factura.

Auto-expiración

La condición es rejectedAt > NOW() - 24h. Pasadas las 24h el banner desaparece sin necesidad de cron.


2. Preferencias de notificación por contribuyente

Problema

Los emails informativos se enviaban siempre. El user quería poder desactivar tipos específicos por contribuyente sin afectar a otros.

Solución

JSONB email_preferences en contribuyentes; los handlers de envío respetan la preferencia antes de mandar.

Archivos

  • Migración 034: contribuyentes.email_preferences jsonb DEFAULT '{}'.
  • apps/api/src/services/notification-preferences.service.ts:
    • EMAIL_TYPES = ['documento_subido', 'weekly_update', 'subscription_expiring', 'recordatorio_fiscal'].
    • getContribuyenteEmailPreferences() con default true.
    • setContribuyenteEmailPreferences() merge sobre JSONB.
  • apps/api/src/services/notify-upload.service.ts: check prefs.documento_subido antes de enviar.
  • apps/api/src/controllers/notification-preferences.controller.ts: listPreferences + updatePreferences.
  • apps/api/src/routes/notification-preferences.routes.ts: GET / PUT /api/notificaciones.
  • apps/web/app/(dashboard)/configuracion/notificaciones/page.tsx: toggles por tipo de email por contribuyente, optimistic update.

Tipos de email

Tipo Estado Notas
documento_subido activo check en notify-upload.service.ts
weekly_update próximamente el job es tenant-wide, requiere refactor
subscription_expiring próximamente no es per-contribuyente hoy
recordatorio_fiscal placeholder para futuras alertas fiscales

Los toggles "Próximamente" se persisten pero no bloquean el envío todavía. Cuando se implemente cada email per-contribuyente, basta con agregar el check.

Default

Todo activado. Si la columna está vacía o le falta una key, asumimos true para preservar comportamiento previo.


3. Tareas operativas (recurrentes)

Sistema completo de tareas operativas del despacho por contribuyente, distinto del flujo de obligaciones fiscales (que ya existía).

Características

  • Recurrencias: semanal, quincenal, mensual, bimestral, trimestral, semestral, anual.
  • Día de vencimiento: día de la semana (1-7) o día del mes (1-31) según recurrencia.
  • Estados: pendiente / completada (en BD); "atrasada" se calcula en frontend.
  • Permisos:
    • Rol cliente: bloqueado en TODOS los endpoints (403).
    • Flag solo_supervisor_completa: si está en true, solo owner/cfo/supervisor pueden marcarla completa.

Archivos

  • Migración 035:
    tareas_catalogo (id, contribuyente_id, nombre, descripcion, recurrencia,
                     dia_semana, dia_mes, solo_supervisor_completa, es_default,
                     active, orden, created_at)
    tarea_periodos (id, tarea_id, periodo, fecha_limite, completada,
                    completada_at, completada_por, notas, created_at)
    
  • apps/api/src/services/tareas.service.ts:
    • CRUD del catálogo.
    • Materialización lazy: cuando el frontend pide tareas con periodo actual, el backend genera periodos faltantes solo del presente en adelante (códigos 2025-W12, 2025-01, 2025-B1, 2025-Q1, 2025-S1, 2025).
    • seedTareasDefault() con 4 tareas: estados de cuenta (día 5), conciliación (día 10), contabilización (día 14), revisión fiscal preliminar (día 15, solo_supervisor_completa=true).
    • getEventosTareasParaCalendario() retorna shape EventoFiscal con tipo: 'tarea'.
    • contarTareasProximasVencer() para alertas auto.
    • getAuxiliarUserIdDeContribuyente() resuelve cartera → auxiliar.
  • apps/api/src/controllers/tareas.controller.ts:
    • Bloqueo rol cliente.
    • notifyAuxiliarTareaCompletada(): dispara email cuando una tarea con solo_supervisor_completa=true se marca completa por supervisor/owner — el auxiliar recibe aviso.
  • apps/api/src/routes/tareas.routes.ts:
    GET    /api/tareas              — list con periodos materializados
    POST   /api/tareas              — crear
    PATCH  /api/tareas/:id          — update
    DELETE /api/tareas/:id          — soft delete (active=false)
    POST   /api/tareas/seed         — seed defaults
    POST   /api/tareas/periodo/:id/completar
    DELETE /api/tareas/periodo/:id/completar
    
  • apps/api/src/services/email/templates/tarea-completada.ts: template para notificar al auxiliar.
  • apps/api/src/services/alertas-auto.service.ts: alertaTareasProximasVencer() (≤3 días, prioridad media), enchufada en generarAlertasAutomaticas.
  • apps/api/src/controllers/calendario.controller.ts: incluye tareas en el GET de eventos cuando hay contribuyente seleccionado y rol no es cliente.
  • packages/shared/src/types/calendario.ts: agregado 'tarea' a TipoEvento.

Frontend

  • apps/web/components/obligaciones/tareas-tab.tsx: pestaña "Tareas" en /configuracion/obligaciones con tabla, modal crear/editar, check pendiente/completada.
  • apps/web/app/(dashboard)/configuracion/obligaciones/page.tsx: tabs "Obligaciones" / "Tareas".

Flujo email "tarea revisada"

  1. Auxiliar trabaja en algo del contribuyente.
  2. Supervisor/owner marca como completada la tarea solo_supervisor_completa=true (ej. "Revisión fiscal preliminar").
  3. Backend resuelve auxiliar_user_id desde la cartera del contribuyente.
  4. Email al auxiliar con el detalle (quién marcó, periodo, notas).

4. Papelería de Trabajo

Sección nueva en /documentos para subir archivos de trabajo del despacho con flujo opcional de aprobación.

Características

  • No accesible para usuarios rol cliente (oculta + 403 en API).
  • Formatos permitidos: PDF, Word (doc/docx), Excel (xls/xlsx).
  • Tamaño máximo: 5 MB por archivo (validado backend + frontend).
  • Periodo: mes + año (selector en el modal de subida).
  • Aprobación opcional: checkbox al subir; si está activo, queda en estado pendiente y solo owner/supervisor pueden aprobar/rechazar.
  • Comentario opcional al rechazar.

Archivos

  • Migración 036:
    papeleria_trabajo (id, contribuyente_id, nombre, descripcion,
                       archivo bytea, archivo_filename, archivo_mime,
                       archivo_size, anio, mes,
                       requiere_aprobacion, estado, aprobado_por,
                       aprobado_at, comentario_rechazo, subido_por,
                       created_at)
    
    Estados válidos: pendiente | aprobado | rechazado. NULL si no requiere aprobación.
  • apps/api/src/services/papeleria.service.ts:
    • Validación MIME + tamaño.
    • CRUD: uploadPapeleria, listPapeleria (filtros año/mes/estado), downloadArchivo, aprobar, rechazar, eliminar.
    • aprobar/rechazar validan rol owner/cfo/supervisor.
  • apps/api/src/controllers/papeleria.controller.ts:
    • Bloqueo rol cliente con rejectClienteRole().
    • Schema Zod estricto.
    • Notificaciones:
      • Al subir con requiere_aprobacion=true: email a owners + supervisores.
      • Al aprobar/rechazar: email al uploader (excepto si es el mismo).
    • Excepción al uploader: si el aprobador es el mismo user que subió (ej. owner que sube su propia papelería), no se auto-notifica.
  • apps/api/src/services/email/templates/papeleria.ts: 2 templates (papeleriaAprobacionRequeridaEmail + papeleriaDecisionEmail).
  • apps/api/src/routes/papeleria.routes.ts:
    GET    /api/papeleria                  — list con filtros
    POST   /api/papeleria                  — upload (base64 en JSON body)
    GET    /api/papeleria/:id/download     — descarga binaria
    POST   /api/papeleria/:id/aprobar
    POST   /api/papeleria/:id/rechazar     — body: { comentario? }
    DELETE /api/papeleria/:id
    

Frontend

  • apps/web/components/documentos/papeleria-tab.tsx: pestaña con filtros (año, mes, estado), modal de upload con base64 conversion, badges de estado, modal de rechazo con comentario.
  • apps/web/app/(dashboard)/documentos/page.tsx: pestaña condicional if (user.role !== 'cliente').

5. Módulo "Despacho" — 3 páginas

Reemplazo del módulo /pendientes con sub-secciones específicas por rol. Sidebar renombrado: "Pendientes" → "Despacho".

Estructura

/despachos                           → redirect según rol
/despachos/contribuyentes            → owner/cfo (métricas globales)
/despachos/mis-asignados             → owner/cfo/supervisor/auxiliar
/despachos/equipo                    → owner/cfo/supervisor (jerárquico)

Sub-nav común

  • apps/web/components/despachos/despacho-subnav.tsx: tabs visibles según rol del user (filtrado en frontend).
  • defaultDespachoPathForRole(role): helper para el redirect en /despachos.

5a. /despachos/contribuyentes (owner-only)

7 cards de métricas globales del despacho, todas filtrables por periodo:

Card Fuente Notas
Total contribuyentes entidades_gestionadas activos independiente del periodo
Última extracción SAT MAX(sat_sync_jobs.completed_at) BD central independiente del periodo
Progreso del mes (%) obligaciones+tareas completadas / total barra de color
Declaraciones presentadas declaraciones_provisionales con created_at en periodo
Declaraciones pagadas subset con pdf_pago IS NOT NULL
Declaraciones atrasadas obligaciones de declaración pendientes con periodo < seleccionado heurística por categoría
Tareas atrasadas tareas pendientes con fecha_limite < primer día

Heurística "es declaración" (sin flag explícito en el catálogo):

LOWER(categoria) ~ 'mensual|anual|declarac' OR LOWER(nombre) LIKE '%declarac%'

Si crece la base se puede agregar columna es_declaracion boolean y ajustar.

5b. /despachos/mis-asignados

Tabla con contribuyentes filtrada por cartera del user:

Rol Filtro
owner / cfo TODOS los contribuyentes
supervisor contribuyentes en sus carteras top-level + subcarteras
auxiliar contribuyentes en carteras donde es auxiliar_user_id

Columnas:

  • Contribuyente (link → /configuracion/obligaciones con contribuyente preseleccionado).
  • Cartera.
  • Avance (barra de color verde≥80% / ámbar 50-79% / rojo <50% + porcentaje numérico).
  • Atrasos — badge agregado de obligaciones+tareas atrasadas; "Al día" si no tiene.
  • Obl. periodo (completadas / pendientes).
  • Tareas periodo (completadas / pendientes).

Resaltado de filas con atrasos: bg-red-50/50 para llamar la atención.

Orden: por suma de atrasos descendente (rezagados arriba).

5c. /despachos/equipo

Vista jerárquica supervisor → auxiliares. Click en un supervisor expande debajo a sus auxiliares.

Resolución de la relación supervisor → auxiliar (UNION de 3 fuentes):

1. carteras top-level con auxiliar_user_id + supervisor_user_id directos
2. subcarteras con auxiliar_user_id; supervisor del cartera padre
3. tabla legacy auxiliar_supervisores (fallback / override desde /usuarios)

Sección "Auxiliares sin supervisor asignado": si después del UNION queda un auxiliar activo (rol auxiliar en tenant_memberships) sin supervisor, aparece en una sección al final con icono de advertencia ámbar — solo el owner la ve.

Permisos:

  • owner/cfo: ve TODOS los supervisores con sus auxiliares + huérfanos.
  • supervisor: ve solo a sí mismo con sus auxiliares (sin huérfanos).

Métricas por miembro:

  • Contribuyentes asignados.
  • Avance del periodo (barra de color, igual que mis-asignados).
  • Atrasos (obligaciones + tareas).

Backend

  • apps/api/src/services/despacho-stats.service.ts:
    • getContribuyentesStats(pool, tenantId, año?, mes?).
    • getMisAsignados(pool, userId, userRole, año?, mes?).
    • getEquipoStats(pool, userId, userRole, tenantId, año?, mes?).
  • apps/api/src/controllers/despacho-stats.controller.ts: 3 endpoints con guards de rol.
  • apps/api/src/routes/despacho-stats.routes.ts: mountea en /api/despachos.

6. Selector de periodo global

Problema

Antes solo el mes en curso era visible. El user quiere navegar a otros periodos para revisar avance histórico.

Solución

Wrapper <PeriodoSelector /> alrededor del <PeriodSelector /> de @horux/shared-ui (mismo patrón visual que /dashboard, /impuestos, /reportes). Persiste selección en localStorage via Zustand.

Archivos

  • apps/web/stores/periodo-store.ts:
    • fechaInicio / fechaFin (strings ISO YYYY-MM-DD).
    • Default: primer y último día del mes en curso.
    • Helper añoMesFromFechaInicio() para queries que usan año/mes.
  • apps/web/components/periodo-selector.tsx: wrapper que se pasa como children al <Header> en cada página /despachos/*.

Endpoints que aceptan el filtro

  • GET /api/despachos/contribuyentes-stats?año=YYYY&mes=MM
  • GET /api/despachos/mis-asignados?año=YYYY&mes=MM
  • GET /api/despachos/equipo-stats?año=YYYY&mes=MM

Default: mes en curso si no vienen los params.


7. Asignación de supervisor desde /usuarios

Problema

Al invitar un auxiliar se pedía supervisor, pero no había forma de cambiarlo después. Al abrir el modal aparecía "none" porque el endpoint solo leía de auxiliar_supervisores ignorando carteras.

Solución

Endpoint que consulta TODAS las fuentes de la relación supervisor↔auxiliar y un modal "Supervisor" en /usuarios.

Archivos backend

  • apps/api/src/controllers/usuarios.controller.ts:
    • getSupervisor: UNION priorizada — auxiliar_supervisores (1) → cartera directa (2) → cartera padre de subcartera (3). El primer resultado gana.
    • updateSupervisor: upsert en auxiliar_supervisores (override explícito sobre la cartera). Pasar supervisorUserId: null borra la asignación.
  • apps/api/src/routes/usuarios.routes.ts:
    GET /api/usuarios/:id/supervisor
    PUT /api/usuarios/:id/supervisor
    

Frontend

  • Botón "Supervisor" (icono UserCheck) en cada fila de auxiliar, visible solo en tenant tipo despacho y para admins.
  • Modal con <Select> de supervisores + opción "Sin supervisor asignado".

Trade-off

La tabla auxiliar_supervisores actúa como override sobre la cartera. Si después de asignar via cartera, el owner cambia el supervisor desde /usuarios, queda guardado en auxiliar_supervisores y eso prevalece. Si quieres revertir, borrar la fila desde /usuarios (opción "Sin supervisor asignado").


8. Ajustes de UI

8a. Sidebar sin selector de contribuyente

El <ContribuyenteSelector /> ya estaba en el header — duplicado en sidebar agregaba ruido. Removido del sidebar (apps/web/components/layouts/sidebar.tsx).

8b. Selector contribuyente oculto en /despachos/*

Las 3 vistas de Despacho son cross-contribuyente (métricas agregadas, mis asignados es la lista completa, equipo es jerarquía). Filtrar por contribuyente individual rompe el propósito.

apps/web/components/contribuyente-selector.tsx: array HIDDEN_PATHS con /despachos. Si el pathname matchea, retorna null antes de renderizar.

8c. Header

Selector de periodo se pasa como children del <Header> (al lado del título), igual que en /dashboard — no en el lado derecho.

8d. PageNotificaciones centrada

<main className="... mx-auto"> agregado.

8e. Configuración Obligaciones Fiscales — Header

Faltaba el <Header /> en la página /configuracion/obligaciones así que perdía el selector global. Agregado.


9. Migraciones aplicadas

# Archivo Tabla / cambio
033 033_facturapi_orgs_lco_rejection.sql facturapi_orgs.last_lco_rejection_at
034 034_contribuyentes_email_preferences.sql contribuyentes.email_preferences jsonb
035 035_tareas.sql tareas_catalogo + tarea_periodos
036 036_papeleria_trabajo.sql papeleria_trabajo

Aplicadas vía pnpm db:migrate-tenants en ambos tenants (DESPACHO_MO7JE8BZ_VDOPR y DESPACHO_MO3NI6U8_B9VGG).


10. Pendientes

Tareas

  • Soporte de tipo "one-time" (no recurrente). Hoy todas son recurrentes.
  • Asignación explícita de tarea a un user específico (hoy se asume cartera).

Notificaciones

  • Implementar el envío per-contribuyente para weekly_update, subscription_expiring, recordatorio_fiscal (los 3 toggles "Próximamente"). Requiere refactor del cron de weekly-update para que itere por contribuyente en lugar de por tenant.

Métricas Despacho

  • "Es declaración" en obligaciones: agregar flag explícito al catálogo (obligaciones_contribuyente.es_declaracion boolean) y migrar la heurística actual de regex.
  • Drill-down: click en cada card del módulo Contribuyentes lleva a una vista detallada (ej. listado de declaraciones atrasadas con contribuyente y periodo).

Equipo

  • Mostrar % avance histórico (gráfica) de cada miembro a lo largo de varios periodos.
  • Asignar manualmente contribuyentes a un user desde la vista de equipo (hoy solo via carteras).

Papelería

  • Versionado: si el user sube un nuevo archivo del mismo "concepto", guardar histórico de versiones (hoy crea fila independiente cada vez).
  • Filtros por uploader.

Bugs reportados (por revisar)

  • Visibilidad de auxiliares en carteras de supervisores: en la pantalla de carteras, el supervisor está viendo a auxiliares que NO están asignados a él. Debe filtrar para que cada supervisor solo vea los auxiliares en sus propias carteras (top-level + subcarteras donde es supervisor).
  • Tareas completadas > pendientes en módulo Despacho: las métricas muestran más tareas completadas de las que existen pendientes — aritméticamente raro. Posible causa: el conteo de "completadas" incluye periodos históricos mientras que "pendientes" se limita al periodo seleccionado. Validar que ambos lados del ratio usen el mismo universo (mes filtrado) o documentar explícitamente la asimetría si es intencional.
  • Drill-down de alerta TipoRelacion sospechoso incompleto: no están saliendo todos los CFDIs tipo E con posible TipoRelacion errónea en el listado. Revisar la heurística en apps/api/src/services/alertas-auto.service.ts (constante SOSPECHOSA_TIPO_RELACION_WHERE y reuso en getCfdisTipoRelacionSospechosa del controller). Posibles causas: (a) operador && de PostgreSQL sobre string_to_array no detecta algunos casos por case-sensitivity o caracteres extraños en cfdis_relacionados; (b) el filtro excluye E con cfdi_tipo_relacion=NULL cuando algunas inconsistencias podrían tener ese estado; (c) RFC del contribuyente no se aplica uniformemente en el JOIN.