PRD — Coveris
PRD — Coveris
Sección titulada «PRD — Coveris»Versión: 0.1 · Estado: Draft · Stack: Angular 21 + Django 5.2 DRF + PostgreSQL 16 (AWS Amplify / App Runner / RDS)
[!info] Fuentes canónicas Este PRD define el qué y el porqué. El detalle operativo vive en los documentos enlazados: contratos HTTP → [[API]] · convenciones de API → [[api-conventions|stack/api-conventions]] · motor de reglas → [[business-rules|stack/business-rules]] · stack y versiones → [[overview|stack/overview]] · sistema de diseño → [[design-system|stack/design-system]] · datos semilla → [[seed-tags|dev/seed-tags]] · migración de contratos → [[MIGRATION-PLAN]].
1. Problema
Sección titulada «1. Problema»Coveris es un SaaS de Capacity Planning para clínicas privadas. Eso define todo lo que sigue.
Una clínica privada mediana — 20 a 100 empleados, 3 a 15 servicios — gestiona su dotación con planillas. Cuando una guardia queda descubierta, la gerenta de recursos humanos de la clínica “Bienestar” (nombre ficticio, referencia a lo largo de este documento) necesita responder cuatro preguntas en tiempo real:
- ¿Quién tiene horas disponibles para cubrir esa guardia?
- ¿Alguno de esos tiene las certificaciones que exige la unidad?
- Si asigno a esa persona, ¿entra en horas extra? ¿por cuántas?
- Después de la asignación, ¿cómo queda la cobertura general del servicio?
Hoy esas preguntas se responden abriendo tres planillas distintas, llamando a dos jefes de servicio y revisando una carpeta de certificaciones. El proceso tarda horas. Y la respuesta está desactualizada porque la planilla fue editada sin avisar.
El problema no es la falta de datos — la clínica tiene los datos: contratos, regímenes horarios, reducciones, licencias. El problema es que esos datos viven fragmentados. No existe una única fuente de verdad que mida en tiempo real la brecha entre la demanda operativa (los puestos que cada servicio necesita cubrir) y la oferta real (la dotación disponible tras aplicar contratos y reducciones).
Coveris resuelve ese problema. Es agnóstico de institución — no asume un organigrama fijo ni reglas de ningún convenio colectivo. Cada clínica configura su propia estructura; aquí usamos “Bienestar” y todas sus áreas como referencia.
2. Visión
Sección titulada «2. Visión»Coveris pone a las personas en el centro de la clínica. Cada Employee tiene un contrato, un régimen horario, un estado vital, y un lugar en la estructura. Coveris los mantiene vivos, actualizados, y conectados.
Sobre esas personas, Coveris construye una estructura jerárquica — el mapa real de la clínica: sus servicios, sus unidades, sus puestos. Un organigrama que no es decorativo sino operativo, donde cada nodo sabe cuántas horas necesita y cuántas tiene cubiertas en tiempo real.
El sistema cierra el ciclo con una FSM sólida de asignaciones: cada persona puede asignarse a uno o más puestos, con un preview del impacto en horas antes de confirmar. El balance obligated − assigned es la métrica central — positivo significa capacidad disponible (DEFICIT), negativo significa overtime (SURPLUS).
Los usuarios del sistema tienen roles diferenciados: MANAGER opera con escritura total; SUPERVISOR tiene visibilidad amplia; VIEWER acceso restringido; EMPLOYEE ve exclusivamente su propia información. El rol ADMIN existe por razones técnicas y nunca se entrega como parte del producto.
3. Scope
Sección titulada «3. Scope»In-Scope (MVP)
Sección titulada «In-Scope (MVP)»El MVP cubre autenticación con roles, gestión completa de Employee (ciclo de vida, tags tipados para contratos/certificaciones/reducciones), estructura jerárquica configurable de OrgUnit, definición de Position por unidad con requerimientos de tags, asignaciones con preview de impacto y validación de elegibilidad, y cálculo en tiempo real de horas y cobertura. Todo funciona end-to-end en entorno local y es desplegable manualmente en producción.
Out-of-Scope
Sección titulada «Out-of-Scope»El modelo de datos y los nombres de entidades, campos y endpoints se definen considerando estas áreas desde el primer día — aunque su implementación sea posterior. YAGNI en código; no en arquitectura.
- Despliegue cloud y auth de producción
- Roles avanzados y permisos granulares
- Scheduling y calendarios cíclicos
Skills y certificaciones con vencimiento→ In-Scope via ADR-019 (Tag-Based Requirements)- Notificaciones hacia el empleado
- Audit trail completo
- Importación masiva avanzada
4. Arquitectura
Sección titulada «4. Arquitectura»Coveris tiene dos entornos: dev y prod. Todo el trabajo de este MVP ocurre en dev. Prod está definido — AWS Amplify para el frontend, App Runner para el backend, RDS para la base de datos — y ese destino ya orienta decisiones de diseño hoy.
La regla es simple: en dev se usa la implementación más liviana posible; en prod entra el servicio AWS equivalente. El modelo de datos no cambia entre entornos. Los settings de entorno sí.
Un ejemplo concreto: el modelo User tiene un campo sub de tipo UUID. En dev, la autenticación la resuelve el propio backend con tokens locales. En prod, AWS Cognito emite tokens JWT que incluyen un claim sub — el mismo identificador. El modelo no necesita ninguna modificación al migrar: el campo ya está, con el nombre correcto, desde el primer commit. Así se piensa en este proyecto — no se implementa Cognito ahora, pero se nombra sub porque Cognito lo va a necesitar.
Este principio aplica a cualquier entidad, campo o endpoint que se diseñe: si hay un servicio AWS definido para cubrirlo en prod, el nombre y la estructura se eligen con ese futuro en mente.
[!note] Detalle operativo Versiones del stack → [[overview|stack/overview]] · variables de entorno → [[dev/environment]] (dev) y [[prod/environment]] (prod) · servicios AWS → [[aws|prod/aws]].
5. Domain Model
Sección titulada «5. Domain Model»Coveris opera con tres dominios: demanda, oferta y puente.
5.1 Demanda
Sección titulada «5.1 Demanda»Unidad Organizativa — La clínica se organiza en un árbol jerárquico de cuatro niveles (Clínica → Departamento → Servicio → Unidad). Solo las unidades operativas (hojas) contienen puestos de trabajo. The OrgChart view may render leaf UNITs that represent a single management role as inline cards rather than tree branches — a presentational distinction only, with no impact on the data model (see ADR-018). Cada nodo tiene un código jerárquico único (ej: BIE-MED-GUA) y puede activarse o desactivarse.
Puesto — Declaración atómica de demanda: “Esta unidad necesita X horas por semana de este rol.” Un puesto define un título, las horas semanales requeridas (default: 36.00 h/semana), y opcionalmente un conjunto de tags requeridos que el empleado asignado debe poseer (ver ADR-019). Si el puesto no declara tags, cualquier empleado con horas disponibles es elegible. Si declara tags, el motor de reglas valida elegibilidad al momento de la asignación. Es el umbral contra el cual se mide la cobertura.
La cobertura de un puesto tiene cuatro estados: vacante (sin asignaciones), parcial (cobertura incompleta), cubierto (horas cubiertas al 100%), o excedente (más horas asignadas de las requeridas).
5.2 Oferta
Sección titulada «5.2 Oferta»Empleado — Persona con un ciclo de vida de 6 estados gestionado por una FSM (django-fsm). La desactivación es reversible (permite reactivar sin perder datos), a diferencia de la desvinculación que es un estado terminal solo reversible mediante recontratación (rehire cycle).
5.2.1 Estados
Sección titulada «5.2.1 Estados»| Estado | Nombre canónico | Descripción |
|---|---|---|
| Ingreso | ONBOARDING | Estado inicial. Empleado registrado, pendiente de activación. |
| Actividad | ACTIVE | Operativo. Puede recibir asignaciones y propuestas. |
| Propuesta pendiente | PROPOSAL_PENDING | Tiene una propuesta abierta (asignación o transferencia) esperando resolución. Preserva el estado previo (previous_status) para retornar a él tras resolución. |
| Licencia | ON_LEAVE | En licencia aprobada. Reversible. Puede recibir propuestas. |
| Desactivado | DEACTIVATED | Suspendido temporalmente. No pierde datos. Solo puede reactivarse o desvincularse. |
| Desvinculado | TERMINATED | Contrato finalizado. Solo reversible mediante recontratación, que inicia un nuevo ciclo desde ONBOARDING. |
5.2.1a Operaciones permitidas durante ONBOARDING
Sección titulada «5.2.1a Operaciones permitidas durante ONBOARDING»- Asignación de tags durante Inducción: Durante ONBOARDING, se asignan los tags iniciales del empleado: tag de contrato (ej:
Insalubre), calificaciones profesionales (ej:Médico,Enfermero), y certificaciones vigentes (ej:ACLS). Toda mutación de tags genera un registro inmutable enTagChangeLog(ADR-019 §11) con acción, actor, motivo y timestamp.
5.2.2 Transiciones (12)
Sección titulada «5.2.2 Transiciones (12)»| # | Transición | Desde | Hacia | Actor | Guard | Side Effects |
|---|---|---|---|---|---|---|
| 1 | activate | ONBOARDING | ACTIVE | ADMIN | — | — |
| 2 | propose | ACTIVE, ON_LEAVE | PROPOSAL_PENDING | MANAGER | — | Guarda previous_status, pending_proposal_type, pending_proposal_notes, proposal_expires_at |
| 3 | accept_proposal | PROPOSAL_PENDING | (previous) | EMPLOYEE | — | Limpia campos de propuesta |
| 4 | reject_proposal | PROPOSAL_PENDING | (previous) | EMPLOYEE | — | Limpia campos de propuesta |
| 5 | cancel_proposal | PROPOSAL_PENDING | (previous) | MANAGER | — | Limpia campos de propuesta |
| 6 | expire_proposal | PROPOSAL_PENDING | (previous) | SYSTEM | — | Limpia campos de propuesta |
| 7 | go_on_leave | ACTIVE | ON_LEAVE | EMPLOYEE | — | Setea leave_started_at |
| 8 | return_from_leave | ON_LEAVE | ACTIVE | EMPLOYEE | — | Limpia leave_started_at |
| 9 | deactivate | ACTIVE | DEACTIVATED | ADMIN | can_deactivate(): sin asignaciones activas (§7.4) | — |
| 10 | reactivate | DEACTIVATED | ACTIVE | ADMIN | — | — |
| 11 | terminate | ACTIVE, ON_LEAVE, DEACTIVATED | TERMINATED | ADMIN | — | Setea termination_date |
| 12 | rehire | TERMINATED | ONBOARDING | ADMIN | — | Limpia termination_date, leave_started_at, campos de propuesta |
(previous) = retorna al previous_status guardado al momento del propose (ACTIVE o ON_LEAVE). Usa RETURN_VALUE() de django-fsm.
force_accept_proposal: solo un superusuario puede forzar la aceptación de una propuesta en nombre de un EMPLOYEE. El log de auditoría registra forced: true para distinguirlo de una aceptación normal.
expire_proposal es la única transición no expuesta como endpoint HTTP — la ejecuta el management command expire_proposals (cron/sistema).
proposal_type enum: ASSIGNMENT, TRANSFER. (SCHEDULE_CHANGE declarado en API.md como forward-declared, no implementado en MVP.)
[!note] Endpoints HTTP de las 12 transiciones (actor, body, códigos de estado): [[API#Employee FSM Transitions]].
5.2.3 Diagrama de estados
Sección titulada «5.2.3 Diagrama de estados» ┌─────────────────────────────────────┐ │ PROPOSAL_PENDING │ │ (preserva previous_status) │ └──┬────┬────┬────┬───────────────────┘ propose │ │ │ │ accept / reject / ┌──────────┘ │ │ └──cancel / expire │ │ │ │ ▼ │ │ ▼ ┌────────┐ activate ┌───────┴┐ │ ┌────────┐ │ONBOARD-│─────────→│ ACTIVE │◄──┘ │ON_LEAVE│ │ ING │ │ │◄──────►│ │ └────────┘ └───┬──┬─┘ └───┬────┘ ▲ │ │ │ │ rehire │ │ go_on_leave /│ │ │ │ return_from_ │ │ │ │ leave │ │ deactivate │ │ │ │ ▼ │ │ │ ┌──────────┐ │ │ │DEACTIVAT-│ │ │ │ ED │ │ │ └─────┬────┘ │ │ │ │ │ terminate │ terminate │ terminate │ (from any) ▼ │ │ ┌──────────┐ │ └───────────│TERMINATED│◄────────────┘ └──────────┘5.2.4 Audit trail
Sección titulada «5.2.4 Audit trail»Toda transición genera un EmployeeTransitionLog inmutable (§7.5) con: from_status, to_status, transition, actor, reason, metadata (JSON), created_at. Ver ADR-017 para la arquitectura de separación entre dominio FSM y API de consulta.
Tags — Cada atributo de un empleado se representa como un tag tipado de un catálogo centralizado (TagCatalog). Los tags tienen una categoría (CONTRACT, QUALIFICATION, EXCEPTION, CERTIFICATION) y un hours_delta semanal (positivo = suma al pool, negativo = reduce, cero = calificación pura). Ver ADR-019.
Un empleado puede tener múltiples tags activos simultáneamente, incluso del mismo tipo — stacking es una feature: dos tags “Guardia 12h” (+12 h/semana cada uno) suman 24 h/semana al pool. Las horas son fungibles — las asignaciones consumen del pool total, sin vincular a un tag específico.
El pool efectivo de horas se calcula como: max(0, Σ(hours_delta de tags activos)). Sin pipeline de decoradores, sin prioridades. Tags son conmutativos.
Ejemplos de tags:
Insalubre(CONTRACT, +36.00) — jornada insalubreSindicalizado(EXCEPTION, −12.00) — reducción por afiliación gremialMédico(QUALIFICATION, 0.00) — calificación profesionalACLS(CERTIFICATION, 0.00) — certificación vigente
Nota de migración: Los tags reemplazan ContractTemplate, ContractInstance y ContractAdjustment; los datos existentes se migran a TagCatalog + EmployeeTag. Plan de implementación del módulo de tags en [[MIGRATION-PLAN]] (§B1).
5.3 Puente
Sección titulada «5.3 Puente»Asignación — Conecta un empleado con un puesto: “El empleado X cubre Y horas semanales del puesto Z.” Las asignaciones tienen un ciclo de vida de 3 estados gestionado por una FSM (django-fsm). Solo las asignaciones activas cuentan para cobertura y balance de horas.
5.3.1 Estados
Sección titulada «5.3.1 Estados»| Estado | Nombre canónico | Descripción |
|---|---|---|
| Activa | ACTIVE | Estado inicial (default al crear). La asignación contribuye a cobertura del puesto y consumo de horas del empleado. |
| Suspendida | SUSPENDED | Temporalmente removida de los cálculos de cobertura. Reversible vía resume. |
| Finalizada | ENDED | Estado terminal. La asignación dejó de existir operativamente. No reversible. |
5.3.2 Transiciones (4 aristas, 3 métodos)
Sección titulada «5.3.2 Transiciones (4 aristas, 3 métodos)»| # | Transición | Desde | Hacia | Body | Guard | Side Effects |
|---|---|---|---|---|---|---|
| 1 | suspend | ACTIVE | SUSPENDED | {reason?} | — | Setea suspended_at. Remueve la asignación del cálculo de cobertura del puesto y del balance del empleado. |
| 2 | resume | SUSPENDED | ACTIVE | {reason?} | — | Limpia suspended_at. Restaura la asignación al cálculo de cobertura y balance. |
| 3a | end | ACTIVE | ENDED | {end_reason?, notes?} | — | Setea end_date, end_reason. Remueve permanentemente de cobertura y balance. |
| 3b | end | SUSPENDED | ENDED | {end_reason?, notes?} | — | Setea end_date, end_reason. |
end_reason enum: RESIGNATION, TRANSFER, POSITION_CLOSED, TERMINATED, OTHER.
[!note] Endpoints HTTP (
suspend/resume/end): [[API#Assignment FSM Transitions]].
5.3.3 Guards de creación (no FSM)
Sección titulada «5.3.3 Guards de creación (no FSM)»Las siguientes validaciones operan al crear la asignación, no como guards de transiciones FSM:
| Validación | Nivel | Detalle |
|---|---|---|
| Estado del empleado | Serializer | Solo empleados ACTIVE u ON_LEAVE pueden recibir asignaciones. ONBOARDING, DEACTIVATED, TERMINATED y PROPOSAL_PENDING son rechazados con 400. |
| Unicidad activa | DB constraint | Una sola asignación ACTIVE por par (employee, position). Constraint: unique_active_assignment_per_employee_position. |
| Horas positivas | Serializer | effective_hours > 0 (mínimo 0.01). |
| Reglas de negocio | Preview endpoint | POST /api/v1/assignments/preview/ evalúa reglas habilitadas (§5.5) sin persistir — e.g., MAX_WEEKLY_HOURS, DUPLICATE_ASSIGNMENT. Contrato: [[API#Assignment Endpoints (Asignaciones)]]. |
5.3.4 Impacto en cobertura y balance
Sección titulada «5.3.4 Impacto en cobertura y balance»Cada transición que cambia el estado de una asignación altera dos métricas computadas en tiempo real (§7.1):
| Transición | Cobertura del puesto | Balance del empleado |
|---|---|---|
| crear (→ ACTIVE) | +hours: cobertura sube | −hours: horas disponibles bajan |
suspend (ACTIVE → SUSPENDED) | −hours: cobertura baja | +hours: horas disponibles suben |
resume (SUSPENDED → ACTIVE) | +hours: cobertura sube | −hours: horas disponibles bajan |
end (→ ENDED) | −hours si venía de ACTIVE | +hours si venía de ACTIVE |
Las métricas nunca se persisten — se recalculan desde el estado actual de asignaciones activas (ADR-003, §7.1).
5.3.5 Diagrama de estados
Sección titulada «5.3.5 Diagrama de estados» suspend end ┌────────┐ ──────────→ ┌───────────┐ ──────→ ┌────────┐ │ ACTIVE │ │ SUSPENDED │ │ ENDED │ │(initial│ ←────────── │ │ │(termi- │ │ ) │ resume └───────────┘ │ nal) │ └───┬────┘ └────────┘ │ end ▲ └────────────────────────────────────────────┘5.3.6 Eliminación
Sección titulada «5.3.6 Eliminación»Las asignaciones no se eliminan (§7.4). DELETE retorna 405 Method Not Allowed. El flujo correcto es end() → la asignación queda en ENDED con end_date y end_reason para auditoría.
5.3.7 Audit trail
Sección titulada «5.3.7 Audit trail»Toda transición genera un AssignmentTransitionLog inmutable (§7.5) con: from_status, to_status, transition, actor, reason, metadata (JSON), created_at. El signal post_transition captura automáticamente cada cambio de estado. Ver ADR-017.
5.4 La Ecuación Central
Sección titulada «5.4 La Ecuación Central»El sistema responde a una pregunta constante: ¿la oferta cubre la demanda?
- Demanda = horas semanales que cada puesto requiere
- Oferta = horas base del contrato, menos la suma de ajustes vigentes
- Cobertura = proporción de horas asignadas sobre horas requeridas por puesto
- Balance = horas disponibles del empleado menos horas ya asignadas. Positivo = capacidad disponible; negativo = overtime
El balance y la cobertura deben reflejar el estado actual en tiempo real — nunca datos históricos precalculados.
5.5 Reglas de Negocio
Sección titulada «5.5 Reglas de Negocio»Un motor de validación transversal evalúa toda operación que modifique capacidad — no solo asignaciones, también contratos, ajustes y cobertura. Es un componente de primer nivel: dominio propio, endpoint consultable por el frontend, y configurable por administradores. Cada regla tiene tres severidades — BLOCKING (impide), WARNING (advierte pero permite), INFO (registra).
Reglas mínimas del MVP: MAX_WEEKLY_HOURS, DUPLICATE_ASSIGNMENT, EMPLOYEE_TERMINATED, COVERAGE_EXCEEDED (warning), MAX_CONSECUTIVE_SHIFTS (warning), CONTRACT_NEAR_EXPIRY (info). Se siembran al desplegar, se configuran en runtime (habilitar/deshabilitar, severidad, umbral) y se evalúan en cada operación relevante — agregar una regla no requiere tocar código.
[!info] Canónico Severidades, umbrales, evaluadores y arquitectura del motor: [[business-rules|stack/business-rules]].
6. Modules
Sección titulada «6. Modules»Seis módulos en orden de dependencia. Cada módulo requiere que los anteriores existan.
6.1 Organigrama
Sección titulada «6.1 Organigrama»El núcleo del sistema. Representa visualmente la estructura jerárquica de la clínica con indicadores de cobertura por nodo. Ofrece dos perspectivas: una vista de árbol visual y una vista tabular con búsqueda y filtros. Sin organigrama no hay “dónde” para nada.
6.2 Nómina (Roster)
Sección titulada «6.2 Nómina (Roster)»Registro completo de empleados. Permite buscar, filtrar por estado de ciclo de vida, ver detalle de contrato y balance de horas, ejecutar transiciones de estado, y consultar historial. Referencia Bienestar: 35 empleados.
6.3 Estructura + Puestos
Sección titulada «6.3 Estructura + Puestos»Vista de la demanda operativa. Muestra la jerarquía organizacional con métricas de cobertura agregadas por nodo. Permite inspeccionar los puestos de cada unidad operativa con su estado de cobertura, horas requeridas y asignaciones. Referencia Bienestar: 46 puestos en 11 unidades.
Conveniencia: Duplicar Puesto — clona un puesto dentro de su misma unidad: copia título, horas y tags requeridos; no copia asignaciones; el clon recibe el sufijo “(copy)”. Contrato: [[API#Demand — Position Endpoints (Puestos)]].
6.4 Asignaciones
Sección titulada «6.4 Asignaciones»Puente entre oferta y demanda. El usuario selecciona empleado y puesto, define las horas, y ve un preview del impacto en ambos lados (balance del empleado y cobertura del puesto) antes de confirmar. Las asignaciones pueden suspenderse temporalmente o finalizarse con motivo.
6.5 Horas (Hours Ledger)
Sección titulada «6.5 Horas (Hours Ledger)»Cálculo en tiempo real del balance de horas por empleado. Toma las horas base del contrato, aplica los ajustes vigentes, resta las horas asignadas, y produce un balance con estado (disponible, balanceado, excedente). Nunca persiste resultados — siempre recalcula desde la fuente.
6.5.1 Convención de colores para horas
Sección titulada «6.5.1 Convención de colores para horas»El equipo usa una convención de colores como vocabulario compartido para describir la naturaleza de las horas en dashboards, conversaciones y documentación:
- Horas verdes (green hours) — Horas de contrato que hacen match exacto con un puesto. Cobertura completa, sin exceso, sin déficit.
- Horas rojas (red hours) — Horas no cumplidas. Típicamente causadas por vacantes o puestos que no pudieron cubrirse. No es idéntico a “vacante” pero la vacante suele ser el factor principal.
- Horas amarillas (yellow hours) — Horas de un puesto cubierto parcialmente. Existen asignaciones pero no alcanzan el 100%.
- Horas azules (blue hours) — Horas extras adicionadas más allá del requerimiento base del puesto — refuerzos. Usa un mecanismo análogo al slot de Vacante (slot de “Refuerzo”).
- Horas grises (grey hours) — Fallback/default. Horas canceladas o aún no definidas.
Coexistencia: Estos colores coexisten a nivel de unidad — una unidad puede tener simultáneamente horas amarillas, rojas y azules. Las horas verdes son excluyentes: solo aparecen cuando cada puesto individual está exactamente cubierto. Incluso si el agregado total llega al 100% por combinación de amarillas + azules, las horas se siguen contando por separado — verde solo aplica cuando no hay desviación en ningún puesto.
A nivel de empleado, verde significa que las horas de contrato están completamente consumidas por asignaciones sin exceso. Si alguna asignación es parcial o hay horas extra, las horas se desglosan en los colores correspondientes.
Azul ≠ todo OVER_COVERED: Las horas azules son exclusivamente refuerzos intencionales (slots de Refuerzo). Una posición puede estar en estado OVER_COVERED por sobre-asignación accidental — esas horas excedentes no son “azules”. Blue es un mecanismo de demanda, no un estado.
Desambiguación de “excedente”: El término aparece en tres contextos distintos que no deben confundirse:
- Posición excedente =
OVER_COVERED(>100% horas asignadas vs requeridas). - Empleado excedente =
SURPLUS(balance negativo: asignado > obligado). - Horas azules = refuerzos intencionales via slot de Refuerzo (mecanismo, no estado).
6.5.2 Reglas de totalización por colores
Sección titulada «6.5.2 Reglas de totalización por colores»Las siguientes reglas definen cómo se agregan y presentan los colores de horas a nivel de unidad y en el árbol del organigrama.
1. Totalización de horas por unidad. Cada unidad muestra un total que es la suma de las horas de sus puestos, desglosado por color:
- Horas verdes: todos los puestos tienen cobertura exacta al 100%. Verde es excluyente — si cualquier puesto se desvía, el verde desaparece por completo a nivel de unidad.
- Horas amarillas: horas de puestos cubiertos parcialmente (<100%).
- Horas azules: horas de refuerzo añadidas por encima del requerimiento base del puesto (slots de Refuerzo).
- Horas rojas: horas de puestos vacantes (0% de cobertura, sin asignaciones).
2. Coexistencia de colores en la barra de cobertura.
- Verde es excluyente: si está presente, no existen amarillo ni azul.
- Amarillo y azul pueden coexistir y son aditivos.
- Las horas rojas (vacantes) se muestran siempre de forma separada.
- En la barra de cobertura, el azul se renderiza siempre al final (posición más a la derecha).
3. Color del total agregado. El porcentaje total hereda el color dominante según las siguientes reglas, en orden de precedencia:
- Si todo verde → total verde.
- Si solo amarillo (sin azul) → total amarillo.
- Si solo azul (sin amarillo) → total azul.
- Si coexisten amarillo y azul:
- Cobertura total > 100% → total azul.
- Cobertura total < 100% → total amarillo.
4. Aplicación. Estas reglas aplican tanto a la vista de árbol del organigrama (/organigram) como a la vista de detalle de unidad (/organigram/:id). La barra de cobertura refleja la composición de colores; el porcentaje total hereda el color dominante según las reglas anteriores.
[!info] Implementación visual (barra de cobertura, tokens PrimeNG): [[design-system|stack/design-system]].
6.6 Cobertura (Coverage Dashboard)
Sección titulada «6.6 Cobertura (Coverage Dashboard)»Vista ejecutiva. Agrega la cobertura desde las unidades operativas hacia arriba: unidad → servicio → departamento → clínica. Muestra KPIs globales: total de puestos, cobertura promedio, deficit total en horas. Permite configurar umbrales de alerta por nivel organizacional.
7. Non-Functional Requirements
Sección titulada «7. Non-Functional Requirements»7.1 Cálculo en Tiempo Real
Sección titulada «7.1 Cálculo en Tiempo Real»El balance de horas y la cobertura nunca se almacenan como datos persistentes. Se computan en cada consulta desde el estado actual de contratos, ajustes y asignaciones. No hay datos stale.
7.2 Granularidad Semanal
Sección titulada «7.2 Granularidad Semanal»Toda computación de horas opera en semanas ISO (lunes a domingo). Las vistas mensuales son agregaciones de lectura, no unidades de cálculo.
7.3 Tope Legal
Sección titulada «7.3 Tope Legal»El sistema debe respetar un tope máximo de horas semanales por empleado (60h por defecto, contexto legal argentino). El tope debe ser configurable por clínica.
7.4 Eliminación No Destructiva
Sección titulada «7.4 Eliminación No Destructiva»Ninguna entidad se elimina permanentemente. Los registros desactivados conservan su historial. No se puede desactivar un puesto con asignaciones activas ni un empleado con transiciones pendientes.
Excepción GDPR (Art. 17 — derecho al olvido): DELETE /api/v1/auth/account/ es un soft-delete (is_active=False), no un borrado físico — coherente con esta regla: la cuenta se desactiva y se limpian las cookies, pero ningún dato se destruye. Detalle en §7.7; contrato HTTP en [[API#GDPR Endpoints]].
7.5 Trazabilidad
Sección titulada «7.5 Trazabilidad»Toda transición de estado (empleado y asignación) genera un registro inmutable con estado anterior, estado nuevo, actor, motivo y timestamp.
7.6 Precisión Numérica
Sección titulada «7.6 Precisión Numérica»Todas las horas se representan con precisión decimal fija. El pool de horas nunca puede ser negativo — se limita a cero como mínimo.
7.7 GDPR Compliance
Sección titulada «7.7 GDPR Compliance»Dos endpoints GDPR para usuarios autenticados (sesión por cookie httpOnly), bajo /api/v1/auth/:
- Right to Erasure (Art. 17) —
DELETE /api/v1/auth/account/: soft-delete (is_active=False), limpia cookies, conserva todos los datos para auditoría y recuperación (§7.4).204 No Content. - Right to Data Portability (Art. 20) —
GET /api/v1/auth/export/: descarga JSON conemail,given_name,family_name,role,date_joined,last_login. Sin material sensible (tokens, hashes).
[!info] Contratos HTTP completos (status, headers, payloads): [[API#GDPR Endpoints]].
8. Open Questions
Sección titulada «8. Open Questions»8.1 Regímenes de contrato — ✅ RESUELTA (2026-03-24, actualizada 2026-03-25)
Sección titulada «8.1 Regímenes de contrato — ✅ RESUELTA (2026-03-24, actualizada 2026-03-25)»Decisión: MVP usa TagCatalog.name libre con category tipada, sin campo regime_code enum. Los contratos, certificaciones, reducciones y calificaciones son todos tags (ADR-019).
Justificación: El modelo de tags unifica lo que antes eran ContractTemplate (nombres libres) con certificaciones y calificaciones profesionales bajo una sola abstracción. La category (CONTRACT, QUALIFICATION, EXCEPTION, CERTIFICATION) permite reglas de negocio específicas por grupo sin enums rígidos.
Catálogo inicial de tags (seed data): 23 entradas — 9 CONTRACT, 3 EXCEPTION, 9 QUALIFICATION, 2 CERTIFICATION. Catálogo completo con hours_delta, categoría y base legal en [[seed-tags|dev/seed-tags]]. Configurable por la clínica: agregar un tipo es insertar una fila — sin migración ni cambio de schema.
8.2 Códigos de razón de reducción — ✅ RESUELTA (2026-03-24)
Sección titulada «8.2 Códigos de razón de reducción — ✅ RESUELTA (2026-03-24)»Decisión: MVP usa ContractAdjustment.reason como texto libre, sin enum reason_code.
Justificación: ADR-016 ya provee estructura mediante ContractAdjustment.type enum (REDUCTION, ADDITIONAL_HOURS, TEMPORARY_CHANGE) — eso categoriza el ajuste. El reason es contexto humano para auditoría, no filtro del sistema. Un enum de razones (Circular 4, Art. 44, Gremial) sería específico de legislación pública y no generaliza a clínicas privadas. API.md ya define el schema correcto.
Nota: El modelo ContractAdjustment (y por tanto este campo reason) es supersedido por ADR-019. Ver §8.6.
8.3 Contexto legal argentino
Sección titulada «8.3 Contexto legal argentino»¿Debe documentarse el marco legal (LCT 20.744, Ley 9539, Circular 4) en un ADR dedicado, o basta con referenciar las leyes en las reglas de negocio?
8.4 Scheduling
Sección titulada «8.4 Scheduling»¿Cuándo entra la programación de turnos? Las posiciones declaran horas/semana pero no distribución diaria. ¿Scheduling es un módulo independiente o extensión de Asignaciones?
8.5 Skills y certificaciones — ✅ RESUELTA (2026-03-25)
Sección titulada «8.5 Skills y certificaciones — ✅ RESUELTA (2026-03-25)»Decisión: Sí. El MVP valida elegibilidad al momento de la asignación mediante tags tipados. Los puestos declaran tags requeridos (PositionTag). Los empleados poseen tags (EmployeeTag). La validación es por contención de conjuntos: employee_tags ⊇ position_required_tags.
Implementación: ADR-019 — Tag-Based Position Requirements and Employee Attributes. Tags unifican contratos, certificaciones, reducciones y calificaciones bajo una sola abstracción. La regla de negocio TAG_REQUIREMENT_MISMATCH es configurable por severidad (default BLOCKING, admin puede bajar a WARNING).
8.6 Reducciones especiales — ✅ RESUELTA (2026-03-24, actualizada 2026-03-25)
Sección titulada «8.6 Reducciones especiales — ✅ RESUELTA (2026-03-24, actualizada 2026-03-25)»Decisión: No se necesitan tipos de reducción específicos de clínica. Cada reducción es un tag con category=EXCEPTION y hours_delta negativo.
Justificación: Las reducciones conocidas (Circular 4 lactancia/discapacidad, Art. 44, Gremial, Licencia Sin Goce, horas extra autorizadas) se modelan como tags en TagCatalog con category=EXCEPTION + hours_delta negativo + dates en EmployeeTag. El modelo es extensible sin cambios de schema — agregar una nueva reducción es una fila en el catálogo. Nota: El modelo anterior (ContractAdjustment + pipeline de decoradores) es supersedido por el modelo de tags; implementación del módulo en [[MIGRATION-PLAN]] (§B1).