diff --git a/docs/auditorias/workplace-dashboard/01-inventario-funcional.md b/docs/auditorias/workplace-dashboard/01-inventario-funcional.md new file mode 100644 index 0000000..797b5d1 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/01-inventario-funcional.md @@ -0,0 +1,251 @@ +# Inventario funcional del puesto de trabajo + +Fecha: 22 de octubre de 2026. Auditoría estática del árbol de trabajo, incluidos sus cambios locales previos. Entrada a los entregables: [resumen ejecutivo](08-resumen-ejecutivo.md). + +## Alcance y evidencia + +La entidad implementada es **Workstation**, en el módulo Assets; «Workplace» es el nombre de la carpeta solicitado para esta auditoría, no otro agregado. Companies compone su ficha. El objetivo de diseño asumido es ayudar al técnico a identificar el puesto, entender su composición y consultar sus entornos de usuario. Esta hipótesis no sustituye investigación con usuarios. + +Etiquetas utilizadas en todos los documentos: + +- **CONFIRMADO**: inspeccionado en implementación; no implica ejecución ni comprobación de la BD instalada. +- **DERIVABLE**: necesita el recorrido o cálculo descrito sobre fuentes existentes. +- **DOCUMENTADO**: aparece en documentación, sin implementación confirmada para esa afirmación. +- **PROPUESTO**: decisión de presentación o desarrollo futuro. +- **NO DISPONIBLE**: no localizado en el ámbito y búsquedas declarados. + +La implementación se ha mantenido en solo lectura. No se ha consultado una BD ni iniciado la API: su arranque ejecuta migraciones. No se han creado usuarios ni fixtures persistentes. El JSON entregado es exclusivamente un ejemplo de diseño. La sintaxis y los contratos de ese ejemplo se verifican sin acceso a red ni BD; véase el registro de validaciones en [viabilidad](05-viabilidad-tecnica.md). + +## Tecnologías verificadas + +| Fuente | Declaración comprobada | +| --- | --- | +| `package.json` raíz | Node >=24 <25; PNPM 11.25.0. Entorno ejecutado: Node 24.20.0, PNPM 11.25.0 | +| `apps/web/package.json` | React 19.2.5, Vite 8.3.0, TanStack Query 5.98.0, Tailwind 4.2.2, i18next 26.0.4 | +| `apps/server/package.json` | Express 4.22.1, Sequelize 6.37.8, driver MariaDB 3.5.4, Zod 4.3.6 | +| `apps/web/components.json` | `base-vega`, RSC desactivado, iconografía Tabler | +| `packages/shadcn-ui/components.json` | Vendor base-vega, con su iconografía interna Lucide conservada | +| `modules/assets/package.json`, `modules/companies/package.json` | React Hook Form 7.72.1, TanStack Table 8.21.3; APIs públicas `./client`, `./common`, `./module` | + +Son versiones declaradas en manifests, no una certificación de ejecución de cada dependencia. TypeScript 6.0.2 está fijado en la raíz. + +## Atributos completos del puesto + +Fuentes normativas de datos: `WorkstationInputSchema`, `WorkstationSchema`, `WorkstationValues`, `Workstation` y `WorkstationPersistenceMapper` (referencias E01–E06 al final). + +| Campo | Tipo / obligatorio | Origen | Uso funcional | Visible actualmente | +| --- | --- | --- | --- | --- | +| `id` | UUID; sí | Identidad del agregado, columna `workstations.id` | URL, comandos y referencias | En URL; sin campo de lectura específico | +| `companyId` | UUID; sí | Columna y contexto de inventario | Ámbito de compañía atendida | Contexto del shell y URL | +| `values.name` | String 1–100; sí | Columna `name`; VO `Name` | Identidad legible; nombre normalizado por VO | Cabecera editable e historial | +| `values.active` | Boolean; sí | Columna `active` | Estado del puesto y reserva de miembros | Cabecera, edición e historial | +| `values.siteId` | UUID o null; clave requerida | FK `company_sites` | Sede del puesto | Cabecera enlazada al listado de sedes | +| `values.personId` | UUID o null; clave requerida | FK `company_contacts` | Responsable del puesto | Cabecera si existe y panel Personas | +| `values.computerAssetId` | UUID, null o ausente | FK `assets`; el VO omite la propiedad si no hay ordenador | Ordenador principal; asociación de perfiles y software | Equipamiento e historial | +| `values.assetIds` | UUID[]; requerido, 0–200 | `workstation_members` | Composición exclusiva actual; sin duplicados | Ordenador y demás activos | +| `history` | Revisiones, mínimo 1 | `workstation_revisions` y `workstation_revision_members` | Instantáneas completas del puesto | Pestaña Historial | +| `history[].id` | UUID; sí | Identidad de revisión | Identificar revisión, no entidad relacionada nueva | Clave interna de cronología | +| `history[].values` | Mismo contrato de valores | Columnas de revisión y sus miembros | Estado en cada revisión | Valores de la cronología | +| `history[].actorId` | UUID; sí | FK `users` | Operador que registró la revisión | No como campo independiente | +| `history[].actorName` | String no vacío; sí en DTO | Resuelto desde usuarios al construir respuesta | Mostrar autor | Cronología; no es snapshot histórico del nombre del usuario | +| `history[].recordedAt` | ISO datetime; sí | Columna de revisión | Momento de registro | Cronología localizada | +| `position`, `activeKey`, `kind` de miembros | Internos de persistencia | Tablas relacionales / proyección | Orden, reserva exclusiva y coherencia | No son campos de negocio del DTO | + +**NO DISPONIBLE** en el agregado: descripción, notas propias, tipología propia del puesto, departamento propio, coordenadas, fecha contractual de ocupación, `createdAt` y `updatedAt` como campos raíz, métricas de salud o disponibilidad online. Se inspeccionaron E01–E06 y la persistencia. Las notas y características pertenecen a activos/perfiles/recursos, no al puesto. + +**DERIVABLE**: fecha de registro inicial = primera revisión; última modificación registrada = última revisión. No equivalen a antigüedad del equipo, alta laboral ni fecha efectiva de asignación. El orden de `assetIds` se conserva al persistir, pero su igualdad de dominio ignora reordenaciones; no prometer una función de ordenación persistente sin revisar ese contrato. + +## Reglas funcionales + +| Regla CONFIRMADA | Fuente / consecuencia | +| --- | --- | +| El ordenador principal, si se indica, debe estar en `assetIds` | `WorkstationValues.create`; no es un miembro adicional | +| Miembros únicos y máximo 200 | Schema y VO; vacío válido, incluso en puesto activo | +| Sede y persona deben existir en contexto de compañía | `mapWorkstationInput`; se puede conservar una referencia inactiva previamente existente | +| Nuevo miembro de puesto activo debe ser operativo | `memberError`; permite conservar un miembro de un puesto antes activo aunque deje de ser operativo | +| Un activo no puede estar en dos puestos activos | Validación de aplicación y `workstation_assets.activeKey` único | +| Puesto inactivo no reserva activos | Mapper; las referencias históricas no equivalen a composición actual | +| Desmantelar desactiva, vacía miembros, ordenador y responsable; conserva sede y nombre | `Workstation.decommission`; crea revisión solo si hay cambio | +| Actualización semánticamente idéntica no crea revisión | `Workstation.update`; no es garantía universal de idempotencia de creación | +| Fechas de revisión válidas y no decrecientes | Agregado y mapper; última instantánea debe coincidir con valores actuales | +| Borrar elimina el puesto y sus revisiones, no los activos | Repositorio, transacción; referencias externas pueden impedirlo con `WORKSTATION_IN_USE` | + +No se encontró una exigencia de categoría «ordenador» sobre `computerAssetId` en el mapper del puesto. La aptitud como host se valida al activar perfiles (`isProfileHost` en `profileReferencesAvailable`); no confundir la selección del editor con una invariante backend adicional. + +## Datos de entidades relacionadas + +| Entidad | Datos que pueden aportar contexto; fuente | +| --- | --- | +| Compañía | `id`, nombre, estado, logo opcional; personas, contactos, sedes, departamentos, asignaciones en `CompanySchema` | +| Sede | Nombre, estado, dirección, teléfono; `SiteSchema` | +| Persona | Nombre, cargo textual `position`, email, teléfono, estado, `isContact`, foto opcional; `PersonSchema` | +| Asignación departamental | Persona, departamento, cargo, fechas, principal y revisiones; `DepartmentAssignmentSchema` | +| Activo | Nombre, plantilla/especificación, sede, marca opcional, `registeredOn` opcional, estado técnico, notas, características tipadas, `assigned`, `displayStatus`, revisiones; `AssetSchema` | +| Perfil | Persona, activo host, cuenta opcional, nombre, activo, notas, `needsReview`, revisiones; `WorkProfileSchema` | +| Software instalado | Activo, recurso aplicación/OS, licencia opcional, versión, estado, notas, revisión y `needsReview`; `SoftwareInstallationSchema` | +| Configuraciones | Carpetas, correo, impresoras, aplicaciones y certificados referidos por `profileId`; tienen estados y revisiones propios | +| Recursos/accesos | Catálogo de recursos, recursos SMB, publicaciones, cuentas, necesidades, concesiones y coberturas; detalle en [mapa](02-mapa-relaciones.md) | +| Conectividad | Soporte de activo; conexiones remotas y autorización de teletrabajo de persona; no hay modalidad presencial/remota propia del puesto | +| Infraestructura | Roles de activo, interfaces ethernet/wifi/other, MAC, direcciones IPv4/IPv6, notas e historial; `InfrastructureSchema` (E35). Son datos registrados, no telemetría | +| Buzones y usos | `MailboxSchema`, `MailboxUseSchema`: dirección, tipo, contexto, cuenta opcional y asignaciones temporales de uso por persona (E36) | +| Procesos | Altas/cambios/bajas de persona con tareas y referencias tipadas; no son tickets ni FK directa al puesto | + +## Localización de implementación y responsabilidades + +Las rutas de esta tabla son relativas a la raíz. En los conjuntos con varios ficheros se enumeran las unidades relevantes, sin implicar que exista un modelo ORM por cada agregado. + +| Elemento | Ruta exacta / responsabilidad | +| --- | --- | +| Valores y agregado | `modules/assets/src/api/domain/workstation-values.ts`, `value-objects/workstation-values.vo.ts`, `workstation.aggregate.ts`, `workstation-revision.entity.ts` | +| Puerto de repositorio | `modules/assets/src/api/application/asset.repository.ts`: `IAssetRepository`, contexto de inventario y comandos de puesto | +| Casos de uso y mapper | `modules/assets/src/api/application/workstation.use-cases.ts`, `workstation-input.mapper.ts`, `workstation-response.builder.ts` | +| HTTP | `modules/assets/src/api/infrastructure/http/workstation.routes.ts`: alta, actualización, desmantelamiento y borrado | +| Persistencia | `modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts`, `workstation-persistence.mapper.ts`, `load-workstations.ts`, `workstation-relational.ts` | +| Modelo/migración | `modules/assets/src/api/infrastructure/persistence/workstation.model.ts`: `WorkstationAssetModel`, `migrateWorkstations`; raíces leídas/escritas mediante SQL parametrizado, no `WorkstationModel` | +| Retirada de miembros compartidos | `modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets.ts` | +| Contratos | `modules/assets/src/common/workstations.ts`, `inventory.ts` | +| Composición servidor | `modules/assets/src/api/module.ts`, `modules/companies/src/api/module.ts`, `apps/server/src/index.ts` | +| Ficha | `modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx`, `workstation-detail-content.tsx`, `workstation-assets.tsx`, `workstation-people.tsx`, `workstation-profiles.tsx` | +| Coordinación | `modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts` | +| Cálculo de relaciones e inversa | `modules/companies/src/web/workstations/detail/helpers/workstation-relations.ts`, `person-workstations.ts` | +| Entrada de compañía | `modules/companies/src/web/companies/detail/ui/company-inventory.tsx` | +| Listado y controlador | `modules/assets/src/web/workstations/list/ui/workstations-page.tsx`, `list/controllers/use-workstations-page-controller.ts` | +| Editor | `modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx`, `workstation-form.tsx`, `workstation-identity-fields.tsx`, `workstation-association-fields.tsx`, `workstation-computer-field.tsx`, `workstation-asset-fields.tsx`, `workstation-form-actions.tsx` | +| Estado y creación | `modules/assets/src/web/workstations/shared/controllers/use-workstation-form-controller.ts`, `shared/helpers/workstation-creation.ts`, `shared/workstation-name.ts` | +| Acciones | `modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx`, `workstation-lifecycle-dialog.tsx` | +| Renombrado | `modules/assets/src/web/workstations/rename/ui/workstation-name-editor.tsx`, `rename/api/rename-workstation.api.ts` | +| Historial | `modules/assets/src/web/workstations/history/ui/workstation-history.tsx`, `workstation-history-dialog.tsx` | +| Cliente HTTP y claves | `modules/assets/src/web/workstations/shared/api/save-workstation.api.ts`, `modules/assets/src/web/shared/api/inventory-request.ts`, `modules/assets/src/web/shared/queries/index.ts` | +| Rutas de compañía | `modules/companies/src/web/workspace/shared/helpers/workspace-routes.ts`; selección de ficha por UUID | +| Traducciones | `modules/assets/src/common/locales/es.json`, `en.json`; claves `workstationDetail`, `workstationLifecycle`, `workstationComputer`, `workstationPerson`. Perfiles/configuraciones usan `modules/companies/src/common/locales/es.json`, `en.json` | +| Tests de dominio/UI | `modules/assets/src/api/domain/workstation.test.ts`; `modules/companies/src/web/workstations/detail/helpers/workstation-relations.test.ts`; `detail/ui/workstation-detail.test.tsx` | +| Tests de edición/alta | `modules/assets/src/web/workstations/shared/helpers/workstation-creation.test.ts`, `shared/workstation-name.test.ts`, `rename/api/rename-workstation.test.ts` | +| Integración | `apps/server/src/workstations.integration.ts`, `workstation-lifecycle.integration.ts`; no se ejecutan aquí porque requieren persistencia | + +## APIs y acciones existentes + +| Operación | Contrato | Efecto / exposición actual | +| --- | --- | --- | +| Consultar puesto | `GET /api/assets/:companyId` | Devuelve inventario completo con `workstations`, activos y relaciones. El controller selecciona el UUID; no se encontró GET individual del puesto | +| Crear | `POST /api/assets/:companyId/workstations` con `{values}` | 201, revisión inicial; se inicia en listado | +| Editar / renombrar / cambiar composición | `POST /api/assets/:companyId/workstations/:id/update` con `{values}` | 200; cabecera y modal; renombrado reutiliza comando de guardado | +| Desmantelar | `POST /api/assets/:companyId/workstations/:id/decommission`, body `{}` | Menú con confirmación; libera reserva, no desactiva perfiles automáticamente | +| Borrar | `POST /api/assets/:companyId/workstations/:id/delete`, body `{}` | Confirmación; retorno al listado tras éxito; puede fallar por referencias | +| Crear perfil | `POST /api/work-profiles/:companyId` | Modal separado; la ficha requiere puesto activo, ordenador presente y no retirado; aplicación valida además host, persona y cuenta | +| Software | `GET/POST /api/software-installations/:companyId`, `POST /:id/update`, `POST /:id/delete` bajo esa base | Panel filtra por ordenador principal, permite alta, edición, historial, borrado | +| Navegar | `/companies/:companyId/workstations/:id`, `/people/:id`, `/assets/:id`, `/work-profiles/:id`, `/resources/:id` | Enlaces reales; sede apunta a `/sites`, sin ficha individual de sede | + +Errores HTTP de comandos de puesto: 400 para `INVALID_REQUEST`, 404 para códigos terminados en `NOT_FOUND`, 409 para otros fallos de negocio; excepciones se propagan al middleware. Autenticación y límites del aislamiento: [viabilidad](05-viabilidad-tecnica.md). + +## Skills y normativa aplicadas + +Se inspeccionaron las 15 carpetas de `.agents/skills/` y las 15 entradas de `skills-lock.json`. Los hashes del lock no equivalen a releases ni se verificó upstream. + +| Skill leída | Criterio aplicado | +| --- | --- | +| `ux-dashboard` — principal | Empezar por preguntas operativas; priorizar y justificar cada indicador; evitar gráficos decorativos | +| `ux-tables` | Elegir tabla para comparación y lista para lectura; formatos, volumen, búsqueda y acciones accesibles; prevalece el patrón Metis de acciones visibles | +| `ux-design-review` | Unificar hallazgos por prioridad y separar evidencia estática, reproducción y pendientes | +| `shadcn` y referencias de composición/Base vs Radix | Comprobar APIs instaladas y wrappers antes de proponer componentes; `render` de Base UI, títulos y estructura semántica | +| `vercel-composition-patterns`, regla `architecture-avoid-boolean-props` | Bloques por responsabilidad y composición explícita; sin componente universal de dashboard | +| `vercel-react-best-practices`, reglas `async-parallel`, `rerender-derived-state-no-effect` | Consultas independientes, cálculo derivado sin efectos y caché remota única | + +Las otras nueve skills están inventariadas, no se declara su aplicación. Faltan las skills auditoras `ux-*-audit` que referencia el orquestador y su política `docs/orchestration-policy.md`; la revisión es manual apoyada en las guías disponibles, sin auditor especializado de accesibilidad. No se instaló nada. DESIGN.md gobierna color, tipografía y espaciado; no se diseña otra identidad. + +Normativa consultada: AGENTS.md, DESIGN.md, `docs/skills-frontend-ui-ux.md`, capas/composición de servidor, estructura web, rutas del cliente, componentes de interfaz, patrones de listados/diálogos, procedimiento Playwright y funcionalidades archivadas. Las referencias enlazadas en los demás documentos distinguen norma e implementación. + +## Referencias de evidencia + +El catálogo de fuentes con enlaces a símbolos y líneas figura a continuación. Las referencias E se utilizan en los ocho entregables; las líneas corresponden al árbol auditado y pueden cambiar con ediciones posteriores. + +| Evidencia | Archivo y símbolo / línea | +| --- | --- | +| E01 | [modules/assets/src/api/domain/workstation-values.ts:3](../../../modules/assets/src/api/domain/workstation-values.ts#L3) — `WorkstationInputSchema` | +| E01 | [modules/assets/src/common/workstations.ts:19](../../../modules/assets/src/common/workstations.ts#L19) — `WorkstationSchema` | +| E02 | [modules/assets/src/api/domain/value-objects/workstation-values.vo.ts:20](../../../modules/assets/src/api/domain/value-objects/workstation-values.vo.ts#L20) — `static create` | +| E03 | [modules/assets/src/api/domain/workstation.aggregate.ts:56](../../../modules/assets/src/api/domain/workstation.aggregate.ts#L56) — `decommission(` | +| E04 | [modules/assets/src/api/application/workstation-input.mapper.ts:11](../../../modules/assets/src/api/application/workstation-input.mapper.ts#L11) — `mapWorkstationInput` | +| E05 | [modules/assets/src/api/infrastructure/persistence/workstation.model.ts:18](../../../modules/assets/src/api/infrastructure/persistence/workstation.model.ts#L18) — `migrateWorkstations` | +| E06 | [modules/assets/src/api/infrastructure/persistence/workstation-persistence.mapper.ts:79](../../../modules/assets/src/api/infrastructure/persistence/workstation-persistence.mapper.ts#L79) — `toPersistence(` | +| E06 | [modules/assets/src/api/application/workstation-response.builder.ts:4](../../../modules/assets/src/api/application/workstation-response.builder.ts#L4) — `export function` | +| E07 | [modules/assets/src/api/infrastructure/persistence/workstation-relational.ts:66](../../../modules/assets/src/api/infrastructure/persistence/workstation-relational.ts#L66) — `readWorkstations` | +| E07 | [modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets.ts:4](../../../modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets.ts#L4) — `removeSharedWorkstationAssets` | +| E07 | [modules/assets/src/api/infrastructure/persistence/load-workstations.ts:10](../../../modules/assets/src/api/infrastructure/persistence/load-workstations.ts#L10) — `loadWorkstations` | +| E08 | [modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx:19](../../../modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx#L19) — `export function` | +| E08 | [modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts:9](../../../modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts#L9) — `export function` | +| E09 | [modules/companies/src/web/workstations/detail/helpers/workstation-relations.ts:6](../../../modules/companies/src/web/workstations/detail/helpers/workstation-relations.ts#L6) — `export function` | +| E10 | [modules/companies/src/web/workstations/detail/helpers/person-workstations.ts:5](../../../modules/companies/src/web/workstations/detail/helpers/person-workstations.ts#L5) — `export function` | +| E11 | [modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx:10](../../../modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx#L10) — `export function` | +| E12 | [modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx:12](../../../modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx#L12) — `export function` | +| E12 | [modules/companies/src/web/workstations/detail/ui/workstation-people.tsx:9](../../../modules/companies/src/web/workstations/detail/ui/workstation-people.tsx#L9) — `export function` | +| E13 | [modules/companies/src/common/dto/shared/company.dto.ts:47](../../../modules/companies/src/common/dto/shared/company.dto.ts#L47) — `CompanySchema` | +| E13 | [modules/companies/src/common/dto/shared/people.dto.ts:55](../../../modules/companies/src/common/dto/shared/people.dto.ts#L55) — `DepartmentAssignmentSchema` | +| E14 | [modules/assets/src/api/domain/asset-values.ts:19](../../../modules/assets/src/api/domain/asset-values.ts#L19) — `AssetInputSchema` | +| E14 | [modules/assets/src/common/inventory.ts:33](../../../modules/assets/src/common/inventory.ts#L33) — `AssetSchema` | +| E14 | [modules/assets/src/api/domain/asset-state.ts:7](../../../modules/assets/src/api/domain/asset-state.ts#L7) — `assetDisplayStatus` | +| E15 | [modules/companies/src/api/domain/work-profile-values.ts:3](../../../modules/companies/src/api/domain/work-profile-values.ts#L3) — `WorkProfileInputSchema` | +| E15 | [modules/companies/src/api/application/work-profile-input.mapper.ts:10](../../../modules/companies/src/api/application/work-profile-input.mapper.ts#L10) — `profileReferencesAvailable` | +| E15 | [modules/companies/src/api/application/work-profile-response.builder.ts:7](../../../modules/companies/src/api/application/work-profile-response.builder.ts#L7) — `buildWorkProfileResponse` | +| E15 | [modules/companies/src/api/infrastructure/persistence/work-profile-migration.ts:6](../../../modules/companies/src/api/infrastructure/persistence/work-profile-migration.ts#L6) — `migrateWorkProfiles` | +| E15 | [modules/companies/src/api/infrastructure/persistence/sequelize-work-profile.repository.ts:17](../../../modules/companies/src/api/infrastructure/persistence/sequelize-work-profile.repository.ts#L17) — `class SequelizeWorkProfileRepository` | +| E16 | [modules/companies/src/api/domain/access/access-definition.ts:103](../../../modules/companies/src/api/domain/access/access-definition.ts#L103) — `AccessDefinitionSchema` | +| E16 | [modules/companies/src/api/domain/access/access-workspace.aggregate.ts:25](../../../modules/companies/src/api/domain/access/access-workspace.aggregate.ts#L25) — `class AccessWorkspace` | +| E16 | [modules/companies/src/api/infrastructure/persistence/access-persistence.ts:70](../../../modules/companies/src/api/infrastructure/persistence/access-persistence.ts#L70) — `migrateAccess` | +| E17 | [modules/companies/src/api/domain/folder-mapping-values.ts:5](../../../modules/companies/src/api/domain/folder-mapping-values.ts#L5) — `FolderMappingInputSchema` | +| E17 | [modules/companies/src/common/folder-mappings.ts:16](../../../modules/companies/src/common/folder-mappings.ts#L16) — `FolderMappingSchema` | +| E18 | [modules/companies/src/api/application/folder-mapping-policy.ts:39](../../../modules/companies/src/api/application/folder-mapping-policy.ts#L39) — `folderAvailable` | +| E18 | [modules/companies/src/api/application/folder-mapping-account-policy.ts:50](../../../modules/companies/src/api/application/folder-mapping-account-policy.ts#L50) — `folderAccountCoverage` | +| E18 | [modules/companies/src/api/domain/access/access-policy.ts:165](../../../modules/companies/src/api/domain/access/access-policy.ts#L165) — `checkCoverage` | +| E18 | [modules/companies/src/api/domain/access/access-projection.ts:302](../../../modules/companies/src/api/domain/access/access-projection.ts#L302) — `projectAccess` | +| E19 | [modules/resources/src/api/domain/resource-values.ts:21](../../../modules/resources/src/api/domain/resource-values.ts#L21) — `ResourceIntegrationSchema` | +| E19 | [modules/resources/src/common/inventory.ts:35](../../../modules/resources/src/common/inventory.ts#L35) — `ResourceSchema` | +| E19 | [modules/companies/src/api/infrastructure/persistence/resource-access-writer.ts:85](../../../modules/companies/src/api/infrastructure/persistence/resource-access-writer.ts#L85) — `async project` | +| E20 | [modules/assets/src/common/network-share-publications.ts:10](../../../modules/assets/src/common/network-share-publications.ts#L10) — `NETWORK_SHARE_PUBLICATION_DEFINITION` | +| E20 | [modules/assets/src/api/infrastructure/persistence/network-share-publication-migration.ts:4](../../../modules/assets/src/api/infrastructure/persistence/network-share-publication-migration.ts#L4) — `CREATE TABLE` | +| E20 | [modules/assets/src/api/infrastructure/http/network-share-publication.routes.ts:24](../../../modules/assets/src/api/infrastructure/http/network-share-publication.routes.ts#L24) — `registerNetworkSharePublicationRoutes` | +| E21 | [modules/companies/src/api/infrastructure/persistence/configuration-snapshots.ts:12](../../../modules/companies/src/api/infrastructure/persistence/configuration-snapshots.ts#L12) — `profileSnapshot` | +| E21 | [modules/companies/src/api/infrastructure/persistence/mail-remote-snapshots.ts:42](../../../modules/companies/src/api/infrastructure/persistence/mail-remote-snapshots.ts#L42) — `mailSnapshot` | +| E21 | [modules/companies/src/api/domain/application-configuration-values.ts:5](../../../modules/companies/src/api/domain/application-configuration-values.ts#L5) — `ApplicationConfigurationInputSchema` | +| E21 | [modules/companies/src/api/domain/printer-configuration-values.ts:5](../../../modules/companies/src/api/domain/printer-configuration-values.ts#L5) — `PrinterConfigurationInputSchema` | +| E21 | [modules/companies/src/api/domain/certificate-configuration-values.ts:5](../../../modules/companies/src/api/domain/certificate-configuration-values.ts#L5) — `CertificateConfigurationInputSchema` | +| E22 | [modules/companies/src/api/domain/remote-connection-values.ts:5](../../../modules/companies/src/api/domain/remote-connection-values.ts#L5) — `RemoteConnectionInputSchema` | +| E22 | [modules/companies/src/api/application/remote-connection-policy.ts:31](../../../modules/companies/src/api/application/remote-connection-policy.ts#L31) — `remoteNeedsReview` | +| E23 | [modules/companies/src/api/domain/telework-authorization-values.ts:3](../../../modules/companies/src/api/domain/telework-authorization-values.ts#L3) — `TeleworkAuthorizationInputSchema` | +| E23 | [modules/companies/src/api/application/telework-authorization-policy.ts:6](../../../modules/companies/src/api/application/telework-authorization-policy.ts#L6) — `teleworkAuthorizationState` | +| E24 | [modules/assets/src/common/support.ts:15](../../../modules/assets/src/common/support.ts#L15) — `SupportConnectionSchema` | +| E24 | [modules/assets/src/api/domain/support-values.ts:3](../../../modules/assets/src/api/domain/support-values.ts#L3) — `SupportValuesSchema` | +| E24 | [modules/assets/src/api/infrastructure/http/support.routes.ts:18](../../../modules/assets/src/api/infrastructure/http/support.routes.ts#L18) — `registerSupportRoutes` | +| E25 | [modules/companies/src/common/local-identity-environments.ts:18](../../../modules/companies/src/common/local-identity-environments.ts#L18) — `LocalIdentityEnvironmentSchema` | +| E25 | [modules/companies/src/api/infrastructure/persistence/local-identity-migration.ts:6](../../../modules/companies/src/api/infrastructure/persistence/local-identity-migration.ts#L6) — `CREATE TABLE` | +| E26 | [modules/assets/src/common/directory-memberships.ts:54](../../../modules/assets/src/common/directory-memberships.ts#L54) — `DirectoryMembershipSchema` | +| E26 | [modules/assets/src/api/infrastructure/http/directory-membership.routes.ts:21](../../../modules/assets/src/api/infrastructure/http/directory-membership.routes.ts#L21) — `registerDirectoryMembershipRoutes` | +| E27 | [modules/companies/src/api/domain/process-values.ts:27](../../../modules/companies/src/api/domain/process-values.ts#L27) — `ProcessReferenceSchema` | +| E27 | [modules/companies/src/api/infrastructure/http/process.routes.ts:27](../../../modules/companies/src/api/infrastructure/http/process.routes.ts#L27) — `registerProcessRoutes` | +| E27 | [docs/funcionalidades-archivadas.md:28](../../../docs/funcionalidades-archivadas.md#L28) — `## Aplicación activa` | +| E27 | [modules/companies/src/api/infrastructure/persistence/remove-archived-support-tables.ts:5](../../../modules/companies/src/api/infrastructure/persistence/remove-archived-support-tables.ts#L5) — `const` | +| E28 | [modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx:17](../../../modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx#L17) — `export function` | +| E28 | [modules/companies/src/web/software-installations/list/ui/software-installations-panel.tsx:26](../../../modules/companies/src/web/software-installations/list/ui/software-installations-panel.tsx#L26) — `export function` | +| E29 | [modules/assets/src/web/workstations/history/ui/workstation-history.tsx:15](../../../modules/assets/src/web/workstations/history/ui/workstation-history.tsx#L15) — `export function` | +| E30 | [modules/companies/src/web/work-environments/detail/hooks/use-environment-queries.ts:20](../../../modules/companies/src/web/work-environments/detail/hooks/use-environment-queries.ts#L20) — `export function` | +| E30 | [modules/companies/src/web/work-environments/shared/helpers/work-environment.model.ts:60](../../../modules/companies/src/web/work-environments/shared/helpers/work-environment.model.ts#L60) — `export function buildEnvironment` | +| E30 | [modules/companies/src/web/work-environments/detail/ui/environment-content.tsx:19](../../../modules/companies/src/web/work-environments/detail/ui/environment-content.tsx#L19) — `export function` | +| E31 | [packages/rdx-ui/src/components/detail-panel.tsx:6](../../../packages/rdx-ui/src/components/detail-panel.tsx#L6) — `export function` | +| E31 | [packages/rdx-ui/src/components/detail-field.tsx:4](../../../packages/rdx-ui/src/components/detail-field.tsx#L4) — `export function` | +| E31 | [packages/rdx-ui/src/components/statistic-card.tsx:19](../../../packages/rdx-ui/src/components/statistic-card.tsx#L19) — `export function` | +| E32 | [apps/web/components.json:3](../../../apps/web/components.json#L3) — `base-vega` | +| E32 | [packages/rdx-ui/src/components/entity-list.tsx:17](../../../packages/rdx-ui/src/components/entity-list.tsx#L17) — `export` | +| E32 | [packages/shadcn-ui/src/components/tabs.tsx:7](../../../packages/shadcn-ui/src/components/tabs.tsx#L7) — `function Tabs` | +| E32 | [packages/shadcn-ui/src/components/button.tsx:42](../../../packages/shadcn-ui/src/components/button.tsx#L42) — `function Button` | +| E33 | [modules/identity/src/api/infrastructure/http/require-authenticated.ts:6](../../../modules/identity/src/api/infrastructure/http/require-authenticated.ts#L6) — `export function` | +| E33 | [modules/assets/src/api/infrastructure/http/workstation.routes.ts:16](../../../modules/assets/src/api/infrastructure/http/workstation.routes.ts#L16) — `registerWorkstationRoutes` | +| E33 | [modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts:157](../../../modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts#L157) — `saveWorkstation(` | +| E34 | [apps/server/src/infrastructure/http/relationship-read-access.ts:7](../../../apps/server/src/infrastructure/http/relationship-read-access.ts#L7) — `class SessionRelationshipReadAccess` | +| E35 | [modules/assets/src/api/domain/infrastructure-values.ts:33](../../../modules/assets/src/api/domain/infrastructure-values.ts#L33) — `InfrastructureInputSchema` | +| E35 | [modules/assets/src/api/infrastructure/persistence/infrastructure-migration.ts:4](../../../modules/assets/src/api/infrastructure/persistence/infrastructure-migration.ts#L4) — `migrateInfrastructure` | +| E35 | [modules/assets/src/api/infrastructure/persistence/sequelize-infrastructure.repository.ts:21](../../../modules/assets/src/api/infrastructure/persistence/sequelize-infrastructure.repository.ts#L21) — `class SequelizeInfrastructureRepository` | +| E36 | [modules/companies/src/common/mailboxes.ts:34](../../../modules/companies/src/common/mailboxes.ts#L34) — `MailboxesResponseSchema` | +| E36 | [modules/companies/src/api/application/mail-configuration-policy.ts:5](../../../modules/companies/src/api/application/mail-configuration-policy.ts#L5) — `mailAuthorized` | +| E36 | [modules/companies/src/api/infrastructure/persistence/mailbox-migration.ts:6](../../../modules/companies/src/api/infrastructure/persistence/mailbox-migration.ts#L6) — `migrateMailboxes` | +| E37 | [apps/server/src/infrastructure/http/identity-runtime.ts:17](../../../apps/server/src/infrastructure/http/identity-runtime.ts#L17) — `createIdentityRuntime` | +| E37 | [modules/assets/src/api/module.ts:67](../../../modules/assets/src/api/module.ts#L67) — `createAssetModule` | +| E37 | [modules/companies/src/api/module.ts:99](../../../modules/companies/src/api/module.ts#L99) — `createCompanyModule` | diff --git a/docs/auditorias/workplace-dashboard/02-mapa-relaciones.md b/docs/auditorias/workplace-dashboard/02-mapa-relaciones.md new file mode 100644 index 0000000..c1822f9 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/02-mapa-relaciones.md @@ -0,0 +1,132 @@ +# Mapa de relaciones del puesto + +Fecha: 22 de octubre de 2026. Etiquetas y catálogo de fuentes E: [inventario](01-inventario-funcional.md). Las tablas describen implementación y DDL inspeccionados, no un esquema instalado consultado. + +## Relaciones directas y composición + +| Entidad | Relación y cardinalidad | Modelo/tabla y condición | API disponible | Uso actual | +| --- | --- | --- | --- | --- | +| Compañía | Cada puesto pertenece a 1 compañía; compañía 0..N puestos | FK `workstations.companyId → companies.id`; filtros de contexto | Inventario Assets | Shell y URL | +| Sede | Puesto 0..1 sede; sede 0..N puestos | `siteId → company_sites.id`; validación de compañía E04 | Compañía, inventario | Cabecera; enlace al listado | +| Responsable | Puesto 0..1 persona; persona 0..N puestos | `personId → company_contacts.id`; no exclusividad por persona | Compañía, inventario | Cabecera y panel Personas | +| Miembros | Puesto 0..200 activos actuales; activo como máximo 1 puesto activo | `workstation_members`; `workstation_assets.activeKey` reserva única | Inventario, comandos puesto | Equipamiento | +| Ordenador | Puesto 0..1 activo principal incluido en miembros | `computerAssetId → assets.id`; E02/E04 | Inventario | Destacado; perfiles y software | +| Revisiones | Puesto 1..N revisiones | `workstation_revisions.workstationId` | En `WorkstationDTO.history` | Cronología | +| Miembros históricos | Revisión 0..200 activos; activo puede aparecer en N revisiones y puestos en el tiempo | `workstation_revision_members` | Valores de cada revisión | Nombres resueltos en historial | +| Actores | Revisión 1 usuario de aplicación; usuario 0..N revisiones | `actorId → users.id`; nombre resuelto E06 | DTO de puesto | Autor de revisión | + +**CONFIRMADO** E05/E06/E07: `workstation_assets` es una proyección de todos los miembros alguna vez registrados, con reserva actual opcional. No usar sus filas como lista de activos actuales. Los miembros actuales proceden de `workstation_members`; los históricos, de cada revisión. Las relaciones están implementadas con FK y SQL parametrizado; no se localizaron asociaciones Sequelize `belongsTo/hasMany` para la raíz Workstation. + +## Personas, departamentos y tiempo + +**CONFIRMADO** E09: la ficha selecciona perfiles cuyo `companyId` coincide y cuyo `values.assetId` es el `computerAssetId` actual. Incluye perfiles inactivos. Deduplica personas de esos perfiles y añade al responsable. Esto no identifica necesariamente ocupantes vigentes. + +**DERIVABLE**: «usuarios actuales» puede definirse como personas de perfiles activos del ordenador actual, separadas del responsable. La decisión de excluir inactivos por defecto sería **PROPUESTA**, no conducta vigente de la ficha. La inversa `personWorkstations` ya distingue responsabilidad y uso, y para uso considera puestos y perfiles activos (E10). + +No hay fechas de inicio/fin de ocupación en Workstation. Se puede reconstruir cuándo se registró un cambio de responsable mediante revisiones; no una asignación laboral efectiva. Sí existen asignaciones temporales a departamentos (`company.assignments`, E13): persona, departamento, `startDate`, `endDate`, cargo y `isPrimary`. Para mostrar departamento desde el puesto hay que escoger persona y fecha y evaluar el periodo; nunca convertirlo en departamento propio del puesto. + +La sede del activo es independiente de la del puesto. No sobrescribir ni deducir igualdad. La persona del perfil tampoco es automáticamente «propietaria» del activo: `AssetInputSchema` no tiene `personId`. + +## Grafo funcional confirmado + +Las líneas representan asociaciones por claves/condiciones comprobadas, no todas son FK. En particular, Puesto–Perfil es un join por ordenador y compañía; no hay `workstationId` en `work_profiles`. + +```mermaid +flowchart TD + Company[Compañía] -->|0..N| Station[Puesto: Workstation] + Station -->|0..1 siteId| Site[Sede] + Station -->|0..1 personId| Person[Persona responsable] + Station -->|0..200 miembros exclusivos| Asset[Activo] + Station -->|0..1 computerAssetId incluido| Computer[Activo principal] + Station -->|1..N| Revision[Revisión de puesto] + Revision -->|0..200 miembros de esa revisión| Asset + Revision -->|actorId| Operator[Usuario de aplicación] + Computer -.->|join por assetId y companyId: 0..N| Profile[Perfil de trabajo] + Profile -->|1 personId| UserPerson[Persona del perfil] + UserPerson -->|0..N asignaciones temporales| Department[Departamento] + Profile -->|0..1 accountId| Account[Cuenta de acceso] + Profile -->|0..N| Folder[FolderMapping] + Folder -->|1 needId| Need[Necesidad de persona] + Folder -->|0..1 accessAccountId| Account + Need -->|1 resourceId| AccessResource[Recurso de Access] + Need -->|0..N| Coverage[Cobertura] + Coverage -->|grantId| Grant[Concesión a cuenta o grupo] + Coverage -->|accountPersonId| AccountPerson[Asignación cuenta-persona] + Coverage -->|0..1 membershipId| Membership[Pertenencia a grupo] + AccountPerson --> Account + Profile -->|0..N| Config[Correo, aplicación, certificado, impresora] + Computer -->|0..N| Software[Instalación de software] + Software -->|aplicación u OS; licencia opcional| Resource[Recurso del inventario] + Asset -->|0..N publicaciones| Publication[Publicación SMB] + Publication -->|1 recurso SMB; máximo 1 publicador activo por recurso| Resource + Asset -->|0..N históricos| Local[Entorno de identidades locales] + Asset -->|pertenencia de directorio| Domain[Recurso de dominio] + Asset -->|0..N| Support[Conexión de soporte] + Asset -->|0..1 configuración con revisiones| Infrastructure[Roles, interfaces y direcciones registradas] + UserPerson -->|contexto de persona| Remote[Conexión remota y autorización de teletrabajo] + Remote -->|origen/destino opcional según tipo| Asset + UserPerson -->|0..N| Process[Proceso de alta, cambio o baja] +``` + +## Equipamiento y relaciones entre activos + +**CONFIRMADO** E14: los activos tienen plantillas, características y estados técnicos `operational`, `broken`, `maintenance`, `retired`; `available`/`in_use` son estados de presentación derivados de reserva, no salud online. Las revisiones pueden conservar estados antiguos. + +Ordenador, monitor, teléfono o dispositivo de red son datos de plantilla/capacidad del activo. El puesto no dispone de una enumeración propia de funciones. La ficha actual llama «periféricos» a todos los miembros salvo el principal (E11); esa etiqueta no demuestra que todos lo sean. + +**PROPUESTO**: agrupar por plantilla real y distinguir el ordenador principal. Las impresoras utilizadas por un perfil pueden ser compartidas y no estar en `assetIds`; deben ir en «Configuraciones del perfil», con su procedencia, sin incorporarlas al equipamiento exclusivo. Las relaciones genéricas `AssetRelationship` (inventario `relationships`) pueden aportar enlaces, pero no transfieren pertenencia, asignación, permisos ni conectividad transitiva. + +Cambiar `computerAssetId` cambia inmediatamente los perfiles y software seleccionados por la ficha. Sus registros siguen vinculados al activo anterior. Desmantelar el puesto no mueve ni desactiva esos registros. Una cronología que muestre «todos los perfiles históricos del puesto» exigiría una regla temporal adicional y no puede derivarse solo del filtro actual. + +## Recursos, accesos y SMB + +| Recorrido | Significado y cardinalidad | Fuente / tablas / API | Clasificación para el dashboard | +| --- | --- | --- | --- | +| Puesto → ordenador → perfiles | 0..N perfiles del host, cada perfil 1 persona/activo y 0..1 cuenta | E09/E15; `work_profiles`, revisiones; `/api/work-profiles/:companyId` | **DERIVABLE**, contexto de perfil | +| Perfil → FolderMapping → need → resource | 0..N mapeos; cada uno 1 necesidad de la misma persona del perfil | E17/E18; `folder_mappings`, `access_items`; `/api/folder-mappings/:companyId`, `/api/access/:scope` | Carpeta configurada en ese perfil, no todos los recursos del responsable | +| Mapping → cuenta elegida | 0..1 cuenta, independiente de cuenta de inicio de sesión del perfil | `accessAccountId`; `folderAccountUsable`, `folderAccountCoverage` | Mantener separadas cuenta del perfil y cuenta de acceso a carpeta | +| Persona → need → coverage → grant | N necesidades y coberturas; concesión a cuenta XOR grupo | E16/E18; `access_items`, `access_references`, revisiones | Contexto de autorización documental; no comprobación real contra servidor SMB | +| Cuenta → account_person → persona | Asignaciones temporales; cuenta compartida admite varias personas según política | `AccessPolicy`, `accessConflict` | No atribuir todos sus permisos al puesto | +| Cuenta → membership → grupo → grant | Pertenencias y concesiones con periodo y configuración | E16/E18 | Respetar configuración, vigencia y catálogo activo | +| Recurso inventariado → integración Access | Recurso con `integration.kind=access`, tipo y referencia | E19; `ResourceIntegrationSchema`, escritor de Access; `resources` y `access_items` | Resolver la identidad mediante integración; no unir por nombre | +| Activo → publicación SMB → recurso | Publicador 0..N recursos; recurso máximo 1 publicador activo | E20; `asset_network_share_publications`; `/api/asset-network-share-publications/:companyId` | Publicar no concede acceso ni prueba consumo desde el puesto | +| Perfil → aplicación/certificado/correo/impresora | Cada configuración pertenece a 1 perfil; recurso/buzón/activo destino según tipo | E21; tablas específicas, revisiones y APIs homónimas | Configuraciones contextualizadas, distintas de software instalado en el equipo | +| Perfil → correo → buzón → uso de persona | Configuración referencia buzón; autorización exige uso vigente de ese buzón por la persona del perfil | E36; `mailboxes`, `mailbox_uses`, revisiones; `/api/mailboxes/:companyId`; `mailAuthorized` | No inferir acceso a todos los buzones por usar un equipo | + +`AccessWorkspace` es un agregado de accesos global o de compañía. **No representa un escritorio ni un puesto** y no debe enlazarse a Workstation por su nombre (E16). + +`FolderMappingSchema` ya expone `permissionsCovered`, `accessAccountCoverage` (`unknown`, `covered`, `not-covered`) y `needsReview`. `folderAccountCoverage` filtra por la cuenta elegida y por necesidades/vías pertinentes. Reutilizar esos resultados; no reducir autorización a `state === configured`. `folderAvailable` comprueba también persona/perfil/activo/recurso, coincidencia de ruta, catálogo y fechas. + +Las publicaciones SMB son explícitas, con inicio, fin, actores y predecessor; el host de una UNC no sustituye esa relación. La referencia canónica SMB y la cobertura son dimensiones separadas. No mostrar «acceso efectivo comprobado»: no se inspeccionaron conexiones reales a SMB ni credenciales. + +## Conectividad, dominios y entornos + +| Capacidad | Evidencia | Qué puede afirmarse | +| --- | --- | --- | +| Autorización de teletrabajo | **CONFIRMADO**, `TeleworkAuthorizationInputSchema`, DTO con `status`, `effective`, `needsReview`; E23 | Pertenece a persona; fechas opcionales. No prueba que esté trabajando en remoto | +| Conexión remota | **CONFIRMADO**, E22 | Persona; origen equipo de compañía/ordenador personal/móvil personal; destino red/activo/recurso; método libre, `requiresVpn`, configuración y revisión | +| VPN | **CONFIRMADO** como booleano en conexión remota y soporte | No hay agregado VPN, túnel activo, credenciales ni telemetría de conexión localizados en contratos inspeccionados | +| Soporte remoto del activo | **CONFIRMADO**, E24 | Herramienta/recurso, destino, VPN requerida, preferencia, estado; no equivale a autorización de teletrabajo | +| Interfaces y direcciones de red | **CONFIRMADO**, E35 | Activo 0..1 agregado de infraestructura, 0..32 interfaces y 0..32 direcciones por interfaz; tipo ethernet/wifi/other, MAC, IPv4/IPv6, notas, roles e historial. No prueba conexión actual | +| Entorno local | **CONFIRMADO**, E25 | `access_local_identity_environments`: ciclo de vida que vincula entorno Access y activo con `identityScope=local-device` | +| Directorio corporativo | **CONFIRMADO**, E26 | `DirectoryMembership` une activo y recurso de dominio con rol e historial; no demuestra conexión online | +| Portátil corporativo | **DERIVABLE** de plantilla/capacidad de activo y compañía | No es una modalidad de Workstation ni prueba movilidad efectiva | +| Presencia/remoto actual, topología y conectividad online | **NO DISPONIBLE** en puesto, conexiones/soporte e inventarios inspeccionados | No dibujar mapas de red, estado «conectado» ni ubicación física actual como hechos | + +Para conexiones remotas: mostrar por separado «conexiones que usan este equipo» (`sourceAssetId` o destino activo) y «contexto de esta persona» (`personId`). No asumir que todas las conexiones de una persona usan el ordenador del puesto. Una autorización documental tampoco asegura acceso real a recursos. + +Infraestructura persiste en `asset_infrastructure`, `asset_infrastructure_revisions`, `asset_infrastructure_roles`, `asset_interface_snapshots` y `asset_interface_address_snapshots`. Tiene FK de compañía/activo, revisiones ordenadas e identidad para interfaces/direcciones. Sus roles son `network`, `server`, `storage`, `shared_peripheral`; no clasifican el puesto ni convierten un activo en miembro compartido. Puede mostrarse como detalle técnico del equipo sin inventar una topología física. + +## Tickets, procesos e historial + +**DOCUMENTADO y CONFIRMADO en composición activa**: tickets, intervenciones y plantillas de intervención se retiraron. Se buscaron `ticket`, `incidencia`, `intervention`, `workplace` y `workstationId` en módulos, apps y paquetes activos; se contrastaron composition roots, mapa de rutas y `remove-archived-support-tables.ts`. `docs/funcionalidades-archivadas.md` describe el código conservado en `archive/tickets-and-interventions`. No se propone restaurarlo. + +**NO DISPONIBLE**: panel de tickets/incidencias/solicitudes del puesto como capacidad activa. Omitirlo del mockup de capacidades actuales; no mostrar un cero que sugiera un módulo operativo vacío. + +**CONFIRMADO** E27: los procesos de persona (`onboarding`, `change`, `offboarding`) tienen tareas con referencias a perfil, activo, cuenta, necesidad y configuraciones. No admiten referencia tipada `workstation`. **DERIVABLE**: localizar procesos de personas relacionadas o tareas que refieran activos/perfiles, explicando la vía. No llamarlos incidencias ni cambios automáticos del puesto. + +La actividad propia segura es `workstation.history`: instantáneas de creación y cambios registrados. Se puede comparar semánticamente cada revisión con la anterior para resumir cambios (**DERIVABLE**). Los nombres de personas/sedes/activos se resuelven con datos actuales: no prometer fidelidad histórica de sus etiquetas. Un feed combinado de revisiones de varias entidades requeriría agregación, identificadores de origen, deduplicación y una política temporal; queda **PROPUESTO**. + +## Discrepancia documental + +`docs/rutas-del-cliente.md` aún describe puestos que agrupan activos «exclusivos o compartidos». El código actual elimina miembros `shared` y reconstruye únicamente `exclusive` (E07). Para esta propuesta prevalece la implementación contrastada y se registra la discrepancia sin modificar la guía ajena al alcance. diff --git a/docs/auditorias/workplace-dashboard/03-auditoria-ui-actual.md b/docs/auditorias/workplace-dashboard/03-auditoria-ui-actual.md new file mode 100644 index 0000000..9a5c2d3 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/03-auditoria-ui-actual.md @@ -0,0 +1,102 @@ +# Auditoría de la ficha actual + +Fecha: 22 de octubre de 2026. Revisión estática, sin reproducción visual de la ficha. Evidencias E: [inventario](01-inventario-funcional.md). + +## Pantalla y composición + +Ruta: `/companies/:companyId/workstations/:id`. `CompanyInventory` selecciona la pantalla; `WorkstationDetailPage` recibe compañía, UUID y lookups de editor. No es un modal de detalle. + +```text +CompanyInventory +└─ WorkstationDetailPage (@container) + ├─ BrowserLink: volver al listado + ├─ Carga / error de inventario con reintento / UUID no encontrado + ├─ header + │ ├─ WorkstationNameEditor + │ ├─ EntityStatus + sede + responsable enlazados + │ └─ WorkstationActions + │ ├─ Editar + │ └─ Menú → WorkstationLifecycleDialog (desmantelar/borrar) + ├─ WorkstationDetailContent + │ └─ Tabs + ScrollableTabsList + │ ├─ Información (keepMounted) + │ │ ├─ WorkstationAssets → AssetSummaryCard / EmptyState + │ │ ├─ WorkstationPeople → PersonAvatar + enlaces + │ │ └─ WorkstationProfiles → EntityTable + Nuevo perfil + │ ├─ Software (keepMounted, carga tras primera apertura) + │ │ └─ SoftwareInstallationsPanel + │ │ ├─ EntityTable + búsqueda + mostrar cerrados + │ │ └─ Editor / historial / DeleteRecordDialog + │ └─ Historial → DetailPanel → WorkstationHistory → TimeLine + ├─ WorkProfileEditorDialog (cuando se abre) + └─ WorkstationEditorDialog (cuando se abre) +``` + +Fuentes: E08/E11/E12/E28/E29. Información distribuye Equipamiento y Personas en dos columnas desde 1000 px de contenedor; periféricos usan dos desde 560 px. Son condiciones del código, no una validación responsive. El historial incluye estado, nombre, sede, responsable, ordenador y miembros de cada revisión, con autor y fecha; los nombres relacionados se resuelven hoy. + +## Matriz de cobertura + +«BD» significa persistencia declarada en código; no inspección de una instancia. «API» puede requerir varias consultas. Los datos derivados no son columnas nuevas. + +| Información | Existe en BD | Disponible en API | Visible en ficha | Acción disponible | Observaciones | +| --- | --- | --- | --- | --- | --- | +| Identidad y estado | Sí | Inventario | Sí | Renombrar, editar, desmantelar, borrar | UUID solo en URL | +| Sede | Sí, FK | Compañía/inventario | Nombre | Ir a sedes | Dirección y teléfono no están en cabecera | +| Responsable | Sí, FK | Compañía | Nombre | Ir a persona; cambiar en editor | Si no hay responsable, su segmento de cabecera desaparece | +| Otras personas | Perfiles | Perfiles + compañía | Sí | Ir a persona | Incluye personas de perfiles inactivos | +| Departamento | Asignación de persona | Compañía | No | Navegación por entidades existentes | Contexto indirecto, requiere fecha/persona | +| Ordenador y miembros | Sí | Inventario + catálogo | Sí | Fichas y editar composición | Tarjetas, no tabla compacta | +| Características completas | En activo | Inventario | Resumen de AssetSummaryCard | Abrir activo | No todo el detalle técnico cabe en tarjeta | +| Perfiles | Sí, por host | Perfiles | Tabla | Crear y abrir perfil | Nombre, persona, estado y revisión | +| Cuenta y notas de perfil | Sí | Perfiles + Access | No en tabla | Abrir perfil | Datos ya disponibles en otras pantallas | +| Software del ordenador | Sí | Instalaciones + recursos/catálogo | Pestaña Software | Alta, edición, historial y borrado | No agrega software de todos los miembros | +| Licencias de software | Recurso vinculado | Sí | Software | Abrir recurso | No equivale a cumplimiento global | +| Carpetas/correo/apps/certificados/impresoras de perfil | Sí | APIs específicas | No | Desde ficha de perfil | Agregables por perfiles confirmados | +| Cobertura documental de carpetas | Derivada backend | DTO de FolderMapping | No | Gestión de carpeta y accesos | No prueba de acceso SMB real | +| Publicaciones SMB de activos | Sí | API paginada | No | En activos/recursos | Publicador no significa consumidor | +| Identidades locales y dominio | Sí | APIs específicas | No | En activos/accesos | Contexto de equipo | +| Interfaces, MAC e IP registradas | Sí, infraestructura | API infraestructura | No en bloque específico | Consultar activo/infraestructura | Sin detección online ni topología inferida | +| Soporte del ordenador | Sí | Support API | No | Pantalla de activo/entorno | Diferente de conexión remota de persona | +| Teletrabajo / conexiones de persona | Sí | APIs específicas | No | Secciones con `personId` | No propiedades directas del puesto | +| Procesos de personas relacionadas | Sí | Process API | No | Abrir proceso | Contexto indirecto, no incidencias | +| Revisiones propias | Sí | Inventario | Historial | Consultar | No hay feed combinado ni diff resumido | +| Tickets/incidencias | Archivado | Sin API activa localizada | No | Sin acción activa | No incluir widget operativo | + +## Hallazgos priorizados + +Todos los siguientes son **CONFIRMADOS por código** o **PROPUESTOS** explícitamente. No hay un fallo visual reproducido en esta auditoría. Prioridades: P1 = evita interpretación funcional equivocada; P2 = mejora consulta habitual; P3 = detalle y pulido. + +| ID | Prioridad | Evidencia y efecto | Recomendación PROPUESTA | +| --- | --- | --- | --- | +| U01 | P1 | E09 y `WorkstationPeople`: personas de perfiles activos e inactivos aparecen juntas; la persona solo lleva etiqueta «persona del perfil» | Separar uso actual e histórico; mantener responsable como rol independiente. No titular todo el grupo «ocupantes actuales» | +| U02 | P1 | E11: todos los miembros no principales se llaman periféricos | Usar «Otros activos» o plantilla real; no inventar taxonomía | +| U03 | P1 | E09/E28: al cambiar principal, cambian perfiles/software derivados; no hay traslado de datos | Explicar alcance del host y ofrecer acceso al historial para consultar composición anterior | +| U04 | P2 | E08/E12: la ficha no consulta configuraciones del perfil; estas existen en E21/E30 | Resumir por perfil y enlazar su detalle; no copiar cada pantalla de configuración | +| U05 | P2 | E28: software e historial separados de la información principal | Mostrar resumen útil de software y último cambio en vista general, conservando detalle secundario | +| U06 | P2 | E08: cuando no hay responsable la cabecera omite el segmento; E11 ocupa espacio de equipamiento con EmptyState y explicación | Hacer explícito el responsable ausente en resumen; compactar vacíos preservando su acción de completar | +| U07 | P2 | E11 genera una tarjeta por activo y recorre búsquedas de activo/catálogo por miembro; máximo 200 | Lista compacta o tabla al crecer, búsquedas/orden/paginación según patrón; riesgo de densidad y coste, sin medición realizada | +| U08 | P2 | Controller ignora `catalog.isError` en página; `WorkstationAssets` recibe catálogo vacío por defecto | Identificar fallo de metadatos de catálogo sin hacer pasar su ausencia por dato real inexistente | +| U09 | P2 | E08 carga con texto; perfiles y personas comparten dependencia de perfiles aunque el responsable ya sea conocido | Mantener responsable visible, estado parcial de usuarios y estructura estable. No usar cero hasta completar consulta | +| U10 | P2 | E30 `useEnvironmentQueries` retorna `data: undefined` si falta cualquiera de 11 fuentes | Reutilizar lógica y queries, pero revisar el bloqueo total antes de integrar un dashboard que necesita fallos parciales | +| U11 | P3 | E29 resuelve etiquetas históricas mediante nombres actuales | Describir historial como valores registrados y referencias; no afirmar snapshot de etiquetas relacionadas | +| U12 | P3 | En `WorkstationProfiles`, revisión usa colores amber explícitos; DESIGN no define warning/success globales | Revisar uso según sistema existente, sin inventar tokens ni afirmar contraste fallido no medido | + +No se localizaron mecanismos de conversión, precios u opt-in relevantes para una auditoría de dark patterns en este flujo. No se ejecutó un auditor externo ni se declara cumplimiento WCAG. + +## Patrones acertados que conservar + +- Enlaces de entidades mediante `BrowserLink`, separados de acciones de edición; sedes enlaza a un destino existente. +- Edición, creación de perfil, ciclo de vida e historial de software en componentes modales separados. +- Cabecera de identidad, estado textual y distinción entre ordenador principal y demás miembros. +- `EntityTable` en perfiles y software; valores ausentes representados con «—» en las celdas inspeccionadas. +- Estados de error y reintento en inventario y perfiles; Personas no llama vacío a una carga fallida. +- Perfiles filtrados por compañía y ordenador, con deduplicación de personas. +- Software se monta después de abrir su pestaña: evita coste inicial innecesario. + +## Accesibilidad, usabilidad y responsive pendientes + +El JSX permite comprobar regiones etiquetadas en `DetailPanel`, nombres accesibles en acciones, iconos decorativos ocultos, `role=status/alert`, fechas en `TimeLine` y uso de primitivas Base UI. Esto no acredita el comportamiento del teclado, el foco ni el contraste calculado. + +Playwright MCP respondió a `browser_tabs` con una sola pestaña `about:blank`. La comprobación de `http://localhost:5173/` con curl falló (HTTP 000, conexión rechazada/no disponible desde este entorno); `ss -ltnp` no pudo abrir netlink por permisos. No se confirmó otro origen Vite ni una sesión sintética utilizable. No se navegó a una ficha ni se tomó una captura. No se inició la API por la prohibición de ejecutar migraciones. La disponibilidad del MCP está comprobada; la ficha visual no. + +Pendiente: escritorio/móvil, ventana baja, zoom 200/400 %, Tab/Shift+Tab, enlaces y menús, Escape y retorno de foco de editores, orden de lectura, nombre completo accesible, no desbordamiento global, contraste, lector de pantalla, estados de carga/refetch/error y volumen. Matriz propuesta de resolución y datos: [escenarios](06-escenarios-mockup.md). Volumen persistente utilizado: **0 compañías, 0 personas, 0 activos y 0 recursos**. El fixture de diseño no sustituye esta validación. diff --git a/docs/auditorias/workplace-dashboard/04-arquitectura-informacion.md b/docs/auditorias/workplace-dashboard/04-arquitectura-informacion.md new file mode 100644 index 0000000..64a4648 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/04-arquitectura-informacion.md @@ -0,0 +1,93 @@ +# Arquitectura de información propuesta + +Fecha: 22 de octubre de 2026. **PROPUESTO**: esta organización prepara un mockup posterior; no describe un dashboard implementado. Fuentes de los datos: [inventario](01-inventario-funcional.md) y [relaciones](02-mapa-relaciones.md). + +## Preguntas y prioridades + +Audiencia asumida: técnico de soporte consultando un puesto concreto. No hay evidencia de entrevistas ni de preferencias de otros roles. Diseñar un resumen operativo de datos registrados, sin sugerir monitorización en tiempo real. + +| Nivel | Pregunta | Contenido | Justificación | +| --- | --- | --- | --- | +| 1. Inmediato | ¿Qué puesto y qué equipo estoy atendiendo? | Compañía, nombre, estado del puesto, sede, responsable, ordenador principal y estado técnico de este | Evita actuar sobre una compañía/equipo equivocados | +| 1. Inmediato | ¿Hay algo registrado que deba revisar? | Perfiles con `needsReview`; configuraciones pendientes o en revisión, cuando sus consultas estén completas | Reutiliza señales reales; no sintetiza una salud global | +| 2. Operativo | ¿Quién lo usa y con qué entorno? | Perfiles y personas; responsable diferenciado; cuenta y resumen de configuraciones por perfil | El ordenador puede servir a varias personas; no hay un ocupante único inferible | +| 2. Operativo | ¿Qué equipamiento y software tiene? | Composición actual, plantilla, estado, software del principal y licencia vinculada | Lo necesario para localizar fichas y preparar intervención | +| 2. Operativo | ¿Qué recursos están configurados aquí? | Carpetas, correo, aplicaciones, certificados e impresoras de cada perfil | Explica la procedencia y evita atribuir permisos de persona al puesto | +| 2. Operativo | ¿Cómo se presta soporte? | Conexiones de soporte del principal, y acceso a su gestión | Pertenece al equipo y facilita consulta técnica; no iniciar conexiones ficticias | +| 3. Secundario | ¿Qué contexto organizativo/remoto hay? | Departamento temporal de la persona, teletrabajo, conexiones remotas, identidades locales, dominio | Información indirecta y con sus propias reglas de vigencia | +| 3. Secundario | ¿Qué cambió? | Última revisión y acceso al historial completo; procesos vinculados por contexto si se decide incluirlos | Mantiene trazabilidad sin convertir todas las revisiones de otras entidades en actividad propia | + +La primera vista debe distinguir «estado del puesto», «estado técnico del equipo» y «configuración registrada». No usar un único semáforo «Todo correcto». + +## Composición recomendada + +1. **Cabecera de identidad**: retorno, contexto de compañía, nombre, estado, sede y responsable explícito aunque esté ausente. Editar como acción principal; menú de ciclo de vida, conservando confirmaciones. +2. **Resumen del ordenador y señales de revisión**: nombre enlazado, plantilla, estado técnico; resumen de relaciones y señales verificadas. Si no hay ordenador, explicación y acción de completar, sin aparentar error de sistema. +3. **Personas y perfiles**: perfiles activos primero, con acceso a archivados; responsable independiente. Tabla con perfil, persona, cuenta, estado, revisión y acceso al entorno. La cuenta necesita Access para resolver su etiqueta. +4. **Equipamiento**: ordenador principal distinguido y lista compacta del resto, agrupación opcional por plantilla. No repetir una tarjeta completa del principal si ya está descrito arriba. +5. **Software del ordenador principal**: resumen con nombre, versión, licencia y estado, y detalle mediante el panel existente. Diferenciar instalaciones de equipo y aplicaciones configuradas por perfil. +6. **Configuraciones por perfil**: selector o filtro de perfil explícito, agrupación de carpetas/correo/apps/certificados/impresoras. Mostrar persona y perfil de origen incluso cuando se use una lista agregada. +7. **Soporte y contexto técnico**: soporte del equipo como bloque operativo; dominio, entorno local y conexiones remotas como detalle contextual, sin mezclar publicación SMB y carpetas consumidas. +8. **Último cambio e historial**: vista breve de la última revisión; historial íntegro en sección secundaria. No llamarlo «actividad en directo». + +Los bloques sin datos se compactan con su motivo y acción pertinente. Los bloques que no existen en el producto (tickets, salud online) se omiten. Un fallo de consulta conserva el bloque con estado recuperable. + +## Catálogo de widgets candidatos + +Todos los componentes sugeridos deben respetar sus APIs reales; E31/E32 identifica las primitivas inspeccionadas. No hay necesidad demostrada de un componente genérico `DashboardWidget`. + +| Widget | Información y decisión | Fuente de datos | Componente sugerido | Prioridad | Viabilidad | +| --- | --- | --- | --- | --- | --- | +| Identidad | Reconocer puesto y ámbito | Workstation + Company | Cabecera actual, `EntityStatus`, `BrowserLink`, `WorkstationActions` | 1 | Reutilización directa y composición | +| Equipo principal | Identificar host y abrirlo | `computerAssetId`, AssetDTO | `AssetSummaryCard` o resumen con `DetailField` | 1 | Adaptación menor | +| Relaciones resumidas | Saber cuánto detalle hay y abrirlo | Miembros, perfiles y personas deduplicados | Contadores de `DetailPanel`; `StatisticCard` solo si aporta navegación clara | 1–2 | Adaptación menor; fórmula definida abajo | +| Revisión registrada | Localizar perfiles/configuraciones con problemas documentados | DTO `needsReview`, configuración | Texto/estado con icono y enlace; componentes de estado existentes | 1–2 | Agregación nueva; no nuevo dato de dominio | +| Perfiles y usuarios | Comparar persona/cuenta/estado | profileApi + Company + Access | `WorkstationProfiles`/`EntityTable`, `PersonAvatar` | 2 | Extensión de composición actual | +| Equipamiento compacto | Comparar miembros al crecer | Inventario + especificaciones | `EntityTable`, `BrowserLink`, `EntityStatus` | 2 | Adaptación menor; conserva acciones de modal | +| Software | Consultar instalación y licencia | softwareApi + Resource Inventory + catálogo | `SoftwareInstallationsPanel`, resumen reducido | 2 | Panel reutilizable; resumen requiere adaptación | +| Configuraciones por perfil | Detectar pendientes y navegar | Cinco APIs; DTO de revisión/cobertura | Lógica `buildEnvironment`, `EnvironmentSection` como referencia, `DetailPanel`, `EntityTable` | 2 | Integración nueva; evitar copiar su UI completa | +| Soporte | Consultar destino/herramienta registrados | Support API por activo | `AssetSupport` | 2 | Reutilizable, pero revisar acciones y dependencias antes de incrustar | +| Contexto remoto | Distinguir conexiones de equipo y persona | remoteApi + teleworkApi | `RemoteSummary`, `TeleworkAuthorizationSummary` | 3 | Nuevos filtros y etiquetas de procedencia | +| Publicaciones / directorio / entorno local | Entender servicios y ámbito del activo | APIs paginadas específicas | Componentes de relación existentes de Assets; sublista compacta | 3 | Integración dirigida; sin consultas por tarjeta | +| Última revisión | Saber cuándo cambió el puesto | history del puesto | `TimeLine`, `WorkstationHistory` | 3, resumen breve visible | Adaptación menor | +| Procesos relacionados | Consultar trabajo de persona o referencias a activos/perfiles | Process API | Enlaces al proceso y lista contextual | 3, opcional | Agregación nueva; no imprescindible para primera versión | +| Salud, uptime, tickets, tendencias | Sin fuentes activas pertinentes | Ninguna confirmada | Ninguno | Fuera | Funcionalidad no soportada; no simular | + +## Fórmulas de contadores + +**PROPUESTO**, salvo el número actual de miembros de la ficha. Calcular solo tras carga satisfactoria, con ámbito de compañía verificado. Si hay paginación parcial indicar «cargados» o no mostrar total; nunca `items.length` como total universal. + +| Etiqueta | Fórmula y alcance | +| --- | --- | +| Activos del puesto | Cardinalidad del conjunto `values.assetIds`; incluye principal una vez. No sumar impresoras configuradas o activos históricos | +| Perfiles actuales | Perfiles de misma compañía, `assetId === computerAssetId` y `active === true` | +| Personas usuarias actuales | Distintos `personId` de los perfiles actuales. No sumar al responsable si no tiene perfil | +| Personas relacionadas | Unión deduplicada de responsable y personas de todos los perfiles del host; reproduce selección actual, pero no equivale a uso vigente | +| Perfiles en revisión | Perfiles actuales con `needsReview === true`; es señal de referencia, no incidencia | +| Configuraciones pendientes | Filas de las cinco familias con `profileId` en perfiles actuales y `state === pending`; identidad por familia + id | +| Retiradas pendientes | Misma población, `state === removal_pending` | +| Configuraciones en revisión | Misma población abierta, con `needsReview`; cobertura insuficiente de carpeta se puede indicar separadamente. No sumar como categorías excluyentes | +| Software configurado | Instalaciones del principal con `state === configured`; distinguir de aplicaciones de perfil | +| Último cambio | Última revisión del puesto, no máximo de todas las revisiones de la compañía | + +La señal de revisión puede solaparse con pendiente/configurado: no representar esas cantidades como porciones de un total. Mostrar cero cuando esté confirmado; durante carga/error usar el estado del bloque, no cero. + +## Responsive, accesibilidad y acciones + +Escritorio: resumen de host y personas en proximidad; tabla de perfiles de ancho suficiente; equipamiento/configuraciones con jerarquía uniforme. Evitar reproducir navegación global dentro de cada tarjeta. + +Portátil/tablet: distribuir por ancho efectivo de contenedor con el shell presente; no adoptar los umbrales de inventario de activos como regla del puesto. Móvil: orden DOM identidad → principal → perfiles → equipamiento → configuraciones/software → soporte/contexto → historial. Tablas con scroll interno y acceso al valor completo; acciones de consulta/edición alcanzables sin hover. + +Usar encabezados semánticos, regiones nombradas, estado textual además de color, enlaces reales para destinos y botones para acciones. No ocultar comparaciones esenciales entre pestañas. Pestañas posibles para detalle extenso e historial; la selección de perfil debe seguir visible. + +Los formularios se abren en los diálogos existentes; no incrustar alta o edición en el dashboard. Conservar foco de retorno, Escape, estados pendientes, errores y no cerrar antes de éxito. Textos nuevos futuros deben tener es/en. En celdas sin valor usar exclusivamente «—»; estados vacíos completos explican motivo y siguiente acción. + +## Decisiones abiertas + +- Confirmar si «personas usuarias» debe excluir inactivos por defecto y cómo ofrecer los archivados. El responsable siempre conserva rol propio. +- Elegir hasta dónde extender software/soporte: propuesta inicial limitada al principal, con navegación al resto de activos. +- Confirmar si configuraciones agregadas necesitan filtro por perfil o secciones por perfil. Comparar con un puesto de alta densidad antes de decidir. +- Priorizar soporte de equipo frente a contexto de teletrabajo según tareas reales de técnicos. +- Decidir si los procesos aportan valor en esta ficha; quedan fuera del mínimo inicial hasta justificar su consulta. + +Estas decisiones no impiden un mockup conceptual basado en las hipótesis indicadas; no autorizan cambiar reglas ni contratos. diff --git a/docs/auditorias/workplace-dashboard/05-viabilidad-tecnica.md b/docs/auditorias/workplace-dashboard/05-viabilidad-tecnica.md new file mode 100644 index 0000000..fbf0c70 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/05-viabilidad-tecnica.md @@ -0,0 +1,105 @@ +# Viabilidad técnica y validación + +Fecha: 22 de octubre de 2026. Investigación estática, sin modificaciones de implementación. Referencias E: [inventario](01-inventario-funcional.md). + +## Consultas reutilizables + +| Fuente / endpoint GET | Cliente y claves existentes | Coste y uso propuesto | +| --- | --- | --- | +| `/api/companies/:companyId` | Compañía ya recibida por la ficha; `companyKeys.detail` | Personas, sedes, departamentos y asignaciones; evitar segunda copia de estado | +| `/api/assets/:companyId` | `inventoryApi.get`, `inventoryKeys.company` de Assets | Toda la compañía, activos/puestos/relaciones/historiales; fuente actual del puesto | +| `/api/asset-catalog/:scope` | `catalogApi`, `catalogKeys.scope` de Assets | Completa plantillas; usar specification del activo cuando proceda | +| `/api/work-profiles/:companyId` | `profileApi.get`, `profileKeys.company` | Todos los perfiles con revisiones; filtrar por host y compañía | +| `/api/software-installations/:companyId` | `softwareApi.get`, `softwareKeys.company` | Todas las instalaciones; filtrar por host, no pedir por fila | +| `/api/resources/:companyId` | `inventoryApi.get`, `inventoryKeys.company` de Resources | Recursos y proyecciones, datos para software/configuraciones | +| `/api/access/:scope` | `getAccess`, `accessKeys.scope` | Catálogo y accesos; cobertura/reviews. No hay filtro de puesto | +| `/api/folder-mappings/:companyId` | `folderApi`, `folderKeys.company` | Mapeos con cobertura de cuenta y revisión | +| `/api/mail-configurations/:companyId` | `mailApi`, `mailKeys.company` | Configuraciones nuevas y snapshots legacy; no unir las legacy por `profileId` inexistente | +| `/api/mailboxes/:companyId` | Contratos `MailboxesResponseSchema`; E36 | Buzones y usos temporales; validar procedencia de acceso de persona | +| `/api/application-configurations/:companyId` | `applicationApi`, `applicationKeys.company` | Aplicaciones de perfil, distintas de instalación en host | +| `/api/certificate-configurations/:companyId` | `certificateApi`, `certificateKeys.company` | Configuración, caducidad/revisión del certificado | +| `/api/printer-configurations/:companyId` | `printerApi`, `printerKeys.company` | Perfil e impresora; no convertirla en miembro exclusivo | +| `/api/telework-authorizations/:companyId` | `teleworkApi`, `teleworkKeys.company` | Contexto de persona y efectividad calculada por servidor | +| `/api/remote-connections/:companyId` | `remoteApi`, `remoteKeys.company` | Conexiones de personas; filtrar explícitamente por origen/destino/persona | +| `/api/asset-support/:companyId/:assetId` | Cliente de soporte y `supportKeys` de Assets | Consulta por equipo; evitar cargar cada miembro para un resumen inicial | +| `/api/asset-infrastructure/:companyId` | `InfrastructureResponseSchema`; E35 | Roles, interfaces y direcciones registradas; filtrar por `assetId` | +| `/api/local-identity-environments/:companyId` | Contratos `LocalIdentityQuerySchema` | Filtros por activo/entorno, paginación `nextAfterId`; 100 máximo por página | +| `/api/asset-directory-memberships/:companyId` y `/:assetId/current` | Contratos `DirectoryMembershipQuerySchema` | Paginación y pertenencia actual; contexto de directorio del host | +| `/api/asset-network-share-publications/:companyId` | Contratos de publicaciones E20 | `assetId`, `resourceId`, `includeEnded`, cursor, pageSize <=100; no total global en respuesta | +| `/api/processes/:companyId` | Process API | Procesos de personas; opcional, sin referencia directa al puesto | + +Las rutas se contrastan con registradores HTTP; los identificadores de catálogos se verifican en el cliente instalado. Los GET de configuraciones citados rechazan query desconocida; no añadir `workstationId` o `profileId` a HTTP por imitar el filtro de la UI. Las rutas de navegador contextuales de perfil no implican filtros backend equivalentes. + +## Reutilización y límites + +**Reutilización directa**: datos básicos del puesto, cabecera/acciones, historial, enlaces, diálogos y panel de software. + +**Adaptación menor**: resumen de equipo, contadores definidos, tabla de equipamiento compacta, cuenta en tabla de perfiles, vista de última revisión. + +**Desarrollo nuevo de composición**: filtros de configuraciones por perfiles del host, procedencia visible, estados parciales, resúmenes y enlaces contextuales de remoto/soporte/directorio. Los datos existen; el agregado de pantalla no está implementado. + +**Funcionalidad no soportada**: tickets activos, telemetría, presencia actual, puntuación de salud y comprobación real de permisos SMB. No desbloquearlas con datos inventados en el fixture. + +El dashboard inicial puede construirse con consultas existentes. No hay necesidad demostrada de nuevo endpoint. Si una medición posterior detecta payload excesivo, latencia o incoherencia entre snapshots de consultas independientes, justificar una lectura agregada/paginada antes de diseñarla. Sería un contrato nuevo con autorización y pruebas propias, fuera de esta auditoría. + +## Composición y dependencias + +La ficha vive en `modules/companies/src/web/workstations/detail/`; el agregado y comandos, en Assets. `useEnvironmentQueries` y `buildEnvironment` de Companies ya reúnen el contexto de perfil (E30). El primero solicita once fuentes y condiciona su `data` a que todas existan: no copiarlo sin revisar la recuperación parcial. El segundo es cálculo puro y conserva `profileId`/compañía y estados abiertos. E37 muestra el registro real de módulos en `identity-runtime.ts`, llamado desde el arranque; no todas las rutas se registran directamente en `index.ts`. + +Reutilizar las APIs públicas de módulos; no importar repositorios ni modelos ORM en frontend. Componentes genéricos de `@repo/rdx-ui` deben recibir datos semánticos y callbacks, sin DTO específico de puesto. No modificar vendor ni extraer una plataforma genérica de widgets sin consumidores reales. + +Las composition roots son explícitas. No se confirmó un sistema dinámico de plugins que deba adaptarse para esta pantalla. Sí hay proveedores/contratos tipados de relaciones (`@metis/relationship-contracts`) y servicios entre módulos: las publicaciones SMB y directorio se componen en sus propietarios. No introducir acceso cruzado a tablas como supuesto «plugin de dashboard». + +## Rendimiento y coherencia + +| Riesgo | Evidencia / alcance | Decisión recomendada | +| --- | --- | --- | +| Payload por compañía | Inventario/perfiles/configuraciones incluyen revisiones; no GET individual de puesto localizado | Usar caché existente y carga por necesidad; medir antes de nuevo endpoint | +| Multiplicar queries por perfil | E30 hace 11 consultas por compañía | Compartir claves y cargar cada familia una vez; filtrar después. No montar un hook agregador por fila | +| N+1 en soporte/publicaciones | APIs admiten consulta por activo; puesto hasta 200 miembros | Consultar principal primero; detalle del resto bajo demanda. No fan-out masivo inicial | +| Coste de filtrado local | E11 hace `find` de activos/catálogos por miembro; controller filtra perfiles | Mapas/sets cuando haya recorrido repetido relevante; no `useMemo` preventivo sin necesidad | +| Conteo de página incompleta | Publicaciones y entornos locales paginados | Seguir cursor para total o etiquetar parcial; no confundir recuento cargado con total | +| Lecturas no atómicas entre APIs | Queries independientes y momentos distintos | Datos parciales y refetch dirigidos; no prometer fotografía transaccional global | +| Cambio de host | Las relaciones derivan de `computerAssetId` actual | Recalcular al cambiar dato de query; no copiar relaciones en estado ni sincronizar con efectos | +| Fallo secundario bloquea todo | E30 retorna `data: undefined` si falta una fuente | Estado por bloque; conservar datos previos al actualizar y reintento específico | + +No se midieron latencias, volumen de respuesta ni planes SQL. El lector de puestos usa consultas en lote de raíces, revisiones y miembros; no hay base para afirmar un N+1 de BD por puesto en ese lector (E07). Hay recorridos locales repetidos y varios loaders de contexto; distinguir riesgo de coste real medido. + +## Mutaciones e invalidación + +- Guardar el puesto invalida `inventoryKeys.company(companyId)` en su controller; el host y relaciones derivadas se recalculan al refrescar. +- Renombrar relee inventario para conservar composición vigente y evita enviar valores capturados al comenzar edición; no se encontró control optimista de revisión en el contrato de puesto. No prometer resolución completa de concurrencia. +- Ciclo de vida invalida inventario; ficha solicita refetch de perfiles tras desmantelar. Esto no desactiva perfiles en backend. +- Software, perfiles y configuraciones conservan sus comandos y claves propias. La integración futura debe revisar cada consumidor antes de introducir contadores derivados; no invalidar toda la caché. +- No descartar promesas con `void` ni ocultar errores en lógica nueva. Los `.catch(() => undefined)` existentes de reintento no se corrigen durante esta documentación; TanStack Query expone el error de consulta, pero no debe suponerse que todos los callbacks futuros harán lo mismo. + +## Autenticación, ámbito y permisos + +**CONFIRMADO** E33: `requireAuthenticated` valida sesión y versión de autenticación; los routers de puesto y consultas relacionadas reciben ese middleware. El `actorId` se obtiene de sesión y los cuerpos/params se validan con Zod. La pertenencia de persona, sede y miembros se valida con contextos por compañía y restricciones relacionales; las escrituras de inventario usan transacciones y bloqueo de compañía. + +No se localizó una ACL usuario–compañía ni un rol específico por operación de puesto en el recorrido de rutas, use cases y repositorio. El middleware autenticado no recibe `companyId` ni consulta membresías del operador. En relaciones tipadas, `SessionRelationshipReadAccess` verifica sesión y existencia de compañía, y utiliza contextos emitidos por servidor; no acredita una ACL por compañía (E34). + +Por tanto, aislamiento de datos por IDs y pertenencia de entidades está confirmado en las rutas inspeccionadas; autorización granular del operador por compañía **NO ESTÁ CONFIRMADA**. No inferirla del filtrado frontend ni simular distintos roles como si ya existieran. El dashboard debe reutilizar el contrato efectivo y cualquier nueva lectura debe conservar comprobaciones en servidor. No exponer accesos de compañías distintas ni considerar la compañía atendida como tenant de la empresa de soporte. + +No se inspeccionaron credenciales ni se generaron conexiones remotas reales. No incluir tokens, contraseñas o destinos sensibles en un mockup. + +## Registro de investigación y validación + +- Ejecutados desde WSL/bash: `pwd`, `git status --short`, `node --version`, `pnpm --version`; versiones correctas. El estado inicial contiene cambios previos ajenos a esta tarea. +- Inspección con `rg`, `rg --files`, lecturas de archivos y manifests/skills. Se revisaron fuentes activas, contratos, tests existentes, DDL y documentación; no se verificó una BD desplegada. +- MCP: `browser_tabs` respondió `about:blank`. Curl a `http://localhost:5173/` falló; `ss -ltnp` no pudo usar netlink. No se confirmó URL Vite ni sesión sintética. No se inició servidor, no se ejecutaron migraciones ni se sembraron datos. +- JSON: 16 bloques de `sources` validados contra sus schemas Zod existentes. Comprobados miembros, referencias, reserva exclusiva, unicidad de perfiles activos, coherencia de historial y contadores con `workstationRelations`/`buildEnvironment`. `projectAccess` reproduce exactamente las coberturas y revisiones del ejemplo. Son comprobaciones locales, no guardados de dominio ni consultas a la BD. +- Pruebas existentes dirigidas: los **tres archivos** finalizaron correctamente, sin fallos. Comando reproducible desde raíz: + +```bash +TSX_TSCONFIG_PATH=tsconfig.json node --import tsx --test \ + modules/assets/src/api/domain/workstation.test.ts \ + modules/companies/src/web/workstations/detail/helpers/workstation-relations.test.ts \ + modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx +``` + +- Una comprobación auxiliar inicial de Access falló al importar `@repo/rdx-ddd` desde el eval de la raíz, donde no es dependencia directa. Se corrigió la comprobación usando la ruta local del VO, sin modificar configuración; la ejecución final fue satisfactoria. No es un fallo del producto ni se ocultó alterando sus tipos. +- Se comprobaron 108 enlaces locales y todas las rutas completas de código citadas entre backticks; sin destinos inexistentes. El catálogo contiene 37 grupos y 89 referencias a archivos/líneas verificadas. Se revisó el contenido completo de los ocho entregables y se comprobó el diff de cada fichero nuevo con `git diff --no-index --check`: sin diagnósticos de whitespace. En modo `--no-index`, el código 1 por archivo nuevo no se interpreta como un error de formato. +- El fixture contiene **1 compañía, 2 personas, 5 activos, 7 recursos, 4 puestos y 4 perfiles**. No se cargaron en persistencia. Las comprobaciones locales no cumplen ni sustituyen el volumen funcional/visual exigido por AGENTS.md. + +No se ejecutan `pnpm typecheck`, `pnpm lint`, `pnpm test`, `pnpm check` ni `pnpm build` globales: los únicos cambios son Markdown y un fixture de diseño. Las suites de integración quedan pendientes por su persistencia y por la prohibición de migraciones/datos. No declarar una auditoría visual, de rendimiento o de accesibilidad como superada. diff --git a/docs/auditorias/workplace-dashboard/06-escenarios-mockup.md b/docs/auditorias/workplace-dashboard/06-escenarios-mockup.md new file mode 100644 index 0000000..31b7494 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/06-escenarios-mockup.md @@ -0,0 +1,117 @@ +# Escenarios y contrato del mockup posterior + +Fecha: 22 de octubre de 2026. Son escenarios **PROPUESTOS**, no recorridos ejecutados en navegador. Fixture: [07-datos-ejemplo.json](07-datos-ejemplo.json). Modelo y evidencias: [inventario](01-inventario-funcional.md). + +## Contrato del fixture + +El JSON no es un endpoint ni una propuesta de persistencia JSON. Es un contenedor de diseño con bloques `metadata`, `selection`, `sources`, `presentation` y `scenarios`. `sources` conserva formas de DTO existentes; el envoltorio, los estados de pantalla, los contadores y los escenarios son **PROPUESTOS**. No deben enviarse al servidor como un comando. + +Los nombres, personas, compañías, identificadores y direcciones son ficticios. Fecha de referencia fija: 2026-10-22. Los UUID usan forma v7, con sufijos legibles para rastrear referencias; no representan una generación cronológica real. El operador de revisiones es una identidad ficticia, no un usuario creado. Los datos no contienen contraseñas ni credenciales. + +### Correspondencia de propiedades + +La notación `[]` se aplica a cada elemento; los schemas enlazados en E01–E37 definen cada campo interno. «Disponible» se refiere al contrato de origen, no a que el dashboard ya lo consulte. + +| Propiedad | Fuente real / tipo y cardinalidad | Disponibilidad / agregación | Etiqueta | +| --- | --- | --- | --- | +| `metadata` | Sin DTO: objeto de descripción, fecha fija y límites | Solo fixture | PROPUESTO | +| `selection.companyId`, `selection.workstationId` | UUID de contexto/ruta; uno de cada | Ya seleccionables en navegador | CONFIRMADO para datos; PROPUESTO para envoltorio | +| `sources.company` | `CompanySchema`: objeto con id/name/active y arrays contacts/sites/people/departments/assignments | GET compañía; ya recibida por ficha | CONFIRMADO | +| `sources.inventory.workstations[]` | `WorkstationSchema`: id/companyId/values/history | Inventario; cada puesto con 1..N revisiones | CONFIRMADO | +| `sources.inventory.assets[]` | `AssetSchema`: id/companyId/values/specification/displayStatus/assigned/history | Inventario; equipo principal y otros, incluida impresora fuera de miembros | CONFIRMADO | +| `sources.inventory.relationships[]` | `RelationshipSchema`, 0..N | Inventario; vacío en ejemplo | CONFIRMADO | +| `sources.profiles.profiles[]` | `WorkProfileSchema`: id/companyId/values/needsReview/history | API perfiles; requiere filtro de host | CONFIRMADO | +| `sources.resources.resources[]` | `ResourceSchema`: id/companyId/values/specification/history, networkShare opcional | API recursos; cada característica por fieldId de specification | CONFIRMADO | +| `sources.access.items[]` | `AccessItemSchema`: id/companyId/values/history, values discriminado por kind | API Access; catálogo, cuenta, relación persona, recurso proyectado, necesidad, grant y cobertura | CONFIRMADO | +| `sources.access.coverage[]` | `{needId, covered, missingPermissionIds}` | Proyección backend; no recomputar como autorización real | CONFIRMADO | +| `sources.access.reviews[]` | `{itemId, reason}` | Proyección backend, vacía en el caso principal | CONFIRMADO | +| `sources.folders.mappings[]` | `FolderMappingSchema`: perfil, need, cuenta elegida, UNC, unidad, state, notes, revisiones y tres resultados de cobertura/revisión | API carpetas; filtrar por perfil y compañía | CONFIRMADO | +| `sources.software.configurations[]` | `SoftwareInstallationSchema`: activo, recurso, licencia, versión, state, notes, revisión | API software; filtrar por principal | CONFIRMADO | +| `sources.applications.configurations[]` | `ApplicationConfigurationSchema`: perfil, recurso, delivery, versión, state, notes, revisión | API aplicaciones; no fusionar con instalación | CONFIRMADO | +| `sources.printers.configurations[]` | `PrinterConfigurationSchema`: perfil, printerId, connection, isDefault, state, notes, revisión | API impresoras; referencia a activo ajeno a composición | CONFIRMADO | +| `sources.certificates.configurations[]` | `CertificateConfigurationSchema`: perfil, recurso, ubicación, state, notes, revisión, expiresOn y expired | API certificados; fecha de referencia fija | CONFIRMADO | +| `sources.mail.configurations[]` | `MailConfigurationSchema`: perfil/buzón/método/state/notes/address snapshot, history, needsReview, legacy | API correo; caso nuevo, no legacy | CONFIRMADO | +| `sources.mailboxes.mailboxes[]`, `uses[]` | `MailboxesResponseSchema`: buzón con dirección/tipo/contexto/cuenta/estado/notas e historial; uso con buzón/persona/periodo/historial | API buzones; uso vigente de la persona del perfil | CONFIRMADO | +| `sources.telework.authorizations[]` | `TeleworkAuthorizationSchema`: persona/active/startsOn/expiresOn/notes, status/effective/needsReview/history | API teletrabajo; contexto de persona | CONFIRMADO | +| `sources.remote.connections[]` | `RemoteConnectionSchema`: persona, origen/destino, sede, método, VPN, configuración, notas; resultados e historial | API remoto; caso desde equipo principal | CONFIRMADO | +| `sources.support.connections[]`, `tools[]` | `SupportListSchema`; conexión tiene assetId/values/history, herramienta id/name/active | API soporte por equipo | CONFIRMADO | +| `sources.publications.publications[]`, `nextCursor` | `NetworkSharePublicationPageSchema`: publicación con assetId/resourceId/periodo/actores/predecessor | API paginada; en ejemplo publicador es otro activo, no el puesto | CONFIRMADO | +| `presentation.relatedProfileIds`, `activeProfileIds`, `relatedPersonIds` | UUID[] calculados por filtros/set descritos en 02/04 | Join local sobre fuentes completas | DERIVABLE; forma PROPUESTA | +| `presentation.counts` | Enteros para miembros/perfiles/personas/configuraciones; fórmulas en 04 | Solo fuentes completas; fecha fija | DERIVABLE; forma PROPUESTA | +| `presentation.latestRevisionId` | UUID de última revisión del puesto seleccionado | No feed combinado | DERIVABLE | +| `scenarios[]` | id/name/baseWorkstationId, opcional displayUnavailable y descripción | Instrucciones de estado de pantalla, no propiedades del dominio | PROPUESTO | + +No se añaden `tickets`, «salud» ni permisos efectivos del puesto al fixture. Tampoco se incluyen todos los procesos, dominios y entornos locales posibles: están auditados, pero quedan como detalles opcionales fuera del dataset mínimo. El ejemplo de publicación SMB muestra la diferencia entre el servidor que publica y el puesto que consume por configuración. + +La validación de schemas comprueba forma de DTO y referencias locales; no sustituye las políticas de creación, capacidades de plantilla ni restricciones de una BD. La fixture no incluye un catálogo de producción instalable ni pretende ser un seed HTTP. + +Volumen materializado: **1 compañía, 2 personas, 5 activos, 7 recursos, 4 puestos y 4 perfiles**. Se validaron 16 bloques de DTO, invariantes locales de composición, referencias y contadores. La proyección de Access coincide con las coberturas/revisiones del ejemplo. Los dos perfiles activos del puesto principal comparten equipo, no identidad; la impresora configurada está fuera de su composición exclusiva. + +## Escenarios de diseño + +| ID | Caso compatible con modelo | Preparación del ejemplo / resultado esperado | +| --- | --- | --- | +| S01 | Puesto completo | Seleccionar puesto principal del fixture. Responsable y dos perfiles activos de personas diferentes; uno archivado adicional. Equipo + monitor; software, configuración de carpetas, correo, app, certificado e impresora; última revisión | +| S02 | Sin responsable | Seleccionar puesto de laboratorio: `personId=null`, pero tiene host y perfil activo. Mostrar responsable «—» y uso de la persona del perfil. No deducir «nadie lo utiliza» | +| S03 | Sin activos | Seleccionar puesto pendiente: activo, `assetIds=[]`, sin ordenador ni perfiles derivados. CTA editar/completar. Deshabilitar creación de perfil e instalación sin host; mostrar vacío coherente, no fallo | +| S04 | Muchas relaciones | Variante a preparar desde S01: 80 miembros únicos incluido principal, 50 personas y 60 perfiles coherentes con unicidad activa por persona/host; generar archivados con identidades propias. Software/configuraciones extensos con nombres largos. No superar 200 miembros | +| S05 | Información incompleta | Desde S01, `scenarios.displayUnavailable` simula error de perfiles y carpetas en presentación, sin cambiar DTO a relaciones inválidas. Mantener cabecera/host conocidos; usuarios parciales, contador no definitivo, error y reintento de cada bloque | +| S06 | Desmantelado | Seleccionar puesto inactivo del fixture: revisión inicial con equipo/responsable y última revisión sin ellos. Actual sin miembros/perfiles derivados; historial conserva referencias. No incluir equipo histórico en contador actual | +| S07 | Refetch con datos previos | Desde S01 mantener datos, selección y foco; feedback discreto. Si falla, avisar que hay datos anteriores y reintentar solo fuente afectada | +| S08 | Sin coincidencias | Búsqueda de perfiles/software sin resultados conserva búsqueda y ofrece limpiar; no confundir con ausencia total | +| S09 | UUID ausente / sesión sin acceso | Destino válido no existente muestra ausencia sin seleccionar otro puesto; 401 ofrece flujo de acceso existente. No diseñar un rol por compañía no implementado | +| S10 | Principal sustituido | Variante cambia principal por otro miembro y registra revisión; perfiles/software anteriores siguen en activo anterior. Mostrar nuevo contexto y permitir consultar historial, sin simular migración automática | +| S11 | Permiso documental desconocido | Mapping con cuenta ausente → cobertura de cuenta `unknown`; distinguir configured, permissionsCovered y revisión. No sustituir por acceso concedido/denegado real | + +S01/S02/S03/S06 tienen puestos concretos en el JSON; S05 describe estados sobre S01; S04 y S07–S11 son especificaciones reproducibles pendientes de materializar/ejecutar. No presentar todos como fixtures completas ni pruebas superadas. + +## Esquema de composición para el futuro mockup + +Escritorio, orden de lectura propuesto: + +```text +Volver · Compañía / Puestos +Nombre del puesto · Estado Editar · Más acciones +Sede · Responsable +Ordenador principal + estado técnico | Revisiones y relaciones resumidas +Personas y perfiles (cuenta, estado, revisión, abrir entorno) +Equipamiento compacto | Software del principal +Configuraciones del perfil seleccionado (procedencia visible) +Soporte del equipo | Contexto remoto/organizativo +Último cambio → Historial completo +``` + +Móvil: mismo orden en una columna; cabecera con acciones que envuelven, filtro de perfil visible, tablas dentro de su área desplazable, contexto secundario plegable con títulos descriptivos. Ningún dato esencial requiere hover. Es un wireframe textual, no un mockup definitivo. + +## Comprobaciones posteriores + +Matriz prevista: 1920×1080, 1440×900, 1366×768, 1024×768, 768×1024, 390×844 y 320×640, además de zoom 200/400 %. Evaluar teclado/foco, vuelta a listado/perfil, scroll, nombres y UNC largos, fechas exactas, 0/false/ausente, permisos, carga parcial y no coincidencias. + +Para pruebas funcionales/visuales persistentes preparar al menos 15 compañías y una principal con 50 personas, 50 activos y 50 recursos, ampliando puestos/perfiles coherentes. Usar exclusivamente `metis_desk_test` aislada y desechable según procedimiento Playwright. Esta fase no prepara ni modifica esa BD: lo prohíbe la petición. El fixture de diseño tiene un volumen menor declarado en metadata y validado al cierre; no acredita aislamiento real entre compañías ni rendimiento. + +## Prompt para crear el mockup después + +```text +Lee los ocho entregables de docs/auditorias/workplace-dashboard/ y las instrucciones +vigentes de AGENTS.md, DESIGN.md y la matriz de skills. Diseña un mockup de la ficha +de Workstation orientada a consulta operativa, usando 04 como arquitectura y 07 como +fixture ficticio. Comprueba contratos reales de componentes antes de implementarlo. + +Usa frontend-design para la composición y ux-dashboard como complemento. Conserva +Inter, tokens, identidad del shell, shadcn/Base UI Vega y componentes de @repo/rdx-ui. +No modifiques vendor ni añadas dependencias. No conectes mutaciones reales ni migres BD. + +Distingue responsable, usuarios actuales y perfiles archivados; equipamiento exclusivo, +impresoras configuradas, software del equipo y aplicaciones del perfil. Identifica la +procedencia de recursos y cobertura documental. No incluyas tickets, salud online, +ocupación en directo ni accesos efectivos inventados. + +Representa S01, S02, S03, S05 y S06 y prepara S04 con el volumen y reglas documentados. +Declara las hipótesis de diseño pendientes. Mantén acciones/diálogos reconocibles, +enlaces con destinos existentes, estados por bloque y ausencia «—» en celdas. + +Antes de elegir destino/formato del prototipo, comprueba convenciones existentes y +alcance autorizado de la nueva tarea. Valida su interfaz en Playwright MCP cuando haya +un entorno permitido; informa exactamente qué se ha probado y qué sigue pendiente. +No presentes el mockup como una funcionalidad integrada en producción. +``` diff --git a/docs/auditorias/workplace-dashboard/07-datos-ejemplo.json b/docs/auditorias/workplace-dashboard/07-datos-ejemplo.json new file mode 100644 index 0000000..786a1e5 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/07-datos-ejemplo.json @@ -0,0 +1,1762 @@ +{ + "metadata": { + "purpose": "Fixture ficticio para diseño, no seed ni contrato HTTP agregado.", + "evidence": "PROPUESTO: envoltorio de diseño; sources conserva DTO existentes.", + "referenceDate": "2026-10-22", + "locale": "es", + "timezone": "Europe/Madrid", + "fictitious": true, + "counts": { + "companies": 1, + "people": 2, + "assets": 5, + "resources": 7, + "workstations": 4, + "profiles": 4 + }, + "limits": [ + "No prueba persistencia ni capacidades de plantillas instaladas.", + "No contiene tickets, salud online ni credenciales.", + "No satisface el volumen requerido de una auditoría funcional o visual." + ] + }, + "selection": { + "companyId": "019a0a00-0000-7000-8000-000000000001", + "workstationId": "019a0a00-0000-7000-8000-000000000010" + }, + "sources": { + "company": { + "id": "019a0a00-0000-7000-8000-000000000001", + "name": "Compañía ficticia de servicios y documentación del Noroeste", + "active": true, + "contacts": [], + "people": [ + { + "id": "019a0a00-0000-7000-8000-000000000004", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "name": "Ángela Muñoz de la Peña", + "position": "Técnica de administración", + "email": "angela@example.invalid", + "phone": null, + "active": true, + "isContact": false + }, + { + "id": "019a0a00-0000-7000-8000-000000000005", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "name": "Íñigo Fernández del Río", + "position": null, + "email": "inigo@example.invalid", + "phone": null, + "active": true, + "isContact": false + } + ], + "sites": [ + { + "id": "019a0a00-0000-7000-8000-000000000003", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "name": "Sede de pruebas — área administrativa", + "active": true, + "address": null, + "phone": null + } + ], + "departments": [ + { + "id": "019a0a00-0000-7000-8000-000000000006", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "name": "Administración y gestión documental", + "active": true + } + ], + "assignments": [ + { + "id": "019a0a00-0000-7000-8000-000000000007", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "personId": "019a0a00-0000-7000-8000-000000000004", + "departmentId": "019a0a00-0000-7000-8000-000000000006", + "position": "Responsable de documentación", + "startDate": "2026-09-01", + "endDate": null, + "isPrimary": true, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000020001", + "recordedAt": "2026-09-01T08:00:00.000Z", + "position": "Responsable de documentación", + "startDate": "2026-09-01", + "endDate": null, + "isPrimary": true + } + ] + } + ] + }, + "inventory": { + "workstations": [ + { + "id": "019a0a00-0000-7000-8000-000000000010", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Administración — documentación y atención interna", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetIds": [ + "019a0a00-0000-7000-8000-000000000020", + "019a0a00-0000-7000-8000-000000000021" + ], + "computerAssetId": "019a0a00-0000-7000-8000-000000000020" + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010007", + "values": { + "name": "Administración — puesto inicial", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetIds": [ + "019a0a00-0000-7000-8000-000000000020" + ], + "computerAssetId": "019a0a00-0000-7000-8000-000000000020" + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-01T08:00:00.000Z" + }, + { + "id": "019a0a00-0000-7000-8000-000000010008", + "values": { + "name": "Administración — documentación y atención interna", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetIds": [ + "019a0a00-0000-7000-8000-000000000020", + "019a0a00-0000-7000-8000-000000000021" + ], + "computerAssetId": "019a0a00-0000-7000-8000-000000000020" + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000011", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Laboratorio — equipo compartido sin responsable", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": null, + "assetIds": [ + "019a0a00-0000-7000-8000-000000000023" + ], + "computerAssetId": "019a0a00-0000-7000-8000-000000000023" + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010009", + "values": { + "name": "Laboratorio — equipo compartido sin responsable", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": null, + "assetIds": [ + "019a0a00-0000-7000-8000-000000000023" + ], + "computerAssetId": "019a0a00-0000-7000-8000-000000000023" + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000012", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Puesto pendiente de equipamiento", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": null, + "assetIds": [] + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010010", + "values": { + "name": "Puesto pendiente de equipamiento", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": null, + "assetIds": [] + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000013", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Recepción — puesto desmantelado", + "active": false, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": null, + "assetIds": [] + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010012", + "values": { + "name": "Recepción — puesto desmantelado", + "active": true, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": "019a0a00-0000-7000-8000-000000000004", + "computerAssetId": "019a0a00-0000-7000-8000-000000000020", + "assetIds": [ + "019a0a00-0000-7000-8000-000000000020" + ] + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-09-01T08:00:00.000Z" + }, + { + "id": "019a0a00-0000-7000-8000-000000010013", + "values": { + "name": "Recepción — puesto desmantelado", + "active": false, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "personId": null, + "assetIds": [] + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-09-30T09:00:00.000Z" + } + ] + } + ], + "assets": [ + { + "id": "019a0a00-0000-7000-8000-000000000020", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "PORT-ADM-01 · Documentación y gestión", + "templateId": "019a0a00-0000-7000-8000-000000000200", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001200": "Modelo sintético de pruebas" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010001", + "values": { + "name": "PORT-ADM-01 · Documentación y gestión", + "templateId": "019a0a00-0000-7000-8000-000000000200", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001200": "Modelo sintético de pruebas" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000200", + "templateName": "Portátil corporativo", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001200", + "name": "Modelo", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + }, + "assigned": true, + "displayStatus": "in_use" + }, + { + "id": "019a0a00-0000-7000-8000-000000000021", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "MON-ADM-01 · Monitor panorámico", + "templateId": "019a0a00-0000-7000-8000-000000000201", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001201": "Modelo sintético de pruebas" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010002", + "values": { + "name": "MON-ADM-01 · Monitor panorámico", + "templateId": "019a0a00-0000-7000-8000-000000000201", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001201": "Modelo sintético de pruebas" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000201", + "templateName": "Monitor", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001201", + "name": "Modelo", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + }, + "assigned": true, + "displayStatus": "in_use" + }, + { + "id": "019a0a00-0000-7000-8000-000000000022", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "IMP-RED-01 · Impresora de planta compartida", + "templateId": "019a0a00-0000-7000-8000-000000000202", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001202": "Modelo sintético de pruebas" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010003", + "values": { + "name": "IMP-RED-01 · Impresora de planta compartida", + "templateId": "019a0a00-0000-7000-8000-000000000202", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001202": "Modelo sintético de pruebas" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000202", + "templateName": "Impresora", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001202", + "name": "Modelo", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + }, + "assigned": false, + "displayStatus": "available" + }, + { + "id": "019a0a00-0000-7000-8000-000000000023", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "PC-LAB-01 · Equipo del laboratorio", + "templateId": "019a0a00-0000-7000-8000-000000000203", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001203": "Modelo sintético de pruebas" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010004", + "values": { + "name": "PC-LAB-01 · Equipo del laboratorio", + "templateId": "019a0a00-0000-7000-8000-000000000203", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001203": "Modelo sintético de pruebas" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000203", + "templateName": "Ordenador", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001203", + "name": "Modelo", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + }, + "assigned": true, + "displayStatus": "in_use" + }, + { + "id": "019a0a00-0000-7000-8000-000000000024", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "SRV-DOC-01 · Servidor publicador de documentación", + "templateId": "019a0a00-0000-7000-8000-000000000204", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001204": "Modelo sintético de pruebas" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010005", + "values": { + "name": "SRV-DOC-01 · Servidor publicador de documentación", + "templateId": "019a0a00-0000-7000-8000-000000000204", + "siteId": "019a0a00-0000-7000-8000-000000000003", + "status": "operational", + "notes": "Dato ficticio para diseño; no inventario real.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001204": "Modelo sintético de pruebas" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000204", + "templateName": "Servidor", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001204", + "name": "Modelo", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + }, + "assigned": false, + "displayStatus": "available" + } + ], + "relationships": [] + }, + "profiles": { + "profiles": [ + { + "id": "019a0a00-0000-7000-8000-000000000030", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Ángela · administración", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetId": "019a0a00-0000-7000-8000-000000000020", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "active": true, + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010014", + "values": { + "name": "Ángela · administración", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetId": "019a0a00-0000-7000-8000-000000000020", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "active": true, + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + }, + { + "id": "019a0a00-0000-7000-8000-000000000031", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Íñigo · consultas de documentación", + "personId": "019a0a00-0000-7000-8000-000000000005", + "assetId": "019a0a00-0000-7000-8000-000000000020", + "accountId": null, + "active": true, + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010015", + "values": { + "name": "Íñigo · consultas de documentación", + "personId": "019a0a00-0000-7000-8000-000000000005", + "assetId": "019a0a00-0000-7000-8000-000000000020", + "accountId": null, + "active": true, + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + }, + { + "id": "019a0a00-0000-7000-8000-000000000032", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Ángela · perfil archivado del mismo ordenador", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetId": "019a0a00-0000-7000-8000-000000000020", + "accountId": null, + "active": false, + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010016", + "values": { + "name": "Ángela · perfil archivado del mismo ordenador", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetId": "019a0a00-0000-7000-8000-000000000020", + "accountId": null, + "active": false, + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + }, + { + "id": "019a0a00-0000-7000-8000-000000000033", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Ángela · laboratorio", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetId": "019a0a00-0000-7000-8000-000000000023", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "active": true, + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010017", + "values": { + "name": "Ángela · laboratorio", + "personId": "019a0a00-0000-7000-8000-000000000004", + "assetId": "019a0a00-0000-7000-8000-000000000023", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "active": true, + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + } + ] + }, + "resources": { + "resources": [ + { + "id": "019a0a00-0000-7000-8000-000000000050", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Gestor documental de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000300", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001300": "Aplicación de documentación" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010018", + "values": { + "name": "Gestor documental de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000300", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001300": "Aplicación de documentación" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000300", + "templateName": "Aplicación", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001300", + "name": "Producto", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + } + }, + { + "id": "019a0a00-0000-7000-8000-000000000051", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Sistema operativo de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000310", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001310": "Edición de pruebas" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010019", + "values": { + "name": "Sistema operativo de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000310", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001310": "Edición de pruebas" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000310", + "templateName": "Sistema operativo", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001310", + "name": "Edición", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + } + }, + { + "id": "019a0a00-0000-7000-8000-000000000052", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Licencia documental de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000320", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001320": 0, + "019a0a00-0000-7000-8000-000000001321": false + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010020", + "values": { + "name": "Licencia documental de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000320", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001320": 0, + "019a0a00-0000-7000-8000-000000001321": false + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000320", + "templateName": "Licencia de software", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001320", + "name": "Plazas", + "type": "number", + "unit": "", + "choices": [], + "required": false + }, + { + "fieldId": "019a0a00-0000-7000-8000-000000001321", + "name": "Renovación automática", + "type": "boolean", + "unit": "", + "choices": [], + "required": false + } + ] + } + }, + { + "id": "019a0a00-0000-7000-8000-000000000053", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Documentación administrativa compartida", + "templateId": "019a0a00-0000-7000-8000-000000000330", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001330": "\\\\srv-doc.example.invalid\\documentacion\\administracion" + }, + "integration": { + "kind": "access", + "referenceFieldId": "019a0a00-0000-7000-8000-000000001330", + "resourceTypeId": "019a0a00-0000-7000-8000-000000000042", + "systemId": "019a0a00-0000-7000-8000-000000000040", + "environmentId": "019a0a00-0000-7000-8000-000000000041" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010021", + "values": { + "name": "Documentación administrativa compartida", + "templateId": "019a0a00-0000-7000-8000-000000000330", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001330": "\\\\srv-doc.example.invalid\\documentacion\\administracion" + }, + "integration": { + "kind": "access", + "referenceFieldId": "019a0a00-0000-7000-8000-000000001330", + "resourceTypeId": "019a0a00-0000-7000-8000-000000000042", + "systemId": "019a0a00-0000-7000-8000-000000000040", + "environmentId": "019a0a00-0000-7000-8000-000000000041" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000330", + "templateName": "Carpeta compartida SMB", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001330", + "name": "Ruta UNC", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + }, + "networkShare": { + "capability": "network:smb-share", + "protocol": "smb", + "address": "\\\\srv-doc.example.invalid\\documentacion\\administracion", + "normalizedAddress": "\\\\srv-doc.example.invalid\\documentacion\\administracion", + "host": "srv-doc.example.invalid", + "shareName": "documentacion", + "relativePath": "administracion" + } + }, + { + "id": "019a0a00-0000-7000-8000-000000000054", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Certificado de pruebas de documentación", + "templateId": "019a0a00-0000-7000-8000-000000000340", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001340": "2027-12-31" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010022", + "values": { + "name": "Certificado de pruebas de documentación", + "templateId": "019a0a00-0000-7000-8000-000000000340", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001340": "2027-12-31" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000340", + "templateName": "Certificado digital", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001340", + "name": "Caducidad", + "type": "date", + "unit": "", + "choices": [], + "required": false + } + ] + } + }, + { + "id": "019a0a00-0000-7000-8000-000000000055", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Buzón corporativo ficticio de Ángela", + "templateId": "019a0a00-0000-7000-8000-000000000350", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001350": "angela@example.invalid", + "019a0a00-0000-7000-8000-000000001351": "Correo de pruebas" + }, + "integration": { + "kind": "mailbox", + "addressFieldId": "019a0a00-0000-7000-8000-000000001350", + "contextFieldId": "019a0a00-0000-7000-8000-000000001351", + "mailboxKind": "corporate", + "accountId": null + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010023", + "values": { + "name": "Buzón corporativo ficticio de Ángela", + "templateId": "019a0a00-0000-7000-8000-000000000350", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001350": "angela@example.invalid", + "019a0a00-0000-7000-8000-000000001351": "Correo de pruebas" + }, + "integration": { + "kind": "mailbox", + "addressFieldId": "019a0a00-0000-7000-8000-000000001350", + "contextFieldId": "019a0a00-0000-7000-8000-000000001351", + "mailboxKind": "corporate", + "accountId": null + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000350", + "templateName": "Buzón", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001350", + "name": "Dirección", + "type": "text", + "unit": "", + "choices": [], + "required": false + }, + { + "fieldId": "019a0a00-0000-7000-8000-000000001351", + "name": "Contexto", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + } + }, + { + "id": "019a0a00-0000-7000-8000-000000000056", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "name": "Herramienta de soporte de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000360", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001360": "Herramienta ficticia de consulta" + } + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010024", + "values": { + "name": "Herramienta de soporte de ejemplo", + "templateId": "019a0a00-0000-7000-8000-000000000360", + "active": true, + "notes": "Recurso ficticio para diseño.", + "characteristics": { + "019a0a00-0000-7000-8000-000000001360": "Herramienta ficticia de consulta" + } + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ], + "specification": { + "templateId": "019a0a00-0000-7000-8000-000000000360", + "templateName": "Herramienta de soporte", + "fields": [ + { + "fieldId": "019a0a00-0000-7000-8000-000000001360", + "name": "Producto", + "type": "text", + "unit": "", + "choices": [], + "required": false + } + ] + } + } + ] + }, + "access": { + "items": [ + { + "id": "019a0a00-0000-7000-8000-000000000040", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "system", + "name": "Identidad corporativa de pruebas", + "active": true + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010025", + "values": { + "kind": "system", + "name": "Identidad corporativa de pruebas", + "active": true + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000041", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "environment", + "name": "Entorno corporativo de pruebas", + "active": true, + "systemId": "019a0a00-0000-7000-8000-000000000040" + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010026", + "values": { + "kind": "environment", + "name": "Entorno corporativo de pruebas", + "active": true, + "systemId": "019a0a00-0000-7000-8000-000000000040" + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000042", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "resource_type", + "name": "Carpeta compartida SMB", + "active": true + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010027", + "values": { + "kind": "resource_type", + "name": "Carpeta compartida SMB", + "active": true + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000043", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "account", + "systemId": "019a0a00-0000-7000-8000-000000000040", + "environmentId": "019a0a00-0000-7000-8000-000000000041", + "login": "angela.pruebas", + "usage": "personal", + "active": true, + "expiresOn": null, + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010028", + "values": { + "kind": "account", + "systemId": "019a0a00-0000-7000-8000-000000000040", + "environmentId": "019a0a00-0000-7000-8000-000000000041", + "login": "angela.pruebas", + "usage": "personal", + "active": true, + "expiresOn": null, + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000044", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "account_person", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "personId": "019a0a00-0000-7000-8000-000000000004", + "startDate": "2026-09-01", + "endDate": null, + "configuration": "configured" + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010029", + "values": { + "kind": "account_person", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "personId": "019a0a00-0000-7000-8000-000000000004", + "startDate": "2026-09-01", + "endDate": null, + "configuration": "configured" + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000045", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "permission", + "name": "Lectura documental", + "active": true, + "resourceTypeId": "019a0a00-0000-7000-8000-000000000042", + "description": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010030", + "values": { + "kind": "permission", + "name": "Lectura documental", + "active": true, + "resourceTypeId": "019a0a00-0000-7000-8000-000000000042", + "description": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000053", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "resource", + "name": "Documentación administrativa compartida", + "active": true, + "resourceTypeId": "019a0a00-0000-7000-8000-000000000042", + "systemId": "019a0a00-0000-7000-8000-000000000040", + "environmentId": "019a0a00-0000-7000-8000-000000000041", + "reference": "\\\\srv-doc.example.invalid\\documentacion\\administracion", + "notes": "Recurso ficticio para diseño." + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010031", + "values": { + "kind": "resource", + "name": "Documentación administrativa compartida", + "active": true, + "resourceTypeId": "019a0a00-0000-7000-8000-000000000042", + "systemId": "019a0a00-0000-7000-8000-000000000040", + "environmentId": "019a0a00-0000-7000-8000-000000000041", + "reference": "\\\\srv-doc.example.invalid\\documentacion\\administracion", + "notes": "Recurso ficticio para diseño." + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000046", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "need", + "personId": "019a0a00-0000-7000-8000-000000000004", + "resourceId": "019a0a00-0000-7000-8000-000000000053", + "permissionIds": [ + "019a0a00-0000-7000-8000-000000000045" + ], + "startDate": "2026-09-01", + "endDate": null, + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010032", + "values": { + "kind": "need", + "personId": "019a0a00-0000-7000-8000-000000000004", + "resourceId": "019a0a00-0000-7000-8000-000000000053", + "permissionIds": [ + "019a0a00-0000-7000-8000-000000000045" + ], + "startDate": "2026-09-01", + "endDate": null, + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000047", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "grant", + "resourceId": "019a0a00-0000-7000-8000-000000000053", + "permissionId": "019a0a00-0000-7000-8000-000000000045", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "groupId": null, + "startDate": "2026-09-01", + "endDate": null, + "configuration": "configured" + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010033", + "values": { + "kind": "grant", + "resourceId": "019a0a00-0000-7000-8000-000000000053", + "permissionId": "019a0a00-0000-7000-8000-000000000045", + "accountId": "019a0a00-0000-7000-8000-000000000043", + "groupId": null, + "startDate": "2026-09-01", + "endDate": null, + "configuration": "configured" + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + }, + { + "id": "019a0a00-0000-7000-8000-000000000048", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "kind": "coverage", + "needId": "019a0a00-0000-7000-8000-000000000046", + "grantId": "019a0a00-0000-7000-8000-000000000047", + "accountPersonId": "019a0a00-0000-7000-8000-000000000044", + "membershipId": null, + "startDate": "2026-09-01", + "endDate": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010034", + "values": { + "kind": "coverage", + "needId": "019a0a00-0000-7000-8000-000000000046", + "grantId": "019a0a00-0000-7000-8000-000000000047", + "accountPersonId": "019a0a00-0000-7000-8000-000000000044", + "membershipId": null, + "startDate": "2026-09-01", + "endDate": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-08-01T08:00:00.000Z" + } + ] + } + ], + "coverage": [ + { + "needId": "019a0a00-0000-7000-8000-000000000046", + "covered": true, + "missingPermissionIds": [] + } + ], + "reviews": [] + }, + "folders": { + "mappings": [ + { + "id": "019a0a00-0000-7000-8000-000000000060", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "needId": "019a0a00-0000-7000-8000-000000000046", + "accessAccountId": "019a0a00-0000-7000-8000-000000000043", + "path": "\\\\srv-doc.example.invalid\\documentacion\\administracion", + "driveLetter": "Z", + "state": "configured", + "notes": "Configuración registrada; no comprobación contra el servidor SMB." + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010035", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "needId": "019a0a00-0000-7000-8000-000000000046", + "accessAccountId": "019a0a00-0000-7000-8000-000000000043", + "path": "\\\\srv-doc.example.invalid\\documentacion\\administracion", + "driveLetter": "Z", + "state": "configured", + "notes": "Configuración registrada; no comprobación contra el servidor SMB." + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false, + "permissionsCovered": true, + "accessAccountCoverage": "covered" + } + ] + }, + "software": { + "configurations": [ + { + "id": "019a0a00-0000-7000-8000-000000000061", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "assetId": "019a0a00-0000-7000-8000-000000000020", + "resourceId": "019a0a00-0000-7000-8000-000000000050", + "licenseResourceId": "019a0a00-0000-7000-8000-000000000052", + "version": "4.2", + "state": "configured", + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010036", + "values": { + "assetId": "019a0a00-0000-7000-8000-000000000020", + "resourceId": "019a0a00-0000-7000-8000-000000000050", + "licenseResourceId": "019a0a00-0000-7000-8000-000000000052", + "version": "4.2", + "state": "configured", + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + }, + { + "id": "019a0a00-0000-7000-8000-000000000062", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "assetId": "019a0a00-0000-7000-8000-000000000020", + "resourceId": "019a0a00-0000-7000-8000-000000000051", + "licenseResourceId": null, + "version": null, + "state": "configured", + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010037", + "values": { + "assetId": "019a0a00-0000-7000-8000-000000000020", + "resourceId": "019a0a00-0000-7000-8000-000000000051", + "licenseResourceId": null, + "version": null, + "state": "configured", + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + } + ] + }, + "applications": { + "configurations": [ + { + "id": "019a0a00-0000-7000-8000-000000000063", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "resourceId": "019a0a00-0000-7000-8000-000000000050", + "delivery": "installed", + "version": "4.2", + "state": "configured", + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010038", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "resourceId": "019a0a00-0000-7000-8000-000000000050", + "delivery": "installed", + "version": "4.2", + "state": "configured", + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + } + ] + }, + "printers": { + "configurations": [ + { + "id": "019a0a00-0000-7000-8000-000000000064", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "printerId": "019a0a00-0000-7000-8000-000000000022", + "connection": "network", + "isDefault": true, + "state": "configured", + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010039", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "printerId": "019a0a00-0000-7000-8000-000000000022", + "connection": "network", + "isDefault": true, + "state": "configured", + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false + } + ] + }, + "certificates": { + "configurations": [ + { + "id": "019a0a00-0000-7000-8000-000000000065", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "resourceId": "019a0a00-0000-7000-8000-000000000054", + "location": null, + "state": "pending", + "notes": "Instalación pendiente de completar." + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010040", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "resourceId": "019a0a00-0000-7000-8000-000000000054", + "location": null, + "state": "pending", + "notes": "Instalación pendiente de completar." + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false, + "expiresOn": "2027-12-31", + "expired": false + } + ] + }, + "mail": { + "configurations": [ + { + "id": "019a0a00-0000-7000-8000-000000000066", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "mailboxId": "019a0a00-0000-7000-8000-000000000055", + "method": "imap", + "state": "configured", + "notes": null, + "address": "angela@example.invalid" + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010041", + "values": { + "profileId": "019a0a00-0000-7000-8000-000000000030", + "mailboxId": "019a0a00-0000-7000-8000-000000000055", + "method": "imap", + "state": "configured", + "notes": null, + "address": "angela@example.invalid" + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false, + "legacy": false + } + ] + }, + "mailboxes": { + "mailboxes": [ + { + "id": "019a0a00-0000-7000-8000-000000000055", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "address": "angela@example.invalid", + "kind": "corporate", + "context": "Correo de pruebas", + "accountId": null, + "active": true, + "notes": "Recurso ficticio para diseño." + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010042", + "values": { + "address": "angela@example.invalid", + "kind": "corporate", + "context": "Correo de pruebas", + "accountId": null, + "active": true, + "notes": "Recurso ficticio para diseño." + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ] + } + ], + "uses": [ + { + "id": "019a0a00-0000-7000-8000-000000000067", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "mailboxId": "019a0a00-0000-7000-8000-000000000055", + "personId": "019a0a00-0000-7000-8000-000000000004", + "startDate": "2026-09-01", + "endDate": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010043", + "values": { + "mailboxId": "019a0a00-0000-7000-8000-000000000055", + "personId": "019a0a00-0000-7000-8000-000000000004", + "startDate": "2026-09-01", + "endDate": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ] + } + ] + }, + "telework": { + "authorizations": [ + { + "id": "019a0a00-0000-7000-8000-000000000070", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "personId": "019a0a00-0000-7000-8000-000000000004", + "active": true, + "startsOn": "2026-09-01", + "expiresOn": "2027-08-31", + "notes": null + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010044", + "values": { + "personId": "019a0a00-0000-7000-8000-000000000004", + "active": true, + "startsOn": "2026-09-01", + "expiresOn": "2027-08-31", + "notes": null + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "status": "authorized", + "effective": true, + "needsReview": false + } + ] + }, + "remote": { + "connections": [ + { + "id": "019a0a00-0000-7000-8000-000000000071", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "personId": "019a0a00-0000-7000-8000-000000000004", + "sourceKind": "company_asset", + "sourceAssetId": "019a0a00-0000-7000-8000-000000000020", + "targetKind": "network", + "targetId": null, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "method": "VPN corporativa documentada", + "requiresVpn": true, + "state": "configured", + "notes": "Configuración ficticia, sin comprobación de sesión activa." + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010045", + "values": { + "personId": "019a0a00-0000-7000-8000-000000000004", + "sourceKind": "company_asset", + "sourceAssetId": "019a0a00-0000-7000-8000-000000000020", + "targetKind": "network", + "targetId": null, + "siteId": "019a0a00-0000-7000-8000-000000000003", + "method": "VPN corporativa documentada", + "requiresVpn": true, + "state": "configured", + "notes": "Configuración ficticia, sin comprobación de sesión activa." + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "needsReview": false, + "authorizationEffective": true + } + ] + }, + "support": { + "connections": [ + { + "id": "019a0a00-0000-7000-8000-000000000072", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "values": { + "resourceId": "019a0a00-0000-7000-8000-000000000056", + "destination": "equipo-adm-01.example.invalid", + "requiresVpn": false, + "preferred": true, + "state": "available", + "notes": "Destino ficticio; no iniciar conexiones." + }, + "history": [ + { + "id": "019a0a00-0000-7000-8000-000000010046", + "values": { + "resourceId": "019a0a00-0000-7000-8000-000000000056", + "destination": "equipo-adm-01.example.invalid", + "requiresVpn": false, + "preferred": true, + "state": "available", + "notes": "Destino ficticio; no iniciar conexiones." + }, + "actorId": "019a0a00-0000-7000-8000-000000000002", + "actorName": "Operadora de soporte ficticia", + "recordedAt": "2026-10-20T09:00:00.000Z" + } + ], + "assetId": "019a0a00-0000-7000-8000-000000000020" + } + ], + "tools": [ + { + "id": "019a0a00-0000-7000-8000-000000000056", + "name": "Herramienta de soporte de ejemplo", + "active": true + } + ] + }, + "publications": { + "publications": [ + { + "id": "019a0a00-0000-7000-8000-000000000073", + "companyId": "019a0a00-0000-7000-8000-000000000001", + "assetId": "019a0a00-0000-7000-8000-000000000024", + "resourceId": "019a0a00-0000-7000-8000-000000000053", + "predecessorId": null, + "startedAt": "2026-09-01T08:00:00.000Z", + "startedBy": "019a0a00-0000-7000-8000-000000000002", + "endedAt": null, + "endedBy": null + } + ], + "nextCursor": null + } + }, + "presentation": { + "evidence": "DERIVABLE; forma de presentación PROPUESTA, no DTO de servidor.", + "relatedProfileIds": [ + "019a0a00-0000-7000-8000-000000000030", + "019a0a00-0000-7000-8000-000000000031", + "019a0a00-0000-7000-8000-000000000032" + ], + "activeProfileIds": [ + "019a0a00-0000-7000-8000-000000000030", + "019a0a00-0000-7000-8000-000000000031" + ], + "relatedPersonIds": [ + "019a0a00-0000-7000-8000-000000000004", + "019a0a00-0000-7000-8000-000000000005" + ], + "counts": { + "assets": 2, + "activeProfiles": 2, + "activeUsers": 2, + "relatedPeople": 2, + "profilesNeedingReview": 0, + "pendingConfigurations": 1, + "removalPendingConfigurations": 0, + "configuredSoftware": 2 + }, + "latestRevisionId": "019a0a00-0000-7000-8000-000000010008" + }, + "scenarios": [ + { + "id": "S01", + "name": "Puesto completo", + "baseWorkstationId": "019a0a00-0000-7000-8000-000000000010" + }, + { + "id": "S02", + "name": "Sin responsable, con usuario de perfil", + "baseWorkstationId": "019a0a00-0000-7000-8000-000000000011" + }, + { + "id": "S03", + "name": "Sin miembros", + "baseWorkstationId": "019a0a00-0000-7000-8000-000000000012" + }, + { + "id": "S04", + "name": "Alta densidad", + "baseWorkstationId": "019a0a00-0000-7000-8000-000000000010", + "description": "Variante pendiente de materializar según 06-escenarios-mockup.md." + }, + { + "id": "S05", + "name": "Error parcial simulado de presentación", + "baseWorkstationId": "019a0a00-0000-7000-8000-000000000010", + "displayUnavailable": [ + "profiles", + "folders" + ] + }, + { + "id": "S06", + "name": "Desmantelado", + "baseWorkstationId": "019a0a00-0000-7000-8000-000000000013" + } + ] +} diff --git a/docs/auditorias/workplace-dashboard/08-resumen-ejecutivo.md b/docs/auditorias/workplace-dashboard/08-resumen-ejecutivo.md new file mode 100644 index 0000000..e5de836 --- /dev/null +++ b/docs/auditorias/workplace-dashboard/08-resumen-ejecutivo.md @@ -0,0 +1,49 @@ +# Resumen ejecutivo: dashboard de puesto de trabajo + +22 de octubre de 2026. Investigación y documentación; no se ha implementado el dashboard ni generado su mockup definitivo. + +## Qué representa un puesto + +**CONFIRMADO**: `Workstation` es un agregado de Assets que organiza un conjunto actual de hasta 200 activos exclusivos, con ordenador principal, responsable y sede opcionales, estado y revisiones. La ficha se compone desde Companies. No representa una persona, un perfil de usuario ni `AccessWorkspace`. + +## Relaciones e información disponible + +Tiene vínculos directos con compañía, sede, responsable, activos y revisiones. Los perfiles se relacionan indirectamente por el ordenador principal y compañía, y aportan otras personas, cuenta y configuraciones de carpetas, correo, aplicaciones, impresoras y certificados. El equipo aporta software instalado, soporte, directorio, entornos locales y posibles publicaciones SMB. Personas aportan departamentos temporales, teletrabajo, conexiones remotas y procesos. + +**No todos los permisos de una persona son recursos del puesto.** La cuenta de una carpeta puede diferir de la cuenta de inicio de sesión del perfil. Una publicación SMB identifica al servidor publicador, no al consumidor ni una autorización de acceso. La cobertura es documental; no prueba acceso real. + +Las fuentes existen en inventario, compañía, perfiles, Access y APIs específicas de configuraciones, remoto/teletrabajo y relaciones tipadas. Los [atributos y fuentes](01-inventario-funcional.md) y el [mapa relacional](02-mapa-relaciones.md) proporcionan trazabilidad a símbolos y archivos. + +## Qué muestra la ficha y qué queda disperso + +Actualmente ofrece cabecera con estado/sede/responsable, equipamiento, personas y perfiles; pestañas de software e historial; edición, renombrado, alta de perfil y ciclo de vida en componentes propios. No reúne las configuraciones por perfil, cuentas, cobertura de carpetas ni contexto de soporte/remoto/departamento. + +Los perfiles inactivos también aportan personas al panel actual. Los activos distintos del principal se llaman periféricos aunque el modelo no impone esa clase. Cambiar ordenador principal cambia perfiles/software visibles, sin trasladarlos. Estas tres distinciones deben conservarse o explicarse en el rediseño. Véase la [auditoría de ficha](03-auditoria-ui-actual.md). + +## Dashboard recomendado + +**PROPUESTO**: cabecera de identidad y acciones; resumen del ordenador y señales registradas de revisión; personas/perfiles; equipamiento compacto; software del principal; configuraciones con perfil de origen; soporte y contexto secundario; último cambio e historial. + +La propuesta busca responder qué se atiende, quién lo utiliza, qué tiene configurado y dónde consultar o actuar. Los contadores tienen fórmulas explícitas; no se añaden gráficos sin tarea ni datos. Tickets e intervenciones están archivados y se excluyen de la propuesta actual. No se simula salud ni conectividad online. + +Componentes reutilizables: `DetailPanel`, `DetailField`, `EntityTable`, `EntityStatus`, `EmptyState`, `TimeLine`, `AssetSummaryCard`, acciones/editores de puesto, panel de software y primitivas Base UI/Vega. La lógica `buildEnvironment` es referencia reutilizable para configuraciones. Su hook agregador exige todas las fuentes, por lo que necesita revisar el tratamiento parcial antes de usarlo en un dashboard. Detalle en [arquitectura de información](04-arquitectura-informacion.md). + +## Viabilidad y límites + +El primer dashboard es viable con APIs existentes y nueva composición de lectura; no se ha demostrado necesidad de un endpoint nuevo. Los riesgos son payloads de compañía con historial, consultas por activo, paginación de relaciones y fallos secundarios que bloqueen toda la vista. No hay mediciones de rendimiento. + +Las rutas requieren autenticación y validan pertenencia de entidades a compañía. No se confirmó ACL granular operador–compañía; no debe darse por existente ni deducirse del filtrado frontend. El sistema usa composición explícita de módulos y contratos de relaciones, sin registro dinámico de plugins confirmado para esta pantalla. + +Una guía de rutas aún menciona activos compartidos en el puesto; la implementación actual los retira y acepta miembros exclusivos. Se registra la discrepancia sin editar documentación fuera de estos entregables. Más detalles y validaciones: [viabilidad técnica](05-viabilidad-tecnica.md). + +## Decisiones y preparación del mockup + +Pendiente confirmar: usuarios activos por defecto frente a archivados, nivel de resumen de configuraciones, alcance de soporte/software sobre otros miembros y utilidad de procesos. La propuesta inicial distingue responsable y uso, se limita al ordenador principal para software/soporte y mantiene configuraciones con su perfil. + +Se entregan [escenarios y prompt posterior](06-escenarios-mockup.md) y [fixture ficticio](07-datos-ejemplo.json), con fuentes de forma DTO separadas de presentación derivada. Hay puestos completo, sin responsable, sin miembros y desmantelado; se especifican además alta densidad, errores parciales y cambio de host. El JSON contiene 1 compañía, 2 personas, 5 activos, 7 recursos, 4 puestos y 4 perfiles. No son datos de una BD ni pruebas de volumen. + +Skills consultadas: `ux-dashboard`, `ux-tables`, `ux-design-review` (parcial), `shadcn`, `vercel-composition-patterns` y `vercel-react-best-practices`. Guiaron jerarquía, semántica de tablas, composición, APIs y gestión de consultas. No hay auditores `ux-*-audit` instalados ni política de orquestación localizada; no se instaló nada. + +Playwright MCP está disponible, pero solo se comprobó una pestaña vacía; no hubo origen Vite utilizable confirmado. No se inició API ni se ejecutaron migraciones. La revisión visual, teclado/foco, responsive, accesibilidad y pruebas con al menos 15 compañías/50 personas/50 activos/50 recursos quedan pendientes. Se ha creado únicamente documentación y un fixture ficticio; los cambios locales previos permanecen fuera del alcance. + +Validación realizada: 16 bloques de DTO correctos, relaciones y contadores coherentes, proyección Access coincidente; los tres archivos de pruebas dirigidas pasaron. Se comprobaron enlaces locales y referencias a líneas. No se ejecutaron suites globales de aplicación por tratarse de documentación y datos ficticios. diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index d9569f6..2bf04b6 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -1282,3 +1282,17 @@ de que el anuncio del asistente reciba foco, y lo proporciona mediante búsqueda del inventario o su contenedor como último destino local. El contenedor no se añade a la secuencia Tab. El selector anidado de puestos conserva su foco inicial y su retorno al formulario padre. + +## Paneles compactos del dashboard de puesto + +`DetailPanel` admite `density="compact"` para resúmenes operativos: conserva la región +etiquetada y su contrato de título, icono, descripción, acciones y contenido, con padding +interior de 16 px y separación reducida. El valor por defecto mantiene sus recetas. +Los consumidores actuales son los bloques del dashboard de puesto; no añade consultas, +DTO ni reglas de entidades al componente genérico. + +La composición de [la ficha de puesto](workplace-dashboard-implementacion.md) reutiliza +`EntityTable`, estados por consulta y diálogos existentes. `WorkstationActions.children` +permite intercalar la acción de historial entre editar y el menú; el diálogo de historial +se exporta desde Assets y admite `finalFocus`, con altura estable. Son APIs específicas +del módulo consumidor, no nuevos componentes genéricos de RDX UI. diff --git a/docs/diagrama-entidad-relacion-completo.mmd b/docs/diagrama-entidad-relacion-completo.mmd deleted file mode 100644 index cea7436..0000000 --- a/docs/diagrama-entidad-relacion-completo.mmd +++ /dev/null @@ -1,1397 +0,0 @@ -erDiagram - direction LR - access_account_person_revision_values { - char ownerId PK,FK - char accountId FK - char personId FK - varchar startDate - varchar endDate "nullable" - varchar configuration "nullable" - } - access_account_person_values { - char ownerId PK,FK - char accountId FK - char personId FK - varchar startDate - varchar endDate "nullable" - varchar configuration "nullable" - } - access_account_revision_values { - char ownerId PK,FK - char systemId FK - char environmentId FK "nullable" - varchar login - varchar usage - tinyint active - varchar expiresOn "nullable" - text notes "nullable" - } - access_account_values { - char ownerId PK,FK - char systemId FK - char environmentId FK "nullable" - varchar login - varchar usage - tinyint active - varchar expiresOn "nullable" - text notes "nullable" - } - access_coverage_revision_values { - char ownerId PK,FK - char needId FK - char grantId FK - char accountPersonId FK - char membershipId FK "nullable" - varchar startDate - varchar endDate "nullable" - } - access_coverage_values { - char ownerId PK,FK - char needId FK - char grantId FK - char accountPersonId FK - char membershipId FK "nullable" - varchar startDate - varchar endDate "nullable" - } - access_current_values { - char ownerId - varchar kind - varchar name "nullable" - tinyint active "nullable" - char resourceTypeId "nullable" - mediumtext description "nullable" - char systemId "nullable" - char environmentId "nullable" - varchar login "nullable" - varchar usage "nullable" - varchar expiresOn "nullable" - mediumtext notes "nullable" - char accountId "nullable" - char personId "nullable" - varchar startDate "nullable" - varchar endDate "nullable" - varchar configuration "nullable" - varchar identifier "nullable" - char templateId "nullable" - char groupId "nullable" - varchar reference "nullable" - char resourceId "nullable" - char permissionId "nullable" - char needId "nullable" - char grantId "nullable" - char accountPersonId "nullable" - char membershipId "nullable" - } - access_environment_revision_values { - char ownerId PK,FK - varchar name - tinyint active - char systemId FK - } - access_environment_values { - char ownerId PK,FK - varchar name - tinyint active - char systemId FK - } - access_grant_revision_values { - char ownerId PK,FK - char resourceId FK - char permissionId FK - char accountId FK "nullable" - char groupId FK "nullable" - varchar startDate - varchar endDate "nullable" - varchar configuration - } - access_grant_values { - char ownerId PK,FK - char resourceId FK - char permissionId FK - char accountId FK "nullable" - char groupId FK "nullable" - varchar startDate - varchar endDate "nullable" - varchar configuration - } - access_group_revision_values { - char ownerId PK,FK - char systemId FK - char environmentId FK "nullable" - varchar name - tinyint active - varchar identifier - text description "nullable" - char templateId FK "nullable" - } - access_group_values { - char ownerId PK,FK - char systemId FK - char environmentId FK "nullable" - varchar name - tinyint active - varchar identifier - text description "nullable" - char templateId FK "nullable" - } - access_historical_values { - char ownerId - varchar kind - varchar name "nullable" - tinyint active "nullable" - char resourceTypeId "nullable" - mediumtext description "nullable" - char systemId "nullable" - char environmentId "nullable" - varchar login "nullable" - varchar usage "nullable" - varchar expiresOn "nullable" - mediumtext notes "nullable" - char accountId "nullable" - char personId "nullable" - varchar startDate "nullable" - varchar endDate "nullable" - varchar configuration "nullable" - varchar identifier "nullable" - char templateId "nullable" - char groupId "nullable" - varchar reference "nullable" - char resourceId "nullable" - char permissionId "nullable" - char needId "nullable" - char grantId "nullable" - char accountPersonId "nullable" - char membershipId "nullable" - } - access_items { - char id PK - char workspaceId FK - varchar kind - } - access_membership_revision_values { - char ownerId PK,FK - char accountId FK - char groupId FK - varchar startDate - varchar endDate "nullable" - varchar configuration - } - access_membership_values { - char ownerId PK,FK - char accountId FK - char groupId FK - varchar startDate - varchar endDate "nullable" - varchar configuration - } - access_need_permissions { - char ownerId PK,FK - int position PK - char permissionId FK - } - access_need_revision_permissions { - char ownerId PK,FK - int position PK - char permissionId FK - } - access_need_revision_values { - char ownerId PK,FK - char personId FK - char resourceId FK - varchar startDate - varchar endDate "nullable" - text notes "nullable" - } - access_need_values { - char ownerId PK,FK - char personId FK - char resourceId FK - varchar startDate - varchar endDate "nullable" - text notes "nullable" - } - access_permission_revision_values { - char ownerId PK,FK - varchar name - tinyint active - char resourceTypeId FK - text description "nullable" - } - access_permission_values { - char ownerId PK,FK - varchar name - tinyint active - char resourceTypeId FK - text description "nullable" - } - access_person_references { - char itemId PK,FK - char personId PK,FK - } - access_references { - int id PK - char itemId FK - char targetId FK - } - access_resource_revision_values { - char ownerId PK,FK - varchar name - tinyint active - char resourceTypeId FK - char systemId FK "nullable" - char environmentId FK "nullable" - varchar reference "nullable" - text notes "nullable" - } - access_resource_type_revision_values { - char ownerId PK,FK - varchar name - tinyint active - } - access_resource_type_values { - char ownerId PK,FK - varchar name - tinyint active - } - access_resource_values { - char ownerId PK,FK - varchar name - tinyint active - char resourceTypeId FK - char systemId FK "nullable" - char environmentId FK "nullable" - varchar reference "nullable" - text notes "nullable" - } - access_revisions { - char id PK - char itemId FK - int sequence - char actorId FK - varchar recordedAt - } - access_system_revision_values { - char ownerId PK,FK - varchar name - tinyint active - } - access_system_values { - char ownerId PK,FK - varchar name - tinyint active - } - access_template_revision_values { - char ownerId PK,FK - varchar name - tinyint active - text description "nullable" - char systemId FK "nullable" - } - access_template_values { - char ownerId PK,FK - varchar name - tinyint active - text description "nullable" - char systemId FK "nullable" - } - access_workspaces { - char id PK - char companyId FK "nullable" - varchar scopeKey - } - application_configuration_revisions { - char id PK - char configurationId FK - int position - char actorId FK - varchar recordedAt - char profileId FK "nullable" - char resourceId FK "nullable" - varchar delivery - varchar version "nullable" - varchar state - text notes "nullable" - } - application_configurations { - char id PK - char companyId FK - char profileId FK - char resourceId FK - varchar currentKey "nullable" - varchar delivery - varchar version "nullable" - varchar state - text notes "nullable" - } - asset_availability { - char id PK - char catalogId FK - char companyId FK - char definitionId FK - tinyint active "nullable" - } - asset_brands { - char id PK - varchar name - tinyint active - tinyint builtin - text logoSvg "nullable" - } - asset_catalogs { - char id PK - varchar catalogKey - } - asset_characteristics { - char assetId PK,FK - char fieldId PK,FK - varchar valueType - text textValue "nullable" - double numberValue "nullable" - tinyint booleanValue "nullable" - date dateValue "nullable" - } - asset_definitions { - char id PK - char catalogId FK - char companyId FK "nullable" - varchar stockKey "nullable" - tinyint active - varchar kind - varchar name - text description - varchar fieldType "nullable" - varchar unit "nullable" - varchar inverseName "nullable" - tinyint symmetric "nullable" - tinyint sectionsDefined - varchar icon "nullable" - } - asset_field_choices { - char fieldId PK,FK - int position PK - varchar value - } - asset_relationships { - char id PK - char companyId FK - char sourceId FK - char targetId FK - char typeId FK - char createdBy FK - varchar createdAt - char endedBy FK "nullable" - varchar endedAt "nullable" - varchar activeKey "nullable" - varchar name - varchar inverseName - tinyint symmetric - text notes - } - asset_revision_characteristics { - char revisionId PK,FK - char assetId FK - char fieldId PK,FK - varchar valueType - text textValue "nullable" - double numberValue "nullable" - tinyint booleanValue "nullable" - date dateValue "nullable" - } - asset_revisions { - char id PK - char assetId FK - char actorId FK - varchar recordedAt - int position - char siteId FK "nullable" - char brandId FK "nullable" - varchar name - varchar status - text notes - char templateId FK - tinyint brandDefined - } - asset_specification_field_choices { - char assetId PK,FK - char fieldId PK,FK - int position PK - varchar value - } - asset_specification_fields { - char assetId PK,FK - char fieldId PK,FK - varchar name - varchar fieldType - varchar unit - tinyint required - char sectionId FK "nullable" - int position - } - asset_specification_sections { - char assetId PK,FK - char id PK - varchar name - text description - int position - } - asset_support { - char id PK - char companyId FK - char assetId FK - char preferredKey "nullable" - varchar destination - tinyint requiresVpn - tinyint preferred - varchar state - text notes - char resourceId FK - } - asset_support_revisions { - char id PK - char connectionId FK - char actorId FK - varchar recordedAt - int position - varchar destination - tinyint requiresVpn - tinyint preferred - varchar state - text notes - char resourceId FK - } - asset_template_fields { - char id PK - char templateId FK - char fieldId FK - tinyint required - int position - char sectionId FK "nullable" - } - asset_template_sections { - char id PK - char templateId FK - varchar name - text description - int position - } - assets { - char id PK - char companyId FK - char templateId FK - char siteId FK "nullable" - char brandId FK "nullable" - varchar name - varchar status - text notes - varchar templateName - tinyint sectionsDefined - tinyint brandDefined - } - brand_catalog_lock { - int id PK - tinyint legacyImported - tinyint logosImported - } - certificate_configuration_revisions { - char id PK - char configurationId FK - int position - char actorId FK - varchar recordedAt - char profileId FK "nullable" - char resourceId FK "nullable" - varchar location "nullable" - varchar state - text notes "nullable" - } - certificate_configurations { - char id PK - char companyId FK - char profileId FK - char resourceId FK - varchar currentKey "nullable" - varchar location "nullable" - varchar state - text notes "nullable" - } - companies { - char id PK - varchar name - tinyint active - varchar logoVersion "nullable" - } - company_contacts { - char id PK - char companyId FK - varchar name - varchar position "nullable" - varchar email "nullable" - varchar phone "nullable" - tinyint active - tinyint isContact - varchar photoVersion "nullable" - } - company_departments { - char id PK - char companyId FK - varchar name - varchar nameKey - tinyint active - } - company_logos { - char companyId PK,FK - mediumblob data - varchar version - } - company_sites { - char id PK - char companyId FK - varchar name - text address "nullable" - varchar phone "nullable" - } - department_assignment_revisions { - char id PK - char assignmentId FK - int sequence - varchar position "nullable" - date startDate - date endDate "nullable" - tinyint isPrimary - varchar recordedAt - } - folder_mapping_revisions { - char id PK - char mappingId FK - int position - char actorId FK - varchar recordedAt - char profileId FK "nullable" - char needId FK "nullable" - varchar path - varchar driveLetter "nullable" - varchar state - text notes "nullable" - } - folder_mappings { - char id PK - char companyId FK - char profileId FK - char needId FK - varchar currentKey "nullable" - varchar driveKey "nullable" - varchar path - varchar driveLetter "nullable" - varchar state - text notes "nullable" - } - http_sessions { - varchar sid PK - datetime expires "nullable" - datetime createdAt - datetime updatedAt - char userId FK "nullable" - int authVersion "nullable" - varchar csrfToken "nullable" - varchar cookieExpires "nullable" - bigint originalMaxAge "nullable" - varchar cookiePath "nullable" - varchar cookieDomain "nullable" - varchar cookieSecure "nullable" - tinyint cookieHttpOnly "nullable" - varchar cookieSameSite "nullable" - tinyint cookiePartitioned "nullable" - varchar cookiePriority "nullable" - } - identity_state { - int id PK - char initialAdminId FK "nullable" - } - mail_configuration_revisions { - char id PK - char configurationId FK - int position - char actorId FK - varchar recordedAt - varchar snapshotKind - char personId FK "nullable" - char accountId FK "nullable" - char assetId FK "nullable" - char profileId FK "nullable" - char mailboxId FK "nullable" - varchar address - varchar method - varchar state - text notes "nullable" - } - mail_configurations { - char id PK - char companyId FK - char personId FK "nullable" - char accountId FK "nullable" - char assetId FK "nullable" - varchar currentKey "nullable" - char profileId FK "nullable" - char mailboxId FK "nullable" - varchar snapshotKind - varchar address - varchar method - varchar state - text notes "nullable" - } - mailbox_revisions { - char id PK - char itemId FK - int position - char actorId FK - varchar recordedAt - varchar address - varchar kind - varchar context "nullable" - char accountId FK "nullable" - tinyint active - text notes "nullable" - } - mailbox_use_revisions { - char id PK - char itemId FK - int position - char actorId FK - varchar recordedAt - char mailboxId FK "nullable" - char personId FK "nullable" - varchar startDate - varchar endDate "nullable" - } - mailbox_uses { - char id PK - char companyId FK - char mailboxId FK - char personId FK - varchar startDate - varchar endDate "nullable" - } - mailboxes { - char id PK - char companyId FK - char accountId FK "nullable" - varchar addressKey - varchar address - varchar kind - varchar context "nullable" - tinyint active - text notes "nullable" - } - person_department_assignments { - char id PK - char companyId FK - char personId FK - char departmentId FK - varchar position "nullable" - date startDate - date endDate "nullable" - tinyint isPrimary - } - person_photos { - char personId PK,FK - char companyId FK - mediumblob data - varchar version - datetime createdAt - datetime updatedAt - } - person_process_revision_tasks { - char ownerId PK,FK - int position - varchar title - char taskId PK - varchar status - text notes "nullable" - varchar referenceKind "nullable" - char referenceId "nullable" - varchar referenceLabel "nullable" - varchar sourceKind "nullable" - } - person_process_revisions { - char id PK - int position - char actorId FK "nullable" - varchar recordedAt - char itemId FK - char personId FK "nullable" - char templateId FK "nullable" - varchar templateName - varchar kind - varchar name - varchar state - text notes "nullable" - } - person_process_tasks { - char ownerId PK,FK - int position - varchar title - char taskId PK - varchar status - text notes "nullable" - varchar referenceKind "nullable" - char referenceId "nullable" - varchar referenceLabel "nullable" - varchar sourceKind "nullable" - } - person_processes { - char id PK - char companyId FK - char personId FK - char templateId FK - varchar currentKey "nullable" - varchar templateName - varchar kind - varchar name - varchar state - text notes "nullable" - } - printer_configuration_revisions { - char id PK - char configurationId FK - int position - char actorId FK - varchar recordedAt - char profileId FK "nullable" - char printerId FK "nullable" - varchar connection - tinyint isDefault - varchar state - text notes "nullable" - } - printer_configurations { - char id PK - char companyId FK - char profileId FK - char printerId FK - varchar currentKey "nullable" - varchar defaultKey "nullable" - varchar connection - tinyint isDefault - varchar state - text notes "nullable" - } - process_catalog_lock { - int id PK - } - process_template_instructions { - char ownerId PK,FK - int position PK - varchar title - } - process_template_revision_instructions { - char ownerId PK,FK - int position PK - varchar title - } - process_template_revisions { - char id PK - int position - char actorId FK "nullable" - varchar recordedAt - char itemId FK - varchar name - varchar kind - tinyint active - text notes "nullable" - } - process_templates { - char id PK - varchar nameKey - varchar builtinKey "nullable" - varchar name - varchar kind - tinyint active - text notes "nullable" - } - remote_connection_revisions { - char id PK - char connectionId FK - int position - char actorId FK - varchar recordedAt - char personId FK "nullable" - varchar sourceKind - char sourceAssetId FK "nullable" - varchar targetKind - char targetAssetId FK "nullable" - char targetResourceId FK "nullable" - char siteId FK "nullable" - varchar method - tinyint requiresVpn - varchar state - text notes "nullable" - } - remote_connections { - char id PK - char companyId FK - char personId FK - char sourceAssetId FK "nullable" - char targetAssetId FK "nullable" - char targetResourceId FK "nullable" - char siteId FK "nullable" - varchar currentKey "nullable" - varchar sourceKind - varchar targetKind - varchar method - tinyint requiresVpn - varchar state - text notes "nullable" - } - resource_availability { - char id PK - char catalogId FK - char companyId FK - char definitionId FK - tinyint active "nullable" - } - resource_catalogs { - char id PK - varchar catalogKey - } - resource_characteristics { - char resourceId PK,FK - char fieldId PK,FK - varchar valueType - text textValue "nullable" - double numberValue "nullable" - tinyint booleanValue "nullable" - date dateValue "nullable" - } - resource_definitions { - char id PK - char catalogId FK - char companyId FK "nullable" - varchar stockKey "nullable" - tinyint active - varchar kind - varchar name - text description - varchar fieldType "nullable" - varchar unit "nullable" - varchar usage "nullable" - tinyint sectionsDefined - } - resource_field_choices { - char fieldId PK,FK - int position PK - varchar value - } - resource_revision_characteristics { - char revisionId PK,FK - char resourceId FK - char fieldId PK,FK - varchar valueType - text textValue "nullable" - double numberValue "nullable" - tinyint booleanValue "nullable" - date dateValue "nullable" - } - resource_revisions { - char id PK - char resourceId FK - char actorId FK - varchar recordedAt - int position - varchar name - tinyint active - text notes - varchar integrationKind "nullable" - char addressFieldId FK "nullable" - char contextFieldId FK "nullable" - varchar mailboxKind "nullable" - char accountId FK "nullable" - char referenceFieldId FK "nullable" - char resourceTypeId FK "nullable" - char systemId FK "nullable" - char environmentId FK "nullable" - char templateId FK "nullable" - } - resource_specification_field_choices { - char resourceId PK,FK - char fieldId PK,FK - int position PK - varchar value - } - resource_specification_fields { - char resourceId PK,FK - char fieldId PK,FK - varchar name - varchar fieldType - varchar unit - tinyint required - char sectionId "nullable" - int position - } - resource_specification_sections { - char resourceId PK,FK - char id PK - varchar name - text description - int position - } - resource_template_fields { - char id PK - char templateId FK - char fieldId FK - tinyint required - int position - char sectionId FK "nullable" - } - resource_template_sections { - char id PK - char templateId PK,FK - varchar name - text description - int position - } - resources { - char id PK - char companyId FK - char templateId FK - varchar name - tinyint active - text notes - varchar integrationKind "nullable" - char addressFieldId FK "nullable" - char contextFieldId FK "nullable" - varchar mailboxKind "nullable" - char accountId FK "nullable" - char referenceFieldId FK "nullable" - char resourceTypeId FK "nullable" - char systemId FK "nullable" - char environmentId FK "nullable" - varchar templateName - tinyint sectionsDefined - } - software_installation_revisions { - char id PK - char configurationId FK - int position - char assetId FK - char resourceId FK - varchar version "nullable" - varchar state - text notes "nullable" - char actorId FK - varchar recordedAt - char licenseResourceId FK "nullable" - } - software_installations { - char id PK - char companyId FK - char assetId FK - char resourceId FK - varchar version "nullable" - varchar state - text notes "nullable" - varchar currentKey "nullable" - char licenseResourceId FK "nullable" - } - telework_authorization_revisions { - char id PK - char authorizationId FK - int position - char actorId FK - varchar recordedAt - char personId FK "nullable" - tinyint active - varchar startsOn "nullable" - varchar expiresOn "nullable" - text notes "nullable" - } - telework_authorizations { - char id PK - char companyId FK - char personId FK - tinyint active - varchar startsOn "nullable" - varchar expiresOn "nullable" - text notes "nullable" - } - users { - char id PK - varchar name - varchar email - varchar passwordHash - tinyint active - tinyint isInitialAdmin - int authVersion - } - work_profile_revisions { - char id PK - char profileId FK - char accountId FK "nullable" - int position - char actorId FK - varchar recordedAt - varchar name - char personId FK "nullable" - char assetId FK "nullable" - tinyint active - text notes "nullable" - } - work_profiles { - char id PK - char companyId FK - char personId FK - char accountId FK "nullable" - char assetId FK - varchar currentKey "nullable" - varchar name - tinyint active - text notes "nullable" - } - workstation_assets { - char workstationId PK,FK - char companyId FK - char assetId PK,FK - varchar activeKey "nullable" - } - workstation_members { - char workstationId PK,FK - char assetId PK,FK - varchar kind - int position - } - workstation_revision_members { - char revisionId PK,FK - char assetId PK,FK - varchar kind - int position - } - workstation_revisions { - char id PK - char workstationId FK - char actorId FK - char siteId FK "nullable" - char personId FK "nullable" - varchar recordedAt - int position - varchar name - tinyint active - char computerAssetId FK "nullable" - } - workstations { - char id PK - char companyId FK - char siteId FK "nullable" - char personId FK "nullable" - varchar name - tinyint active - char computerAssetId FK "nullable" - } - access_items ||..o{ access_account_person_revision_values : "accountId" - access_revisions ||..o| access_account_person_revision_values : "ownerId" - company_contacts ||..o{ access_account_person_revision_values : "personId" - access_items ||..o{ access_account_person_values : "accountId" - access_items ||..o| access_account_person_values : "ownerId" - company_contacts ||..o{ access_account_person_values : "personId" - access_items |o..o{ access_account_revision_values : "environmentId" - access_items ||..o{ access_account_revision_values : "systemId" - access_revisions ||..o| access_account_revision_values : "ownerId" - access_items |o..o{ access_account_values : "environmentId" - access_items ||..o| access_account_values : "ownerId" - access_items ||..o{ access_account_values : "systemId" - access_items ||..o{ access_coverage_revision_values : "accountPersonId" - access_items ||..o{ access_coverage_revision_values : "grantId" - access_items |o..o{ access_coverage_revision_values : "membershipId" - access_items ||..o{ access_coverage_revision_values : "needId" - access_revisions ||..o| access_coverage_revision_values : "ownerId" - access_items ||..o{ access_coverage_values : "accountPersonId" - access_items ||..o{ access_coverage_values : "grantId" - access_items |o..o{ access_coverage_values : "membershipId" - access_items ||..o{ access_coverage_values : "needId" - access_items ||..o| access_coverage_values : "ownerId" - access_items ||..o{ access_environment_revision_values : "systemId" - access_revisions ||..o| access_environment_revision_values : "ownerId" - access_items ||..o| access_environment_values : "ownerId" - access_items ||..o{ access_environment_values : "systemId" - access_items |o..o{ access_grant_revision_values : "accountId" - access_items |o..o{ access_grant_revision_values : "groupId" - access_items ||..o{ access_grant_revision_values : "permissionId" - access_items ||..o{ access_grant_revision_values : "resourceId" - access_revisions ||..o| access_grant_revision_values : "ownerId" - access_items |o..o{ access_grant_values : "accountId" - access_items |o..o{ access_grant_values : "groupId" - access_items ||..o| access_grant_values : "ownerId" - access_items ||..o{ access_grant_values : "permissionId" - access_items ||..o{ access_grant_values : "resourceId" - access_items |o..o{ access_group_revision_values : "environmentId" - access_items ||..o{ access_group_revision_values : "systemId" - access_items |o..o{ access_group_revision_values : "templateId" - access_revisions ||..o| access_group_revision_values : "ownerId" - access_items |o..o{ access_group_values : "environmentId" - access_items ||..o| access_group_values : "ownerId" - access_items ||..o{ access_group_values : "systemId" - access_items |o..o{ access_group_values : "templateId" - access_workspaces ||..o{ access_items : "workspaceId" - access_items ||..o{ access_membership_revision_values : "accountId" - access_items ||..o{ access_membership_revision_values : "groupId" - access_revisions ||..o| access_membership_revision_values : "ownerId" - access_items ||..o{ access_membership_values : "accountId" - access_items ||..o{ access_membership_values : "groupId" - access_items ||..o| access_membership_values : "ownerId" - access_items ||..o{ access_need_permissions : "permissionId" - access_need_values ||..o{ access_need_permissions : "ownerId" - access_items ||..o{ access_need_revision_permissions : "permissionId" - access_need_revision_values ||..o{ access_need_revision_permissions : "ownerId" - access_items ||..o{ access_need_revision_values : "resourceId" - access_revisions ||..o| access_need_revision_values : "ownerId" - company_contacts ||..o{ access_need_revision_values : "personId" - access_items ||..o| access_need_values : "ownerId" - access_items ||..o{ access_need_values : "resourceId" - company_contacts ||..o{ access_need_values : "personId" - access_items ||..o{ access_permission_revision_values : "resourceTypeId" - access_revisions ||..o| access_permission_revision_values : "ownerId" - access_items ||..o| access_permission_values : "ownerId" - access_items ||..o{ access_permission_values : "resourceTypeId" - access_items ||..o{ access_person_references : "itemId" - company_contacts ||..o{ access_person_references : "personId" - access_items ||..o{ access_references : "itemId" - access_items ||..o{ access_references : "targetId" - access_items |o..o{ access_resource_revision_values : "environmentId" - access_items ||..o{ access_resource_revision_values : "resourceTypeId" - access_items |o..o{ access_resource_revision_values : "systemId" - access_revisions ||..o| access_resource_revision_values : "ownerId" - access_revisions ||..o| access_resource_type_revision_values : "ownerId" - access_items ||..o| access_resource_type_values : "ownerId" - access_items |o..o{ access_resource_values : "environmentId" - access_items ||..o| access_resource_values : "ownerId" - access_items ||..o{ access_resource_values : "resourceTypeId" - access_items |o..o{ access_resource_values : "systemId" - access_items ||..o{ access_revisions : "itemId" - users ||..o{ access_revisions : "actorId" - access_revisions ||..o| access_system_revision_values : "ownerId" - access_items ||..o| access_system_values : "ownerId" - access_items |o..o{ access_template_revision_values : "systemId" - access_revisions ||..o| access_template_revision_values : "ownerId" - access_items ||..o| access_template_values : "ownerId" - access_items |o..o{ access_template_values : "systemId" - companies |o..o{ access_workspaces : "companyId" - application_configurations ||..o{ application_configuration_revisions : "configurationId" - resources |o..o{ application_configuration_revisions : "resourceId" - users ||..o{ application_configuration_revisions : "actorId" - work_profiles |o..o{ application_configuration_revisions : "profileId" - companies ||..o{ application_configurations : "companyId" - resources ||..o{ application_configurations : "resourceId" - work_profiles ||..o{ application_configurations : "profileId" - asset_catalogs ||..o{ asset_availability : "catalogId" - asset_definitions ||..o{ asset_availability : "definitionId" - companies ||..o{ asset_availability : "companyId" - asset_definitions ||..o{ asset_characteristics : "fieldId" - asset_specification_fields ||..o| asset_characteristics : "assetId, fieldId" - assets ||..o{ asset_characteristics : "assetId" - asset_catalogs ||..o{ asset_definitions : "catalogId" - companies |o..o{ asset_definitions : "companyId" - asset_definitions ||..o{ asset_field_choices : "fieldId" - asset_definitions ||..o{ asset_relationships : "typeId" - assets ||..o{ asset_relationships : "companyId, sourceId" - assets ||..o{ asset_relationships : "companyId, targetId" - assets ||..o{ asset_relationships : "sourceId" - assets ||..o{ asset_relationships : "targetId" - companies ||..o{ asset_relationships : "companyId" - users ||..o{ asset_relationships : "createdBy" - users |o..o{ asset_relationships : "endedBy" - asset_definitions ||..o{ asset_revision_characteristics : "fieldId" - asset_revisions ||..o{ asset_revision_characteristics : "revisionId" - asset_revisions ||..o{ asset_revision_characteristics : "revisionId, assetId" - asset_specification_fields ||..o{ asset_revision_characteristics : "assetId, fieldId" - assets ||..o{ asset_revision_characteristics : "assetId" - asset_brands |o..o{ asset_revisions : "brandId" - asset_definitions ||..o{ asset_revisions : "templateId" - assets ||..o{ asset_revisions : "assetId" - company_sites |o..o{ asset_revisions : "siteId" - users ||..o{ asset_revisions : "actorId" - asset_definitions ||..o{ asset_specification_field_choices : "fieldId" - asset_specification_fields ||..o{ asset_specification_field_choices : "assetId, fieldId" - assets ||..o{ asset_specification_field_choices : "assetId" - asset_definitions ||..o{ asset_specification_fields : "fieldId" - asset_specification_sections |o..o{ asset_specification_fields : "assetId, sectionId" - assets ||..o{ asset_specification_fields : "assetId" - assets ||..o{ asset_specification_sections : "assetId" - assets ||..o{ asset_support : "assetId" - assets ||..o{ asset_support : "companyId, assetId" - companies ||..o{ asset_support : "companyId" - resources ||..o{ asset_support : "companyId, resourceId" - resources ||..o{ asset_support : "resourceId" - asset_support ||..o{ asset_support_revisions : "connectionId" - resources ||..o{ asset_support_revisions : "resourceId" - users ||..o{ asset_support_revisions : "actorId" - asset_definitions ||..o{ asset_template_fields : "fieldId" - asset_definitions ||..o{ asset_template_fields : "templateId" - asset_template_sections |o..o{ asset_template_fields : "sectionId" - asset_template_sections |o..o{ asset_template_fields : "templateId, sectionId" - asset_definitions ||..o{ asset_template_sections : "templateId" - asset_brands |o..o{ assets : "brandId" - asset_definitions ||..o{ assets : "templateId" - companies ||..o{ assets : "companyId" - company_sites |o..o{ assets : "siteId" - certificate_configurations ||..o{ certificate_configuration_revisions : "configurationId" - resources |o..o{ certificate_configuration_revisions : "resourceId" - users ||..o{ certificate_configuration_revisions : "actorId" - work_profiles |o..o{ certificate_configuration_revisions : "profileId" - companies ||..o{ certificate_configurations : "companyId" - resources ||..o{ certificate_configurations : "resourceId" - work_profiles ||..o{ certificate_configurations : "profileId" - companies ||..o{ company_contacts : "companyId" - companies ||..o{ company_departments : "companyId" - companies ||..o| company_logos : "companyId" - companies ||..o{ company_sites : "companyId" - person_department_assignments ||..o{ department_assignment_revisions : "assignmentId" - access_items |o..o{ folder_mapping_revisions : "needId" - folder_mappings ||..o{ folder_mapping_revisions : "mappingId" - users ||..o{ folder_mapping_revisions : "actorId" - work_profiles |o..o{ folder_mapping_revisions : "profileId" - access_items ||..o{ folder_mappings : "needId" - companies ||..o{ folder_mappings : "companyId" - work_profiles ||..o{ folder_mappings : "profileId" - users |o..o{ http_sessions : "userId" - users |o..o{ identity_state : "initialAdminId" - access_items |o..o{ mail_configuration_revisions : "accountId" - assets |o..o{ mail_configuration_revisions : "assetId" - company_contacts |o..o{ mail_configuration_revisions : "personId" - mail_configurations ||..o{ mail_configuration_revisions : "configurationId" - mailboxes |o..o{ mail_configuration_revisions : "mailboxId" - users ||..o{ mail_configuration_revisions : "actorId" - work_profiles |o..o{ mail_configuration_revisions : "profileId" - access_items |o..o{ mail_configurations : "accountId" - assets |o..o{ mail_configurations : "assetId" - companies ||..o{ mail_configurations : "companyId" - company_contacts |o..o{ mail_configurations : "personId" - mailboxes |o..o{ mail_configurations : "mailboxId" - work_profiles |o..o{ mail_configurations : "profileId" - access_items |o..o{ mailbox_revisions : "accountId" - mailboxes ||..o{ mailbox_revisions : "itemId" - users ||..o{ mailbox_revisions : "actorId" - company_contacts |o..o{ mailbox_use_revisions : "personId" - mailbox_uses ||..o{ mailbox_use_revisions : "itemId" - mailboxes |o..o{ mailbox_use_revisions : "mailboxId" - users ||..o{ mailbox_use_revisions : "actorId" - companies ||..o{ mailbox_uses : "companyId" - company_contacts ||..o{ mailbox_uses : "personId" - mailboxes ||..o{ mailbox_uses : "mailboxId" - access_items |o..o{ mailboxes : "accountId" - companies ||..o{ mailboxes : "companyId" - companies ||..o{ person_department_assignments : "companyId" - company_contacts ||..o{ person_department_assignments : "personId, companyId" - company_departments ||..o{ person_department_assignments : "departmentId, companyId" - company_contacts ||..o| person_photos : "personId, companyId" - person_process_revisions ||..o{ person_process_revision_tasks : "ownerId" - company_contacts |o..o{ person_process_revisions : "personId" - person_processes ||..o{ person_process_revisions : "itemId" - process_templates |o..o{ person_process_revisions : "templateId" - users |o..o{ person_process_revisions : "actorId" - person_processes ||..o{ person_process_tasks : "ownerId" - companies ||..o{ person_processes : "companyId" - company_contacts ||..o{ person_processes : "personId" - process_templates ||..o{ person_processes : "templateId" - assets |o..o{ printer_configuration_revisions : "printerId" - printer_configurations ||..o{ printer_configuration_revisions : "configurationId" - users ||..o{ printer_configuration_revisions : "actorId" - work_profiles |o..o{ printer_configuration_revisions : "profileId" - assets ||..o{ printer_configurations : "printerId" - companies ||..o{ printer_configurations : "companyId" - work_profiles ||..o{ printer_configurations : "profileId" - process_templates ||..o{ process_template_instructions : "ownerId" - process_template_revisions ||..o{ process_template_revision_instructions : "ownerId" - process_templates ||..o{ process_template_revisions : "itemId" - users |o..o{ process_template_revisions : "actorId" - assets |o..o{ remote_connection_revisions : "sourceAssetId" - assets |o..o{ remote_connection_revisions : "targetAssetId" - company_contacts |o..o{ remote_connection_revisions : "personId" - company_sites |o..o{ remote_connection_revisions : "siteId" - remote_connections ||..o{ remote_connection_revisions : "connectionId" - resources |o..o{ remote_connection_revisions : "targetResourceId" - users ||..o{ remote_connection_revisions : "actorId" - assets |o..o{ remote_connections : "sourceAssetId" - assets |o..o{ remote_connections : "targetAssetId" - companies ||..o{ remote_connections : "companyId" - company_contacts ||..o{ remote_connections : "personId" - company_sites |o..o{ remote_connections : "siteId" - resources |o..o{ remote_connections : "targetResourceId" - companies ||..o{ resource_availability : "companyId" - resource_catalogs ||..o{ resource_availability : "catalogId" - resource_definitions ||..o{ resource_availability : "definitionId" - resource_definitions ||..o{ resource_characteristics : "fieldId" - resources ||..o{ resource_characteristics : "resourceId" - companies |o..o{ resource_definitions : "companyId" - resource_catalogs ||..o{ resource_definitions : "catalogId" - resource_definitions ||..o{ resource_field_choices : "fieldId" - resource_definitions ||..o{ resource_revision_characteristics : "fieldId" - resource_revisions ||..o{ resource_revision_characteristics : "revisionId" - resources ||..o{ resource_revision_characteristics : "resourceId" - access_items |o..o{ resource_revisions : "accountId" - access_items |o..o{ resource_revisions : "environmentId" - access_items |o..o{ resource_revisions : "resourceTypeId" - access_items |o..o{ resource_revisions : "systemId" - resource_definitions |o..o{ resource_revisions : "addressFieldId" - resource_definitions |o..o{ resource_revisions : "contextFieldId" - resource_definitions |o..o{ resource_revisions : "referenceFieldId" - resource_definitions |o..o{ resource_revisions : "templateId" - resources ||..o{ resource_revisions : "resourceId" - users ||..o{ resource_revisions : "actorId" - resource_definitions ||..o{ resource_specification_field_choices : "fieldId" - resources ||..o{ resource_specification_field_choices : "resourceId" - resource_definitions ||..o{ resource_specification_fields : "fieldId" - resources ||..o{ resource_specification_fields : "resourceId" - resources ||..o{ resource_specification_sections : "resourceId" - resource_definitions ||..o{ resource_template_fields : "fieldId" - resource_definitions ||..o{ resource_template_fields : "templateId" - resource_template_sections |o..o{ resource_template_fields : "templateId, sectionId" - resource_definitions ||..o{ resource_template_sections : "templateId" - access_items |o..o{ resources : "accountId" - access_items |o..o{ resources : "environmentId" - access_items |o..o{ resources : "resourceTypeId" - access_items |o..o{ resources : "systemId" - companies ||..o{ resources : "companyId" - resource_definitions |o..o{ resources : "addressFieldId" - resource_definitions |o..o{ resources : "contextFieldId" - resource_definitions |o..o{ resources : "referenceFieldId" - resource_definitions ||..o{ resources : "templateId" - assets ||..o{ software_installation_revisions : "assetId" - resources |o..o{ software_installation_revisions : "licenseResourceId" - resources ||..o{ software_installation_revisions : "resourceId" - software_installations ||..o{ software_installation_revisions : "configurationId" - users ||..o{ software_installation_revisions : "actorId" - assets ||..o{ software_installations : "assetId" - companies ||..o{ software_installations : "companyId" - resources |o..o{ software_installations : "licenseResourceId" - resources ||..o{ software_installations : "resourceId" - company_contacts |o..o{ telework_authorization_revisions : "personId" - telework_authorizations ||..o{ telework_authorization_revisions : "authorizationId" - users ||..o{ telework_authorization_revisions : "actorId" - companies ||..o{ telework_authorizations : "companyId" - company_contacts ||..o| telework_authorizations : "personId" - access_items |o..o{ work_profile_revisions : "accountId" - assets |o..o{ work_profile_revisions : "assetId" - company_contacts |o..o{ work_profile_revisions : "personId" - users ||..o{ work_profile_revisions : "actorId" - work_profiles ||..o{ work_profile_revisions : "profileId" - access_items |o..o{ work_profiles : "accountId" - assets ||..o{ work_profiles : "assetId" - companies ||..o{ work_profiles : "companyId" - company_contacts ||..o{ work_profiles : "personId" - assets ||..o{ workstation_assets : "assetId" - assets ||..o{ workstation_assets : "companyId, assetId" - companies ||..o{ workstation_assets : "companyId" - workstations ||..o{ workstation_assets : "companyId, workstationId" - workstations ||..o{ workstation_assets : "workstationId" - assets ||..o{ workstation_members : "assetId" - workstations ||..o{ workstation_members : "workstationId" - assets ||..o{ workstation_revision_members : "assetId" - workstation_revisions ||..o{ workstation_revision_members : "revisionId" - assets |o..o{ workstation_revisions : "computerAssetId" - company_contacts |o..o{ workstation_revisions : "personId" - company_sites |o..o{ workstation_revisions : "siteId" - users ||..o{ workstation_revisions : "actorId" - workstations ||..o{ workstation_revisions : "workstationId" - assets |o..o{ workstations : "computerAssetId" - companies ||..o{ workstations : "companyId" - company_contacts |o..o{ workstations : "personId" - company_sites |o..o{ workstations : "siteId" diff --git a/docs/workplace-dashboard-implementacion.md b/docs/workplace-dashboard-implementacion.md new file mode 100644 index 0000000..e10e618 --- /dev/null +++ b/docs/workplace-dashboard-implementacion.md @@ -0,0 +1,205 @@ +# Dashboard de puesto de trabajo: implementación + +Fecha: 22 de octubre de 2026. Ruta real: `/companies/:companyId/workstations/:id`. + +## Resultado y referencias + +La ficha presenta ordenador principal, configuraciones pendientes, personas y perfiles, responsable y contexto, equipamiento, software, configuraciones por perfil, soporte/acceso remoto y último cambio. El historial completo, edición, renombrado, alta de perfil y ciclo de vida siguen usando las operaciones existentes. + +La implementación utiliza consultas reales. El JSON de la auditoría solo se importa en pruebas; no alimenta la aplicación ni se inserta en una BD. No hay endpoints, modelos, esquemas, migraciones ni dependencias nuevos. + +Se han conservado los ocho entregables de [la auditoría](auditorias/workplace-dashboard/08-resumen-ejecutivo.md) como descripción del estado anterior y base funcional. No existe una imagen aprobada accesible en esta conversación o adjunta a la tarea: la comparación se realiza con la especificación textual del usuario. No se afirma fidelidad de píxel respecto a una imagen no disponible. + +## Composición y diferencias justificadas + +- El shell, sus sidebars, búsqueda superior y navegación conservan su implementación. +- Cabecera con breadcrumb, nombre editable, estado, compañía, sede y responsable. Editar, historial y menú de ciclo de vida permanecen accesibles. +- La primera banda reserva al ordenador la columna izquierda y sitúa pendientes al extremo derecho, sin ocupar el hueco con estadísticas. No se han creado las cuatro tarjetas eliminadas. +- El grid inferior utiliza `minmax(0,1.6fr)` y `minmax(0,1fr)` desde 1000 px de contenedor: aproximadamente 62/38. La primera banda cambia desde 900 px. Por debajo se conserva una columna y el orden lógico pedido. +- Paneles compactos con borde, radio y espaciado del sistema existente; sin sombras decorativas ni gráficos. +- Se utiliza el catálogo propio de iconos Tabler. La especificación menciona Lucide, pero prioriza el design system instalado para componentes/tokens. No se ha introducido otra biblioteca ni cambiado vendor. +- El azul de la ficha utiliza los tokens existentes `secondary` y `secondary-foreground`, asignados localmente a los roles primarios dentro de su contenedor. El tema global y los portales de diálogos conservan su configuración. No se añade otra paleta. +- Las plantillas, nombres y fechas visibles proceden del servidor; no se fuerza «Portátil corporativo» ni la fecha de la ilustración cuando el catálogo o las revisiones reales dicen otra cosa. +- Las celdas sin valor usan «—». Responsable ausente en cabecera se expresa como «Sin responsable», un estado semántico fuera de una celda de listado. + +## Componentes reutilizados y componentes nuevos + +| Unidad | Reutilización / motivo | +| --- | --- | +| `DetailPanel`, `DetailField` | Estructura común. `DetailPanel` incorpora `density="compact"`; el valor por defecto conserva sus recetas anteriores | +| `EntityTable`, `EntityStatus`, `entityListState` | Tablas con búsqueda, orden, paginación, columnas configurables y estados inicial/actualización/error/datos anteriores | +| `WorkstationNameEditor`, `WorkstationActions`, `WorkstationEditorDialog`, `WorkProfileEditorDialog` | Operaciones reales conservadas. `WorkstationActions.children` permite intercalar la acción de historial sin duplicar el menú | +| `WorkstationHistoryDialog`, `WorkstationHistory`, `TimeLine` | Historial completo real. Se exporta el diálogo desde la API pública de Assets; añade altura estable y destino de foco de retorno | +| `SoftwareInstallationsPanel` | Gestión completa de instalaciones dentro de `WorkstationSoftwareDialog`; conserva alta, edición, borrado, cerradas e historial | +| `AssetSupport`, `RemoteSummary`, `TeleworkAuthorizationSummary` | Consulta y gestión de soporte/contexto con semántica existente | +| `buildEnvironment` | Reutiliza el cálculo de configuraciones y sus detalles, sin copiar políticas de autorización | +| `WorkstationComputer`, `WorkstationResponsible`, `WorkstationLatestChange` | Presentación específica de responsabilidades distintas de la ficha | +| `WorkstationPendingConfigurations`, `WorkstationProfileConfigurations`, `WorkstationConfigurationFamily` | Conteo verificable, selección explícita y grupos con estado independiente | +| `WorkstationSoftware`, `WorkstationSupport` | Integración de fuentes/operaciones existentes con contexto del equipo y la persona seleccionados | +| `WorkstationQueryFeedback`, `WorkstationDashboardSkeleton` | Feedback repetido por fuente y geometría de carga inicial de esta pantalla | + +`WorkstationAssets` y `WorkstationProfiles` se adaptan en sus ubicaciones existentes. El antiguo `WorkstationPeople` se retira tras buscar sus referencias: solo lo utilizaban la composición anterior y sus pruebas. La distinción entre responsable y personas usuarias se presenta ahora en los bloques de perfiles y contexto. Sus referencias en la auditoría son históricas, no importaciones activas. + +Los componentes nuevos viven en `modules/companies/src/web/workstations/detail/`; no se ha creado un framework de widgets ni trasladado lógica específica a la biblioteca visual genérica. + +## Consultas, relaciones y selección + +El controller conserva inventario, puesto y operaciones locales. `useWorkstationConfigurationQueries` consulta recursos, Access, carpetas, correo, aplicaciones, impresoras, certificados, remoto y teletrabajo con claves centralizadas. Software utiliza su clave por compañía y soporte su clave por activo. No se crea una consulta por perfil o tarjeta de equipamiento. + +Las consultas del detalle de perfiles/configuraciones/software/soporte no se habilitan sin un ordenador principal confirmado dentro de `assetIds`. El shell puede conservar sus consultas propias por compañía. Se ha retirado del controller la consulta de catálogo que ya no necesitaba esta composición; las especificaciones de activos proporcionan su plantilla legible. + +Las relaciones se resuelven por ID y compañía. Los perfiles pertenecen al ordenador principal; el responsable procede de `personId`. Los activos actuales proceden exclusivamente de `assetIds`. Las impresoras configuradas por perfil y los activos históricos no se incorporan a ese equipamiento. + +Perfiles activos y archivados se distinguen explícitamente; el acceso a archivados amplía la tabla existente. La ausencia de cuenta no invalida un perfil. El selector de configuraciones utiliza perfiles activos, conserva el ID seleccionado durante refetch y, si deja de ser válido, muestra el primer activo con una explicación. No se sincroniza estado remoto mediante efectos. + +Cada configuración conserva su familia, perfil y procedencia. Aplicación del perfil e instalación de equipo son registros distintos. Las carpetas muestran estado y cobertura documental por separado, incluida cobertura desconocida. Cuando faltan metadatos de recursos/Access, se informa de esa dependencia y no se presenta una lista vacía como si fuese un resultado completo. + +El soporte corresponde al ordenador. El contexto remoto identifica a la persona del perfil seleccionado y limita conexiones a las que usan ese activo como origen o destino. La autorización de teletrabajo es de esa persona. No se inicia conexión ni se deduce presencia, VPN activa o permiso efectivo sobre SMB. + +## Indicadores y revisiones + +La única tarjeta estadística es «Configuraciones pendientes». Cuenta IDs distintos por familia de carpetas, correo, aplicaciones, impresoras y certificados en estado `pending`, con `profileId` de un perfil activo del principal y la misma compañía. Excluye legacy sin perfil, archivados, otras compañías, `removal_pending`, revisión y software instalado. + +El total solo se presenta si perfiles y las cinco consultas necesarias tienen datos sin fallo. Ante fallo deja de ser definitivo, aunque se conserven datos anteriores. Sin ordenador la población de perfiles del principal es vacía y el total es cero. El botón de pendientes selecciona un perfil con pendientes, activa el filtro y lleva el foco a la sección. + +Estado técnico y asignación de uso se distinguen: `values.status` describe el estado técnico; `assigned` indica reserva en un puesto activo. Un equipo averiado puede conservar su asignación y no debe aparecer como disponible. No se usa un semáforo global de salud. + +Último cambio compara las dos últimas revisiones por fecha, sin mutar arrays: nombre, estado, sede, responsable, principal y miembros añadidos/retirados. Solo describe diferencias verificadas. No se mezclan eventos de otras entidades; las etiquetas relacionadas se resuelven con sus nombres disponibles actualmente. + +## Carga, errores y accesibilidad + +La carga inicial conserva una geometría próxima al dashboard. Cada consulta secundaria presenta skeleton, actualización o error con reintento propio; no derriba la cabecera ni las otras tarjetas. Las tablas conservan datos y controles si falla una actualización con caché. No se desmonta el selector durante refetch ni se oculta la diferencia entre cero y total desconocido. + +Se reutilizan enlaces reales, regiones nombradas, un `h1`, paneles `h2` y subgrupos `h3/h4`. No hay contenedores con clic que sustituyan a enlaces. Los estados tienen texto, los iconos decorativos están ocultos y las tablas mantienen cabeceras. Las rutas largas se pueden leer completas mediante ajuste de línea; el overflow de tablas queda dentro de su zona. + +El historial devuelve el foco al disparador concreto; el acceso a pendientes mueve el foco a una sección identificada. Las primitivas existentes gestionan teclado, menús, selectores y diálogos. La verificación interactiva y sus límites se registran abajo; no se declara cumplimiento WCAG global. + +## Entorno de pruebas y reproducción + +Se utilizó el preparador aislado `scripts/visual-audit.py`, sin conectar a la BD habitual. Su MariaDB desechable utiliza exclusivamente `metis_desk_test`, API/Vite en puertos libres y sesión real de usuario ordinario. El origen anunciado de esta ejecución fue `http://localhost:49383`. + +Preparación y pruebas reproducibles, desde WSL: + +```bash +bash scripts/setup-playwright-wsl.sh +python3 scripts/visual-audit.py +# En otra terminal, con la ruta privada anunciada por el preparador: +METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-ID/session.json \ + PYTHONDONTWRITEBYTECODE=1 python3 scripts/visual-audit-volume.py +METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-ID/session.json \ + PYTHONDONTWRITEBYTECODE=1 python3 scripts/workstation-dashboard-fixtures.py +METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-ID/session.json \ + PYTHONDONTWRITEBYTECODE=1 python3 scripts/workstation-dashboard-ui.test.py +``` + +Los nuevos preparadores rechazan sesiones fuera de la ubicación privada y escenarios de otra instalación. Crean datos mediante HTTP real y CSRF. Las comprobaciones que sustituyen el principal o modifican la cuenta de carpeta restauran sus valores en `finally`. No se guardan credenciales en el informe ni se instala Chromium en WSL. + +Volumen inicial de la auditoría interactiva: **15 compañías, 59 personas, 135 activos y 73 recursos** en la compañía principal. La variante S04 tiene **80 miembros y 60 perfiles** (50 activos y 10 archivados). S10 añade un activo sintético de sustitución, elevando después el inventario a 136; queda dentro del mismo entorno desechable. + +## Verificaciones y límites + +Verificaciones ejecutadas sobre esta implementación: + +| Comprobación | Resultado | +| --- | --- | +| Tipos de Companies, Assets, RDX UI y Web | Correctos con comandos dirigidos | +| Pruebas dirigidas de dashboard, relaciones y ficha | 20 pruebas correctas, usando `--test-isolation=none` para exponer cada caso | +| Biome sobre archivos del cambio | Correcto, sin supresiones | +| ESLint de espaciado sobre archivos del cambio | Correcto | +| `pnpm build` | Correcto; Vite conserva advertencia de bundle mayor de 500 kB | +| Playwright MCP | 11 métodos interactivos verificados mediante pasadas completas y dirigidas; cubren S01–S11, teclado, contraste, paginación y guardados reales | +| `pnpm typecheck` global | Se detiene en `packages/shadcn-ui/src/components/calendar.tsx:165`, por opcionalidad exacta de `locale`; vendor no modificado | +| `pnpm lint` global | 15 errores fuera del cambio, principalmente bloques sin llaves en código de fotos; también diagnósticos de ejemplos de skills | +| `pnpm check` global | 167 errores fuera del cambio, incluyendo formato de ejemplos/fixtures y código de fotos | +| `pnpm test` global | La última pasada completa deja 41 de 42 archivos de Companies correctos; falla únicamente `companies-page.test.tsx`, con las dos aserciones ajenas descritas abajo. El test del dashboard detectado inicialmente se corrigió y pasa | + +Las dos aserciones ajenas esperan los literales `Personas (1)` y `Esta persona no tiene acceso a Metis Desk`; los archivos de compañías/personas afectados no se han modificado en esta tarea. No se amplía el alcance para corregirlas. Los fallos globales impiden declarar todo el monorepo en verde; las comprobaciones dirigidas distinguen el dashboard de esa deuda. + +La primera pasada interactiva detectó etiquetas de prueba antiguas, una espera de refetch que no provocaba un evento de reconexión real y una comprobación de texto que incluía una etiqueta oculta al lector. Se corrigieron los recorridos sin ocultar fallos ni introducir esperas arbitrarias. También se corrigieron una etiqueta de compañía sin traducir y las cabeceras extensas de perfiles observadas en las capturas. + +Quedan fuera de lo acreditado: comparación con la imagen original ausente, lector de pantalla real, zoom 200/400 %, modo oscuro y auditoría WCAG completa. No se implementa autorización granular por compañía: se conservan autenticación y comprobaciones de pertenencia existentes, con el límite documentado en la auditoría. + +## Evidencias visuales + +Las capturas e informes sintéticos se guardan en `.artifacts/playwright/workstation-dashboard-implementation/`, fuera de Git. Se abrieron capturas de escritorio, móvil y fallos parciales para revisar la composición; la inspección final se detalla al cerrar la pasada. + +Se generan S01 en 1600, 1440, 1280, 1024, 768 y 390 px; vistas de configuraciones; S02, S03, S04 de alta densidad, S05, S06, S07 con datos anteriores, S08 sin coincidencias, S10 y S11. `measurements.json` registra geometría, hashes y comprobaciones; no contiene cookies. Se han inspeccionado también S01 de 1600 px, configuraciones en 390 px, cobertura desconocida y desmantelado. Las mediciones no detectaron desbordamiento horizontal global en los seis anchos. El muestreo de 38 textos/acciones/enlaces dio contraste mínimo **5,03:1**. No hubo excepciones JavaScript de página; los HTTP 503 simulados son parte de los escenarios de error. La ubicación local no garantiza conservación portable: al compartir el trabajo hay que adjuntar los PNG elegidos, nunca `session.json`. + +## Archivos del cambio + +Los siguientes archivos contienen la implementación, sus pruebas o su documentación. Los ocho entregables originales de auditoría permanecen fuera de estos cambios. + +- [docs/componentes-de-interfaz.md](../docs/componentes-de-interfaz.md) +- [docs/workplace-dashboard-implementacion.md](../docs/workplace-dashboard-implementacion.md) +- [modules/assets/src/common/locales/en.json](../modules/assets/src/common/locales/en.json) +- [modules/assets/src/common/locales/es.json](../modules/assets/src/common/locales/es.json) +- [modules/assets/src/web/index.ts](../modules/assets/src/web/index.ts) +- [modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx](../modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx) +- [modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx](../modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx) +- [modules/companies/package.json](../modules/companies/package.json) +- [modules/companies/src/web/workstations/detail/controllers/use-workstation-configuration-queries.ts](../modules/companies/src/web/workstations/detail/controllers/use-workstation-configuration-queries.ts) +- [modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts](../modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts) +- [modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.test.ts](../modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.test.ts) +- [modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.ts](../modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.ts) +- [modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-computer.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-computer.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-configuration-family.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-configuration-family.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-dashboard-skeleton.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-dashboard-skeleton.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-latest-change.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-latest-change.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-pending-configurations.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-pending-configurations.tsx) +- `modules/companies/src/web/workstations/detail/ui/workstation-people.tsx` — retirado; responsabilidad integrada en los bloques actuales. +- [modules/companies/src/web/workstations/detail/ui/workstation-profile-configurations.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-profile-configurations.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-query-feedback.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-query-feedback.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-responsible.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-responsible.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-software-dialog.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-software-dialog.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-software.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-software.tsx) +- [modules/companies/src/web/workstations/detail/ui/workstation-support.tsx](../modules/companies/src/web/workstations/detail/ui/workstation-support.tsx) +- [packages/rdx-ui/src/components/detail-panel.tsx](../packages/rdx-ui/src/components/detail-panel.tsx) +- [scripts/workstation-dashboard-fixtures.py](../scripts/workstation-dashboard-fixtures.py) +- [scripts/workstation-dashboard-ui.test.py](../scripts/workstation-dashboard-ui.test.py) + +## Skills consultadas + +`frontend-design` guio la composición específica y su revisión visual; +`ux-dashboard`, la jerarquía y la exclusión de estadísticas decorativas; +`ux-tables`, las cabeceras breves, comparación y manejo de volumen; +`shadcn`, la comprobación de APIs Base UI y composición de primitivas; +`vercel-composition-patterns`, los slots y la separación de responsabilidades; +`vercel-react-best-practices`, la caché compartida, la ausencia de efectos para datos +derivados y las consultas sin N+1. No se instaló ninguna skill ni auditor externo. + +## Comandos dirigidos adicionales + +```bash +pnpm --filter @metis/companies typecheck +pnpm --filter @metis/assets typecheck +pnpm --filter @repo/rdx-ui typecheck +pnpm --filter @metis/web typecheck +TSX_TSCONFIG_PATH=tsconfig.json node --import tsx --test --test-isolation=none \ + modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.test.ts \ + modules/companies/src/web/workstations/detail/helpers/workstation-relations.test.ts \ + modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx +``` + +Se comprobaron Biome y ESLint sobre la lista de archivos de código anterior y +`git diff --check`. Los comandos globales ejecutados fueron `pnpm typecheck`, +`pnpm lint`, `pnpm test`, `pnpm check` y `pnpm build`, con los resultados y límites +indicados. La sintaxis de los dos scripts Python se comprobó con `ast.parse`, sin +instalar paquetes ni dejar bytecode en el repositorio. + +### Capturas seleccionadas para revisión + +- [S01, escritorio 1600 px](../.artifacts/playwright/workstation-dashboard-implementation/S01-1600.png). +- [S01, móvil 390 px](../.artifacts/playwright/workstation-dashboard-implementation/S01-390.png). +- [Configuraciones en móvil](../.artifacts/playwright/workstation-dashboard-implementation/S01-configurations-390.png). +- [Cobertura documental desconocida](../.artifacts/playwright/workstation-dashboard-implementation/S11-unknown-coverage.png). +- [Errores parciales](../.artifacts/playwright/workstation-dashboard-implementation/S05-folders-only-error.png). +- [Puesto desmantelado](../.artifacts/playwright/workstation-dashboard-implementation/S06.png). + +Se conservan 18 PNG y el informe de medidas en esa carpeta estable. No incluye +sesiones ni credenciales. El preparador y su contenedor se han retirado al terminar; se comprobó que +la sesión privada ya no existe y las capturas permanecen para revisión local. diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index f2323e8..d3f0721 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -688,5 +688,65 @@ "refreshing": "Updating network data…", "stale": "Network data could not be updated. Showing the last available network data." } + }, + "workstationDashboard": { + "breadcrumb": "Workstation navigation", + "companies": "Companies", + "detail": "Workstation details", + "viewHistory": "View history", + "fullHistory": "View full history", + "noResponsible": "No responsible person", + "technicalStatus": "Technical status", + "usageStatus": "Usage status", + "openAsset": "Open asset details", + "responsibleContext": "Responsibility and context", + "department": "Department", + "position": "Position", + "latestChange": "Latest change", + "membersAdded": "Assets added: {{names}}.", + "membersRemoved": "Assets removed from the workstation: {{names}}.", + "changed": { + "name": "The workstation name was updated.", + "active": "The workstation status changed.", + "siteId": "The workstation site changed.", + "personId": "The responsible person changed.", + "computerAssetId": "The main computer changed." + }, + "software": "Main computer software", + "manageSoftware": "Manage software", + "equipment": "Equipment", + "principal": "Main", + "searchEquipment": "Search equipment…", + "pending": "Pending configurations", + "totalUnknown": "The pending total cannot be confirmed.", + "viewPending": "View pending configurations", + "peopleProfiles": "People and profiles", + "archived_one": "{{count}} archived profile", + "archived_other": "{{count}} archived profiles", + "hideArchived": "Hide archived profiles", + "configurations": "Configurations by profile", + "selectProfile": "Select profile", + "selectionChanged": "The previous profile is no longer available. The first active profile is shown.", + "openProfile": "Open profile", + "pendingOnly": "Pending only", + "noActiveProfiles": "There are no active profiles for this computer.", + "support": "Support and remote access", + "recordedSupport": "Recorded support", + "documentedOnly": "Recorded availability; this does not check a live connection.", + "remoteContext": "Remote context", + "remoteScope": "Information for the selected profile's person and connections that use this computer.", + "supportNeedsComputer": "Assign a main computer to view its support.", + "accounts": "Access accounts", + "loadingSection": "Loading {{section}}", + "loadError": "Could not load {{section}}.", + "stale": "Could not refresh {{section}}. Previous data is retained.", + "refreshing": "Refreshing…", + "company": "Company", + "resources": "Resources", + "profile": "Profile", + "person": "Person", + "account": "Account", + "configurationSummary": "Configurations", + "unassigned": "No active workstation" } } diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index d2a946d..7ebca05 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -688,5 +688,65 @@ "refreshing": "Actualizando datos de red…", "stale": "No se pudieron actualizar los datos de red. Se muestran los últimos datos de red disponibles." } + }, + "workstationDashboard": { + "breadcrumb": "Navegación del puesto", + "companies": "Compañías", + "detail": "Ficha del puesto", + "viewHistory": "Ver historial", + "fullHistory": "Ver historial completo", + "noResponsible": "Sin responsable", + "technicalStatus": "Estado técnico", + "usageStatus": "Estado de uso", + "openAsset": "Abrir ficha del activo", + "responsibleContext": "Responsable y contexto", + "department": "Departamento", + "position": "Cargo", + "latestChange": "Último cambio", + "membersAdded": "Activos incorporados: {{names}}.", + "membersRemoved": "Activos retirados del puesto: {{names}}.", + "changed": { + "name": "Se actualizó el nombre del puesto.", + "active": "Se cambió el estado del puesto.", + "siteId": "Se cambió la sede del puesto.", + "personId": "Se cambió el responsable del puesto.", + "computerAssetId": "Se cambió el ordenador principal." + }, + "software": "Software del ordenador principal", + "manageSoftware": "Gestionar software", + "equipment": "Equipamiento", + "principal": "Principal", + "searchEquipment": "Buscar equipamiento…", + "pending": "Configuraciones pendientes", + "totalUnknown": "No se puede confirmar el total de pendientes.", + "viewPending": "Ver configuraciones pendientes", + "peopleProfiles": "Personas y perfiles", + "archived_one": "{{count}} perfil archivado", + "archived_other": "{{count}} perfiles archivados", + "hideArchived": "Ocultar archivados", + "configurations": "Configuraciones por perfil", + "selectProfile": "Seleccionar perfil", + "selectionChanged": "El perfil anterior ya no está disponible. Se muestra el primer perfil activo.", + "openProfile": "Abrir perfil", + "pendingOnly": "Solo pendientes", + "noActiveProfiles": "No hay perfiles activos para este ordenador.", + "support": "Soporte y acceso remoto", + "recordedSupport": "Soporte registrado", + "documentedOnly": "Disponibilidad registrada; no comprueba una conexión en tiempo real.", + "remoteContext": "Contexto remoto", + "remoteScope": "Información de la persona del perfil seleccionado y conexiones que utilizan este ordenador.", + "supportNeedsComputer": "Asigna un ordenador principal para consultar su soporte.", + "accounts": "Cuentas de acceso", + "loadingSection": "Cargando {{section}}", + "loadError": "No se pudo cargar {{section}}.", + "stale": "No se pudo actualizar {{section}}. Se conservan los datos anteriores.", + "refreshing": "Actualizando…", + "company": "Compañía", + "resources": "Recursos", + "profile": "Perfil", + "person": "Persona", + "account": "Cuenta", + "configurationSummary": "Configuraciones", + "unassigned": "Sin puesto activo" } } diff --git a/modules/assets/src/web/index.ts b/modules/assets/src/web/index.ts index b417c64..0fb8e98 100644 --- a/modules/assets/src/web/index.ts +++ b/modules/assets/src/web/index.ts @@ -19,6 +19,7 @@ export { inventoryKeys } from "./shared/queries"; export { AssetSupport } from "./support-connections"; export { WorkstationEditorDialog, WorkstationForm, WorkstationsPage } from "./workstations"; export { WorkstationHistory } from "./workstations/history/ui/workstation-history"; +export { WorkstationHistoryDialog } from "./workstations/history/ui/workstation-history-dialog"; export { WorkstationNameEditor } from "./workstations/rename/ui/workstation-name-editor"; export { WorkstationActions } from "./workstations/shared/ui/workstation-actions"; export { WorkstationLifecycleDialog } from "./workstations/shared/ui/workstation-lifecycle-dialog"; diff --git a/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx b/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx index 9152a84..f5ece26 100644 --- a/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx +++ b/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx @@ -1,4 +1,5 @@ import { FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog"; +import type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; import type { WorkstationHistoryProps } from "./workstation-history"; @@ -10,12 +11,18 @@ export function WorkstationHistoryDialog({ siteName, members, onClose, -}: WorkstationHistoryProps & { onClose: () => void }) { + finalFocus, +}: WorkstationHistoryProps & { + onClose: () => void; + finalFocus?: ComponentProps["finalFocus"]; +}) { const { t } = useTranslation("assets"); return ( { if (!open) { onClose(); diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx index 4d78545..5b78b89 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx @@ -1,5 +1,6 @@ import { EntityAction, EntityRowActions } from "@repo/rdx-ui/components/entity-list"; import { Button } from "@repo/shadcn-ui/components/button"; +import type { ReactNode } from "react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -12,11 +13,13 @@ export function WorkstationActions({ onEdit, onDeleted, onDecommissioned, + children, }: { workstation: WorkstationDTO; onEdit: () => void; onDeleted: () => void; onDecommissioned: () => void; + children?: ReactNode; }) { const { t } = useTranslation("assets"); const [action, setAction] = useState<"decommission" | "delete">(); @@ -27,8 +30,9 @@ export function WorkstationActions({ return ( <> -
+
+ {children} {canDecommission && ( setAction("decommission")}> diff --git a/modules/companies/package.json b/modules/companies/package.json index 5aab6a4..8f944ac 100644 --- a/modules/companies/package.json +++ b/modules/companies/package.json @@ -10,7 +10,7 @@ }, "scripts": { "typecheck": "tsc -p tsconfig.json", - "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/domain/company.aggregate.test.ts src/api/domain/people.test.ts src/web/companies/shared/companies-page.test.tsx src/api/domain/access/access.test.ts src/api/domain/mail-configuration.test.ts src/api/domain/mailbox.test.ts src/web/access/shared/access.test.tsx src/web/mail-configurations/shared/mail.test.tsx src/api/domain/work-profile.test.ts src/web/work-profiles/shared/profile.test.tsx src/api/domain/printer-configuration.test.ts src/web/printer-configurations/shared/printer.test.tsx src/api/domain/application-configuration.test.ts src/web/application-configurations/shared/application.test.tsx src/api/domain/certificate-configuration.test.ts src/web/certificate-configurations/shared/certificate.test.tsx src/web/work-environments/shared/work-environment.test.tsx src/api/domain/telework-authorization.test.ts src/web/telework-authorizations/shared/telework.test.tsx src/api/domain/remote-connection.test.ts src/api/domain/process.test.ts src/web/remote-connections/shared/remote.test.tsx src/web/processes/shared/processes.test.tsx src/web/shared/initials-avatar.test.tsx src/api/infrastructure/company-logo-processor.test.ts src/web/companies/shared/company-avatar.test.tsx src/web/shared/client-contracts.test.ts src/web/infrastructure/shared/helpers/printer-usage.test.ts src/web/workstations/detail/helpers/workstation-relations.test.ts src/web/workstations/detail/ui/workstation-detail.test.tsx src/web/workspace/shared/workspace-routes.test.ts src/web/workspace/view/helpers/sidebar-preference.test.ts src/api/domain/software-installation.test.ts src/web/software-installations/shared/software.test.tsx src/api/application/directory-context-plan.test.ts src/web/access/list/helpers/access-environment-filter.test.ts src/api/domain/folder-mapping-account.test.ts src/api/application/local-identity-plan.test.ts src/web/access/list/helpers/access-resource-filter.test.ts src/web/folder-mappings/shared/helpers/folder-account-options.test.ts src/web/local-identity-environments/shared/local-identities-ui.test.tsx" + "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/domain/company.aggregate.test.ts src/api/domain/people.test.ts src/web/companies/shared/companies-page.test.tsx src/api/domain/access/access.test.ts src/api/domain/mail-configuration.test.ts src/api/domain/mailbox.test.ts src/web/access/shared/access.test.tsx src/web/mail-configurations/shared/mail.test.tsx src/api/domain/work-profile.test.ts src/web/work-profiles/shared/profile.test.tsx src/api/domain/printer-configuration.test.ts src/web/printer-configurations/shared/printer.test.tsx src/api/domain/application-configuration.test.ts src/web/application-configurations/shared/application.test.tsx src/api/domain/certificate-configuration.test.ts src/web/certificate-configurations/shared/certificate.test.tsx src/web/work-environments/shared/work-environment.test.tsx src/api/domain/telework-authorization.test.ts src/web/telework-authorizations/shared/telework.test.tsx src/api/domain/remote-connection.test.ts src/api/domain/process.test.ts src/web/remote-connections/shared/remote.test.tsx src/web/processes/shared/processes.test.tsx src/web/shared/initials-avatar.test.tsx src/api/infrastructure/company-logo-processor.test.ts src/web/companies/shared/company-avatar.test.tsx src/web/shared/client-contracts.test.ts src/web/infrastructure/shared/helpers/printer-usage.test.ts src/web/workstations/detail/helpers/workstation-relations.test.ts src/web/workstations/detail/helpers/workstation-dashboard.test.ts src/web/workstations/detail/ui/workstation-detail.test.tsx src/web/workspace/shared/workspace-routes.test.ts src/web/workspace/view/helpers/sidebar-preference.test.ts src/api/domain/software-installation.test.ts src/web/software-installations/shared/software.test.tsx src/api/application/directory-context-plan.test.ts src/web/access/list/helpers/access-environment-filter.test.ts src/api/domain/folder-mapping-account.test.ts src/api/application/local-identity-plan.test.ts src/web/access/list/helpers/access-resource-filter.test.ts src/web/folder-mappings/shared/helpers/folder-account-options.test.ts src/web/local-identity-environments/shared/local-identities-ui.test.tsx" }, "dependencies": { "@metis/core": "workspace:*", diff --git a/modules/companies/src/web/workstations/detail/controllers/use-workstation-configuration-queries.ts b/modules/companies/src/web/workstations/detail/controllers/use-workstation-configuration-queries.ts new file mode 100644 index 0000000..6fbaa35 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/controllers/use-workstation-configuration-queries.ts @@ -0,0 +1,85 @@ +import { useListQuery } from "@metis/core/client"; +import { inventoryApi, inventoryKeys } from "@metis/resources/client"; + +import { getAccess } from "../../../access/list/api"; +import { accessKeys } from "../../../access/shared/queries"; +import { applicationApi, applicationKeys } from "../../../application-configurations/shared/api"; +import { certificateApi, certificateKeys } from "../../../certificate-configurations/shared/api"; +import { folderApi, folderKeys } from "../../../folder-mappings/shared/api"; +import { mailApi, mailKeys } from "../../../mail-configurations/shared/api"; +import { printerApi, printerKeys } from "../../../printer-configurations/shared/api"; +import { remoteApi, remoteKeys } from "../../../remote-connections/shared/api"; +import { teleworkApi, teleworkKeys } from "../../../telework-authorizations/shared/api"; +import type { EnvironmentSources } from "../../../work-environments/shared/helpers"; + +/** Cada fuente conserva su estado: un fallo secundario no invalida los demás bloques. */ +export function useWorkstationConfigurationQueries(companyId: string, enabled: boolean) { + const resources = useListQuery({ + queryKey: inventoryKeys.company(companyId), + queryFn: () => inventoryApi.get(companyId), + enabled, + }); + const access = useListQuery({ + queryKey: accessKeys.scope(companyId), + queryFn: () => getAccess(companyId), + enabled, + }); + const mail = useListQuery({ + queryKey: mailKeys.company(companyId), + queryFn: () => mailApi.get(companyId), + enabled, + }); + const folders = useListQuery({ + queryKey: folderKeys.company(companyId), + queryFn: () => folderApi.get(companyId), + enabled, + }); + const printers = useListQuery({ + queryKey: printerKeys.company(companyId), + queryFn: () => printerApi.get(companyId), + enabled, + }); + const applications = useListQuery({ + queryKey: applicationKeys.company(companyId), + queryFn: () => applicationApi.get(companyId), + enabled, + }); + const certificates = useListQuery({ + queryKey: certificateKeys.company(companyId), + queryFn: () => certificateApi.get(companyId), + enabled, + }); + const remote = useListQuery({ + queryKey: remoteKeys.company(companyId), + queryFn: () => remoteApi.get(companyId), + enabled, + }); + const telework = useListQuery({ + queryKey: teleworkKeys.company(companyId), + queryFn: () => teleworkApi.get(companyId), + enabled, + }); + + const sources: Omit = { + resources: resources.data?.resources ?? [], + access: access.data?.items ?? [], + mail: mail.data?.configurations ?? [], + folders: folders.data?.mappings ?? [], + printers: printers.data?.configurations ?? [], + applications: applications.data?.configurations ?? [], + certificates: certificates.data?.configurations ?? [], + }; + + return { + sources, + resources, + access, + mail, + folders, + printers, + applications, + certificates, + remote, + telework, + }; +} diff --git a/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts b/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts index 2f7dfcf..d7ac62e 100644 --- a/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts +++ b/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts @@ -1,10 +1,13 @@ -import { catalogApi, catalogKeys, inventoryApi, inventoryKeys } from "@metis/assets/client"; +import { inventoryApi, inventoryKeys } from "@metis/assets/client"; import { useListQuery } from "@metis/core/client"; import { useState } from "react"; import type { CompanyDTO } from "../../../../common"; +import type { EnvironmentSources } from "../../../work-environments/shared/helpers"; import { profileApi, profileKeys } from "../../../work-profiles/shared/api"; -import { workstationRelations } from "../helpers/workstation-relations"; +import { workstationDashboard, workstationPendingCount } from "../helpers/workstation-dashboard"; + +import { useWorkstationConfigurationQueries } from "./use-workstation-configuration-queries"; export function useWorkstationDetailController(company: CompanyDTO, workstationId: string) { const [creatingProfile, setCreatingProfile] = useState(false); @@ -13,33 +16,69 @@ export function useWorkstationDetailController(company: CompanyDTO, workstationI queryKey: inventoryKeys.company(company.id), queryFn: () => inventoryApi.get(company.id), }); + const workstation = inventory.data?.workstations.find( + (item) => item.id === workstationId && item.companyId === company.id + ); + const computer = inventory.data?.assets.find( + (item) => + item.companyId === company.id && + item.id === workstation?.values.computerAssetId && + workstation.values.assetIds.includes(item.id) + ); const profiles = useListQuery({ queryKey: profileKeys.company(company.id), queryFn: () => profileApi.get(company.id), + enabled: Boolean(computer), }); - const catalog = useListQuery({ - queryKey: catalogKeys.scope(company.id), - queryFn: () => catalogApi.get(company.id), - }); - const workstation = inventory.data?.workstations.find((item) => item.id === workstationId); - - const { relatedProfiles, personIds } = workstationRelations( - workstation, - profiles.data?.profiles ?? [] - ); - const people = company.people.filter((person) => personIds.has(person.id)); + const configurationQueries = useWorkstationConfigurationQueries(company.id, Boolean(computer)); + const [selectedProfileId, setSelectedProfileId] = useState(); + const [viewingHistory, setViewingHistory] = useState(false); + const dashboard = workstation + ? workstationDashboard( + company, + workstation, + inventory.data?.assets ?? [], + profiles.data?.profiles ?? [] + ) + : undefined; + const selectedProfile = + dashboard?.activeProfiles.find((item) => item.id === selectedProfileId) ?? + dashboard?.activeProfiles[0]; + const sources: EnvironmentSources = { + assets: inventory.data?.assets ?? [], + workstations: inventory.data?.workstations ?? [], + ...configurationQueries.sources, + }; + const families = [ + configurationQueries.mail, + configurationQueries.folders, + configurationQueries.printers, + configurationQueries.applications, + configurationQueries.certificates, + ]; + const pendingCount = computer + ? profiles.data && !profiles.isError && families.every((query) => query.data && !query.isError) + ? workstationPendingCount(dashboard?.activeProfiles ?? [], sources) + : undefined + : 0; return { + dashboard, + configurationQueries, + sources, + selectedProfile, + selectedProfileId, + setSelectedProfileId, + viewingHistory, + setViewingHistory, + pendingCount, creatingProfile, setCreatingProfile, inventory, - catalog, profiles, workstation, editing, setEditing, - relatedProfiles, - people, }; } diff --git a/modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.test.ts b/modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.test.ts new file mode 100644 index 0000000..d96cf78 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.test.ts @@ -0,0 +1,217 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; + +import { InventoryResponseSchema } from "@metis/assets/common"; +import { z } from "zod/v4"; + +import { ApplicationConfigurationSchema } from "../../../../common/application-configurations"; +import { CompanySchema } from "../../../../common/dto/shared/company.dto"; +import { WorkProfilesResponseSchema } from "../../../../common/work-profiles"; + +import { + workstationDashboard, + workstationOrganization, + workstationPendingCount, + workstationRevisionChanges, +} from "./workstation-dashboard"; + +const fixture: unknown = JSON.parse( + await readFile( + new URL( + "../../../../../../../docs/auditorias/workplace-dashboard/07-datos-ejemplo.json", + import.meta.url + ), + "utf8" + ) +); +const data = z + .object({ + sources: z.object({ + company: CompanySchema, + inventory: InventoryResponseSchema, + profiles: WorkProfilesResponseSchema, + }), + }) + .parse(fixture).sources; +const station = data.inventory.workstations[0]; + +assert.ok(station); + +test("dashboard: ordenador y equipamiento actuales, personas por perfiles y archivados separados", () => { + const model = workstationDashboard( + data.company, + station, + data.inventory.assets, + data.profiles.profiles + ); + + assert.equal(model.computer?.id, station.values.computerAssetId); + assert.equal(model.members.length, 2); + assert.equal(model.activeProfiles.length, 2); + assert.equal(new Set(model.activeProfiles.map((profile) => profile.values.personId)).size, 2); + assert.equal(model.archivedProfiles.length, 1); + assert.ok(!model.members.some((asset) => asset.values.name.startsWith("IMP-RED"))); + assert.equal(model.responsible?.id, station.values.personId); +}); + +test("dashboard: sin responsable sigue mostrando usuarios y un puesto desmantelado no cuenta historia como miembros", () => { + const lab = data.inventory.workstations[1]; + const inactive = data.inventory.workstations[3]; + + assert.ok(lab && inactive); + const model = workstationDashboard( + data.company, + lab, + data.inventory.assets, + data.profiles.profiles + ); + const dismantled = workstationDashboard( + data.company, + inactive, + data.inventory.assets, + data.profiles.profiles + ); + + assert.equal(model.responsible, undefined); + assert.equal(model.activeProfiles.length, 1); + assert.equal(dismantled.members.length, 0); + assert.equal(dismantled.computer, undefined); + assert.equal(dismantled.profiles.length, 0); + assert.ok(dismantled.revisions.some((revision) => revision.values.assetIds.length > 0)); +}); + +test("dashboard: sustituir ordenador resuelve perfiles del nuevo host sin trasladar los anteriores", () => { + const lab = data.inventory.workstations[1]; + + assert.ok(lab); + const changed = { + ...station, + values: { + ...station.values, + computerAssetId: lab.values.computerAssetId, + assetIds: lab.values.assetIds, + }, + }; + const model = workstationDashboard( + data.company, + changed, + data.inventory.assets, + data.profiles.profiles + ); + + assert.equal(model.activeProfiles.length, 1); + assert.equal(model.activeProfiles[0]?.values.assetId, lab.values.computerAssetId); + assert.equal( + data.profiles.profiles.filter( + (profile) => profile.values.assetId === station.values.computerAssetId + ).length, + 3 + ); +}); + +test("dashboard: referencias ajenas o principal fuera de composición no aportan relaciones", () => { + const profile = data.profiles.profiles[0]; + + assert.ok(profile); + const foreign = { ...profile, companyId: "019a0a00-0000-7000-8000-000000000999" }; + const model = workstationDashboard(data.company, station, data.inventory.assets, [foreign]); + const missing = workstationDashboard( + data.company, + { ...station, values: { ...station.values, assetIds: [] } }, + data.inventory.assets, + data.profiles.profiles + ); + + assert.equal(model.profiles.length, 0); + assert.equal(missing.computer, undefined); + assert.equal(missing.profiles.length, 0); + assert.equal(missing.members.length, 0); +}); + +test("pendientes: solo pending, perfiles activos, compañía correcta y deduplicación por familia e identidad", () => { + const profile = data.profiles.profiles[0]; + const archived = data.profiles.profiles[2]; + + assert.ok(profile && archived); + const base = { + id: "019a0a00-0000-7000-8000-000000000888", + companyId: data.company.id, + values: { + profileId: profile.id, + resourceId: "019a0a00-0000-7000-8000-000000000050", + delivery: "web", + version: null, + state: "pending", + notes: null, + }, + needsReview: true, + history: [], + }; + const input = { + mail: [], + folders: [], + printers: [], + certificates: [], + applications: [ + base, + base, + { + ...base, + id: "019a0a00-0000-7000-8000-000000000889", + values: { ...base.values, state: "removal_pending" }, + }, + { ...base, values: { ...base.values, profileId: archived.id } }, + { ...base, companyId: "019a0a00-0000-7000-8000-000000000999" }, + ], + }; + const applications = input.applications.map((item) => + ApplicationConfigurationSchema.parse({ + ...item, + history: [ + { + id: "019a0a00-0000-7000-8000-000000000777", + actorId: data.company.id, + actorName: "Operadora", + recordedAt: "2026-10-20T09:00:00.000Z", + values: item.values, + }, + ], + }) + ); + + assert.equal(workstationPendingCount(data.profiles.profiles, { ...input, applications }), 1); + assert.equal(workstationPendingCount([], { ...input, applications }), 0); +}); + +test("último cambio: orden cronológico sin mutación, altas de miembros y campos realmente cambiados", () => { + const history = [...station.history].reverse(); + const before = history.map((revision) => revision.id); + const change = workstationRevisionChanges(history); + + assert.deepEqual( + history.map((revision) => revision.id), + before + ); + assert.equal(change.latest?.id, station.history.at(-1)?.id); + assert.deepEqual(change.fields, ["name"]); + assert.equal(change.added.length, 1); + assert.equal(change.removed.length, 0); + assert.equal(workstationRevisionChanges([]).latest, undefined); + assert.equal( + workstationRevisionChanges([station.history[0]].filter((item) => item !== undefined)).previous, + undefined + ); +}); + +test("contexto organizativo: cargo y departamento vigentes, sin adoptar asignaciones futuras", () => { + const context = workstationOrganization(data.company, station.values.personId, "2026-10-22"); + + assert.equal(context.department?.name, "Administración y gestión documental"); + assert.equal(context.position, "Responsable de documentación"); + assert.equal(workstationOrganization(data.company, null, "2026-10-22").department, undefined); + assert.equal( + workstationOrganization(data.company, station.values.personId, "2026-08-01").department, + undefined + ); +}); diff --git a/modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.ts b/modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.ts new file mode 100644 index 0000000..6a6dc09 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/helpers/workstation-dashboard.ts @@ -0,0 +1,108 @@ +import type { AssetDTO, WorkstationDTO } from "@metis/assets/common"; + +import type { CompanyDTO, WorkProfileDTO } from "../../../../common"; +import type { EnvironmentSources } from "../../../work-environments/shared/helpers"; + +import { workstationRelations } from "./workstation-relations"; + +export function workstationDashboard( + company: CompanyDTO, + workstation: WorkstationDTO, + assets: readonly AssetDTO[], + profiles: readonly WorkProfileDTO[] +) { + const members = assets.filter( + (asset) => asset.companyId === company.id && workstation.values.assetIds.includes(asset.id) + ); + const computer = members.find((asset) => asset.id === workstation.values.computerAssetId); + const related = computer ? workstationRelations(workstation, profiles).relatedProfiles : []; + const responsible = company.people.find((person) => person.id === workstation.values.personId); + const revisions = [...workstation.history].sort((a, b) => + a.recordedAt.localeCompare(b.recordedAt) + ); + + return { + members, + computer, + responsible, + profiles: related, + activeProfiles: related.filter((profile) => profile.values.active), + archivedProfiles: related.filter((profile) => !profile.values.active), + revisions, + }; +} + +/** Cuenta registros de configuración, no recursos ni señales de revisión que puedan solaparse. */ +export function workstationPendingCount( + profiles: readonly WorkProfileDTO[], + sources: Pick< + EnvironmentSources, + "mail" | "folders" | "applications" | "printers" | "certificates" + > +) { + const active = new Map( + profiles + .filter((profile) => profile.values.active) + .map((profile) => [profile.id, profile.companyId]) + ); + const pending = new Set(); + + for (const kind of ["mail", "folders", "applications", "printers", "certificates"] as const) { + for (const record of sources[kind]) { + const values = record.values; + + if ( + "profileId" in values && + active.get(values.profileId) === record.companyId && + values.state === "pending" + ) { + pending.add(`${kind}:${record.id}`); + } + } + } + + return pending.size; +} + +export function workstationOrganization( + company: CompanyDTO, + personId: string | null, + today: string +) { + const assignments = company.assignments.filter( + (item) => + item.companyId === company.id && + item.personId === personId && + item.startDate <= today && + (item.endDate === null || today < item.endDate) + ); + const assignment = assignments.find((item) => item.isPrimary) ?? assignments[0]; + + return { + department: company.departments.find( + (item) => item.id === assignment?.departmentId && item.companyId === company.id + ), + position: assignment?.position ?? company.people.find((item) => item.id === personId)?.position, + }; +} + +export function workstationRevisionChanges(revisions: WorkstationDTO["history"]) { + const ordered = [...revisions].sort((a, b) => a.recordedAt.localeCompare(b.recordedAt)); + const latest = ordered.at(-1); + const previous = ordered.at(-2); + + if (!(latest && previous)) { + return { latest, previous, fields: [], added: [], removed: [] }; + } + const fields = (["name", "active", "siteId", "personId", "computerAssetId"] as const).filter( + (field) => (latest.values[field] ?? null) !== (previous.values[field] ?? null) + ); + + return { + latest, + previous, + fields, + added: latest.values.assetIds.filter((id) => !previous.values.assetIds.includes(id)), + removed: previous.values.assetIds.filter((id) => !latest.values.assetIds.includes(id)), + }; +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx index fa2d280..35adb18 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx @@ -1,9 +1,9 @@ -import { AssetSummaryCard } from "@metis/assets/client"; -import type { AssetDTO, CatalogItemDTO, WorkstationDTO } from "@metis/assets/common"; +import type { AssetDTO, WorkstationDTO } from "@metis/assets/common"; import { BrowserLink } from "@metis/core/client"; import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; -import { EmptyState } from "@repo/rdx-ui/components/empty-state"; -import { LaptopIcon, MonitorIcon } from "@repo/rdx-ui/lib/icons"; +import { EntityStatus, EntityTable } from "@repo/rdx-ui/components/entity-list"; +import { MonitorIcon } from "@repo/rdx-ui/lib/icons"; +import { Badge } from "@repo/shadcn-ui/components/badge"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -11,79 +11,98 @@ export function WorkstationAssets({ companyId, workstation, assets, - catalog = [], onEdit, }: { companyId: string; workstation: WorkstationDTO; assets: readonly AssetDTO[]; - catalog?: readonly CatalogItemDTO[] | undefined; onEdit: () => void; }) { - const { t } = useTranslation("assets"); - const ids = [...new Set(workstation.values.assetIds)]; - const computerId = workstation.values.computerAssetId; - const computer = assets.find((asset) => asset.id === computerId && ids.includes(asset.id)); - const peripherals = ids.filter((id) => id !== computerId); - - const card = (id: string) => { - const asset = assets.find((item) => item.id === id); - - return ( - - {asset.values.name} - - ) : undefined - } - template={catalog.find((item) => item.id === asset?.values.templateId)} - /> + const { t, i18n } = useTranslation("assets"); + const members = [...new Set(workstation.values.assetIds)] + .map((id) => ({ + id, + asset: assets.find((item) => item.id === id && item.companyId === companyId), + })) + .sort( + (a, b) => + Number(b.id === workstation.values.computerAssetId) - + Number(a.id === workstation.values.computerAssetId) ); - }; return ( - } title={t("workstationDetail.assets")}> - {computer ? ( -
-

- {t("workstationComputer")} -

-
    -
  • - {card(computer.id)} -
  • -
-
- ) : ( - - {t("editWorkstation")} - - } - description={t("workstationDetail.completeComputer")} - icon={} - title={t(ids.length > 0 ? "workstationDetail.noComputer" : "noWorkstationAssets")} - /> - )} - {peripherals.length > 0 && ( -
-

- {t("workstationDetail.peripherals")} -

-
    - {peripherals.map((id) => ( -
  • - {card(id)} -
  • - ))} -
-
- )} + + {t("editWorkstation")} + + } + density="compact" + icon={} + title={t("workstationDashboard.equipment")} + > + item.asset?.values.name, + render: (item) => ( +
+ {item.asset ? ( + + {item.asset.values.name} + + ) : ( + {t("workstationDetail.missingAsset")} + )} + + {item.asset?.specification.templateName ?? "—"} + + {item.id === workstation.values.computerAssetId && ( + + {t("workstationDashboard.principal")} + + )} +
+ ), + }, + { + id: "technical", + label: t("workstationDashboard.technicalStatus"), + value: (item) => (item.asset ? t(item.asset.values.status) : ""), + render: (item) => + item.asset ? ( + + {t(item.asset.values.status)} + + ) : ( + "—" + ), + }, + { + id: "usage", + label: t("workstationDashboard.usageStatus"), + value: (item) => + item.asset + ? t(item.asset.assigned ? "in_use" : "workstationDashboard.unassigned") + : "", + render: (item) => + item.asset + ? t(item.asset.assigned ? "in_use" : "workstationDashboard.unassigned") + : "—", + }, + ]} + emptyMessage={t("noWorkstationAssets")} + items={members} + locale={i18n.language} + searchPlaceholder={t("workstationDashboard.searchEquipment")} + title={t("workstationDashboard.equipment")} + />
); } diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-computer.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-computer.tsx new file mode 100644 index 0000000..47ac752 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-computer.tsx @@ -0,0 +1,72 @@ +import type { AssetDTO } from "@metis/assets/common"; +import { BrowserLink } from "@metis/core/client"; +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; +import { EmptyState } from "@repo/rdx-ui/components/empty-state"; +import { EntityStatus } from "@repo/rdx-ui/components/entity-list"; +import { MonitorIcon } from "@repo/rdx-ui/lib/icons"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { useTranslation } from "react-i18next"; + +export function WorkstationComputer({ + computer, + companyId, + onEdit, +}: { + computer: AssetDTO | undefined; + companyId: string; + onEdit: () => void; +}) { + const { t } = useTranslation("assets"); + + return ( + } title={t("workstationComputer")}> + {computer ? ( +
+ + +
+ + {computer.values.name} + +

{computer.specification.templateName}

+
+
+
+ {t("workstationDashboard.technicalStatus")} +
+
+ + {t(computer.values.status)} + +
+
+
+
{t("workstationDashboard.usageStatus")}
+
{t(computer.assigned ? "in_use" : "workstationDashboard.unassigned")}
+
+
+ + {t("workstationDashboard.openAsset")} + +
+
+ ) : ( + + {t("editWorkstation")} + + } + className="gap-3 border-0 bg-transparent py-3 @min-[560px]:py-3" + description={t("workstationDetail.completeComputer")} + icon={} + title={t("workstationDetail.noComputer")} + /> + )} +
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-configuration-family.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-configuration-family.tsx new file mode 100644 index 0000000..80d58ec --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-configuration-family.tsx @@ -0,0 +1,99 @@ +import { BrowserLink } from "@metis/core/client"; +import { FileKeyIcon, FolderIcon, LayersIcon, MailIcon, PrinterIcon } from "@repo/rdx-ui/lib/icons"; +import type { ComponentProps } from "react"; +import { useTranslation } from "react-i18next"; + +import type { + EnvironmentRow, + ProfileConfigurationKind, +} from "../../../work-environments/shared/helpers"; + +import { WorkstationQueryFeedback } from "./workstation-query-feedback"; + +const FAMILY_ICONS = { + mail: MailIcon, + folders: FolderIcon, + applications: LayersIcon, + printers: PrinterIcon, + certificates: FileKeyIcon, +}; +const FAMILY_PATHS = { + mail: "mail-configurations", + folders: "folder-mappings", + applications: "application-configurations", + printers: "printer-configurations", + certificates: "certificate-configurations", +}; + +export function WorkstationConfigurationFamily({ + kind, + rows, + query, + companyId, + profileId, + resolved, +}: { + kind: ProfileConfigurationKind; + rows: readonly EnvironmentRow[]; + query: ComponentProps["query"]; + companyId: string; + profileId: string; + resolved: boolean; +}) { + const { t } = useTranslation("companies"); + const Icon = FAMILY_ICONS[kind]; + + return ( +
+
+

+

+ + {t("environment.manage")} + +
+ + {query.data !== undefined && resolved && ( +
    + {rows.map((row) => ( +
  • +
    +

    {row.name}

    + + {t(`mail.${row.state}`)} + +
    +
    + {row.details.map((detail) => ( +
    +
    {t(detail.label)}:
    +
    {detail.translated ? t(detail.value) : detail.value}
    +
    + ))} +
    + {row.review && ( +

    {t("environment.needsReview")}

    + )} + {row.notes && ( +

    {row.notes}

    + )} +
  • + ))} +
+ )} + {query.data !== undefined && resolved && !query.isError && !rows.length && ( +

{t("environment.empty")}

+ )} +
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-dashboard-skeleton.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-dashboard-skeleton.tsx new file mode 100644 index 0000000..c4e5ba4 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-dashboard-skeleton.tsx @@ -0,0 +1,44 @@ +import { Skeleton } from "@repo/shadcn-ui/components/skeleton"; +import { useTranslation } from "react-i18next"; + +export function WorkstationDashboardSkeleton() { + const { t } = useTranslation("assets"); + + return ( +
+ {t("loading")} + + + +
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx index e47147a..d572004 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-detail-content.tsx @@ -1,129 +1,147 @@ -import { WorkstationHistory } from "@metis/assets/client"; -import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; -import { ScrollableTabsList } from "@repo/rdx-ui/components/scrollable-tabs-list"; -import { ClockIcon } from "@repo/rdx-ui/lib/icons"; -import { Tabs, TabsContent } from "@repo/shadcn-ui/components/tabs"; +import { entityListState } from "@repo/rdx-ui/lib/entity-list-state"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import type { CompanyDTO } from "../../../../common"; -import { SoftwareInstallationsPanel } from "../../../software-installations/list/ui/software-installations-panel"; +import { buildEnvironment } from "../../../work-environments/shared/helpers"; import type { WorkstationDetailController } from "../controllers/use-workstation-detail-controller"; +import { workstationPendingCount } from "../helpers/workstation-dashboard"; import { WorkstationAssets } from "./workstation-assets"; -import { WorkstationPeople } from "./workstation-people"; +import { WorkstationComputer } from "./workstation-computer"; +import { WorkstationLatestChange } from "./workstation-latest-change"; +import { WorkstationPendingConfigurations } from "./workstation-pending-configurations"; +import { WorkstationProfileConfigurations } from "./workstation-profile-configurations"; import { WorkstationProfiles } from "./workstation-profiles"; +import { WorkstationQueryFeedback } from "./workstation-query-feedback"; +import { WorkstationResponsible } from "./workstation-responsible"; +import { WorkstationSoftware } from "./workstation-software"; +import { WorkstationSupport } from "./workstation-support"; export function WorkstationDetailContent({ company, controller: c, + onHistory, }: { company: CompanyDTO; controller: WorkstationDetailController; + onHistory: () => void; }) { - const { t, i18n } = useTranslation("assets"); - const [tab, setTab] = useState("information"); - const [softwareOpened, setSoftwareOpened] = useState(false); + const { t } = useTranslation("assets"); + const [pendingOnly, setPendingOnly] = useState(false); const workstation = c.workstation; + const model = c.dashboard; - if (!workstation) { + if (!(workstation && model)) { return null; } - - const computer = c.inventory.data?.assets.find( - (asset) => - asset.id === workstation.values.computerAssetId && - workstation.values.assetIds.includes(asset.id) - ); - const canCreateProfile = Boolean( - workstation.values.active && computer && computer.values.status !== "retired" - ); + const configurations = + c.pendingCount === undefined + ? undefined + : new Map( + model.profiles.map((profile) => [ + profile.id, + [...new Set(buildEnvironment(profile, c.sources).rows.map((row) => row.kind))], + ]) + ); return ( - { - if (typeof value === "string") { - setTab(value); - if (value === "software") { - setSoftwareOpened(true); - } - } - }} - value={tab} - > - - -
- c.setEditing(true)} - workstation={workstation} - /> - +
+ c.setEditing(true)} + /> +
+ { + const profile = model.activeProfiles.find( + (item) => workstationPendingCount([item], c.sources) > 0 + ); + + if (profile) { + c.setSelectedProfileId(profile.id); + } + setPendingOnly(true); + const section = document.getElementById("workstation-configurations"); + + section?.focus(); + section?.scrollIntoView({ block: "start" }); + }} />
- - c.setCreatingProfile(true)} - onRetry={() => { - c.profiles.refetch().catch(() => undefined); - }} - people={company.people} - profiles={c.relatedProfiles} - /> - - - {softwareOpened && ( - +
+
+ {model.computer && ( + + )} + c.profiles.refetch({ throwOnError: false })} + people={company.people} + profiles={model.profiles} + state={entityListState( + { + available: !model.computer || c.profiles.data !== undefined, + fetching: Boolean(model.computer && c.profiles.isFetching), + failed: Boolean(model.computer && c.profiles.isError), + }, + { + loading: t("loading"), + unavailable: t("workstationDetail.profilesError"), + refreshing: t("workstationDashboard.refreshing"), + stale: t("workstationDashboard.stale", { + section: t("workstationDetail.profiles"), + }), + } + )} + {...(configurations ? { configurations } : {})} + onCreate={() => c.setCreatingProfile(true)} /> - )} - - - } - title={t("workstationHistory")} - > - - ids - .map( - (id) => - c.inventory.data?.assets.find((asset) => asset.id === id)?.values.name ?? "—" - ) - .join(", ") || "—" - } - personName={(id) => company.people.find((person) => person.id === id)?.name ?? "—"} - siteName={(id) => company.sites.find((site) => site.id === id)?.name ?? "—"} - workstation={workstation} +
+ + c.setEditing(true)} + workstation={workstation} + /> + +
+ - - - +
+ + +
+
); } diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx index d44d6ee..bc16ad6 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx @@ -1,20 +1,32 @@ import { WorkstationActions, WorkstationEditorDialog, + WorkstationHistoryDialog, WorkstationNameEditor, } from "@metis/assets/client"; import { BrowserLink, navigateTo } from "@metis/core/client"; import { EntityStatus } from "@repo/rdx-ui/components/entity-list"; -import { ChevronLeftIcon, MapPinIcon, MonitorIcon, PersonIcon } from "@repo/rdx-ui/lib/icons"; +import { Building2Icon, ClockIcon, MapPinIcon, PersonIcon } from "@repo/rdx-ui/lib/icons"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@repo/shadcn-ui/components/breadcrumb"; import { Button } from "@repo/shadcn-ui/components/button"; import type { ComponentProps } from "react"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import type { CompanyDTO } from "../../../../common"; import { WorkProfileEditorDialog } from "../../../work-profiles/shared"; import { useWorkstationDetailController } from "../controllers/use-workstation-detail-controller"; +import { WorkstationDashboardSkeleton } from "./workstation-dashboard-skeleton"; import { WorkstationDetailContent } from "./workstation-detail-content"; +import { WorkstationQueryFeedback } from "./workstation-query-feedback"; export function WorkstationDetailPage({ company, @@ -27,103 +39,136 @@ export function WorkstationDetailPage({ }) { const { t } = useTranslation("assets"); const c = useWorkstationDetailController(company, workstationId); - const { - creatingProfile, - setCreatingProfile, - inventory, - profiles, - workstation, - editing, - setEditing, - } = c; + const historyReturn = useRef(null); + const { workstation, inventory, dashboard } = c; const root = `/companies/${company.id}`; - const responsible = company.people.find((person) => person.id === workstation?.values.personId); + const responsible = dashboard?.responsible; + + const onHistory = () => { + historyReturn.current = + document.activeElement instanceof HTMLElement ? document.activeElement : null; + c.setViewingHistory(true); + }; return ( -
- - - {inventory.isPending &&

{t("loading")}

} - {inventory.isError && ( -
-

{t("workstationDetail.loadError")}

- -
+
+ + + + }> + {t("workstationDashboard.companies")} + + + + + }> + {t("workstations")} + + + + + + {workstation?.values.name ?? t("workstationDashboard.detail")} + + + + + {inventory.isPending ? ( + + ) : ( + )} {inventory.isSuccess && !workstation && (

{t("workstationDetail.notFound")}

)} {workstation && ( <> -
-
-
- - -
- -
- - {t(workstation.values.active ? "active" : "inactive")} - - - - {responsible && ( - - - )} -
-
+
+
+
+
{ - profiles.refetch().catch(() => undefined); - }} + onDecommissioned={() => c.profiles.refetch({ throwOnError: false })} onDeleted={() => navigateTo(`${root}/workstations`, { replace: true })} - onEdit={() => setEditing(true)} + onEdit={() => c.setEditing(true)} workstation={workstation} - /> + > + + +
+
+ + {t(workstation.values.active ? "active" : "inactive")} + + + + + + +
- - {creatingProfile && ( + + {c.viewingHistory && ( + historyReturn.current ?? true} + members={(ids) => + ids + .map( + (id) => + inventory.data?.assets.find((asset) => asset.id === id)?.values.name ?? "—" + ) + .join(", ") || "—" + } + onClose={() => c.setViewingHistory(false)} + personName={(id) => company.people.find((person) => person.id === id)?.name ?? "—"} + siteName={(id) => company.sites.find((site) => site.id === id)?.name ?? "—"} + workstation={workstation} + /> + )} + {c.creatingProfile && ( setCreatingProfile(false)} + onClose={() => c.setCreatingProfile(false)} profile={undefined} workstationId={workstation.id} /> )} - {editing && ( + {c.editing && ( setEditing(false)} + onClose={() => c.setEditing(false)} workstation={workstation} /> )} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx index 4dc3c51..80a517c 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx @@ -29,7 +29,6 @@ import { companiesEnglish, companiesSpanish } from "../../../shared"; import { profileKeys } from "../../../work-profiles/shared/api"; import { WorkstationDetailPage } from "./workstation-detail-page"; -import { WorkstationPeople } from "./workstation-people"; import { WorkstationProfiles } from "./workstation-profiles"; const companyId = "0199b200-0000-7000-8000-000000000001"; @@ -148,23 +147,18 @@ test("ficha de puesto es/en: cabecera, retorno, bloques vacíos y edición modal assert.equal(header.includes(companies.profiles.new), false); assert.equal(header.includes(labels.workstationDetail.subtitle), false); assert.equal(header.includes(labels.workstationDetail.profileUnavailable), false); - assert.ok(html.includes(labels.workstationDetail.profiles)); - assert.ok(html.includes(labels.workstationDetail.people)); + assert.ok(html.includes(labels.workstationDashboard.peopleProfiles)); assert.ok(html.includes(companies.profiles.new)); assert.equal(html.includes("]*>[\s\S]*?<\/button>/g)] .map(([button]) => button) .filter((button) => button.includes('role="tab"')); - assert.equal(tabs.length, 3); - assert.ok(tabs.some((button) => button.includes(labels.history))); - assert.equal( - tabs.some((button) => button.includes(labels.workstationDetail.profiles)), - false - ); - assert.ok(html.includes(labels.workstationDetail.profilesHint)); - assert.ok(html.includes(labels.workstationDetail.software)); + assert.equal(tabs.length, 0); + assert.ok(html.includes(labels.workstationDashboard.viewHistory)); + assert.ok(html.includes(labels.workstationDashboard.software)); + assert.ok(html.includes(labels.workstationDashboard.responsibleContext)); assert.ok(html.includes(labels.noWorkstationAssets)); assert.ok(html.includes(labels.workstationDetail.profileUnavailable)); assert.match( @@ -377,14 +371,15 @@ test("ficha: ordenador destacado y periféricos sin repetir, perfiles solo del o assert.ok(html.includes(`href="/companies/${companyId}/assets/${computer.id}"`)); assert.ok(html.includes(`href="/companies/${companyId}/assets/${peripheral.id}"`)); assert.ok(html.indexOf(computer.values.name) < html.indexOf(peripheral.values.name)); - assert.equal((html.match(/data-slot="card" data-size="sm"/g) ?? []).length, 2); - assert.ok(html.includes("ThinkCentre")); - assert.ok(html.includes(labels.workstationDetail.peripherals)); + assert.ok(html.includes(labels.workstationDashboard.equipment)); assert.ok(html.includes(labels.workstationDetail.assignedPerson)); assert.ok(html.includes(`href="/companies/${companyId}/work-profiles/${profiles[0]?.id}"`)); - assert.ok(html.includes(`href="/companies/${companyId}/work-profiles/${profiles[1]?.id}"`)); + assert.equal( + html.includes(`href="/companies/${companyId}/work-profiles/${profiles[1]?.id}"`), + false + ); + assert.ok(html.includes("Perfil 51")); assert.equal(html.includes("Perfil 52"), false); - assert.ok(html.includes(labels.workstationDetail.needsReview)); assert.equal(html.includes(labels.workstationDetail.profileUnavailable), false); } }); @@ -394,12 +389,14 @@ test("personas y perfiles: carga y error no se presentan como listados vacíos", const labels = language === "es" ? assetsSpanish : assetsEnglish; const people = await render(language, true, { node: ( - undefined} + onRetry={() => Promise.resolve()} people={[]} - responsibleId={null} + profiles={[]} + state={{ status: "loading", message: labels.loading }} /> ), }); @@ -408,17 +405,20 @@ test("personas y perfiles: carga y error no se presentan como listados vacíos", undefined} - onRetry={() => undefined} + onRetry={() => Promise.resolve()} people={[]} profiles={[]} + state={{ + status: "unavailable", + message: labels.workstationDetail.profilesError, + retrying: false, + }} /> ), }); - assert.ok(people.includes('role="status"')); + assert.ok(people.includes('aria-busy="true"')); assert.equal(people.includes(labels.workstationDetail.noPeople), false); assert.ok(profiles.includes('role="alert"')); assert.ok(profiles.includes(labels.retry)); diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-latest-change.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-latest-change.tsx new file mode 100644 index 0000000..a00b4ad --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-latest-change.tsx @@ -0,0 +1,73 @@ +import type { AssetDTO, WorkstationDTO } from "@metis/assets/common"; +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; +import { ClockIcon } from "@repo/rdx-ui/lib/icons"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { useTranslation } from "react-i18next"; + +import { workstationRevisionChanges } from "../helpers/workstation-dashboard"; + +export function WorkstationLatestChange({ + workstation, + assets, + onHistory, +}: { + workstation: WorkstationDTO; + assets: readonly AssetDTO[]; + onHistory: () => void; +}) { + const { t, i18n } = useTranslation("assets"); + const change = workstationRevisionChanges(workstation.history); + + const name = (id: string) => + assets.find((asset) => asset.id === id && asset.companyId === workstation.companyId)?.values + .name ?? "—"; + + return ( + + {t("workstationDashboard.fullHistory")} + + } + density="compact" + icon={} + title={t("workstationDashboard.latestChange")} + > + {change.latest ? ( +
+ +

+ {t("recordedBy", { name: change.latest.actorName })} +

+
    + {!change.previous &&
  • {t("workstationDetail.created")}
  • } + {change.fields.map((field) => ( +
  • {t(`workstationDashboard.changed.${field}`)}
  • + ))} + {change.added.length > 0 && ( +
  • + {t("workstationDashboard.membersAdded", { + names: change.added.map(name).join(", "), + })} +
  • + )} + {change.removed.length > 0 && ( +
  • + {t("workstationDashboard.membersRemoved", { + names: change.removed.map(name).join(", "), + })} +
  • + )} +
+
+ ) : ( +

{t("workstationDetail.noHistory")}

+ )} +
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-pending-configurations.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-pending-configurations.tsx new file mode 100644 index 0000000..621a8d5 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-pending-configurations.tsx @@ -0,0 +1,75 @@ +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; +import { SettingsIcon } from "@repo/rdx-ui/lib/icons"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { Skeleton } from "@repo/shadcn-ui/components/skeleton"; +import { useTranslation } from "react-i18next"; + +import type { WorkstationDetailController } from "../controllers/use-workstation-detail-controller"; + +import { WorkstationQueryFeedback } from "./workstation-query-feedback"; + +export function WorkstationPendingConfigurations({ + controller: c, + onPending, +}: { + controller: WorkstationDetailController; + onPending: () => void; +}) { + const { t } = useTranslation("assets"); + const { t: companies } = useTranslation("companies"); + const q = c.configurationQueries; + const failed = [ + { query: c.profiles, label: t("workstationDetail.profiles") }, + ...(["mail", "folders", "applications", "printers", "certificates"] as const).map((kind) => ({ + query: q[kind], + label: companies(`${kind}.title`), + })), + ].filter((item) => item.query.isError); + const updating = + c.dashboard?.computer && + [c.profiles, q.mail, q.folders, q.applications, q.printers, q.certificates].some( + (query) => query.isFetching + ); + + return ( + } + title={t("workstationDashboard.pending")} + > + {c.pendingCount === undefined ? ( + <> + {failed.length ? ( +

+ {t("workstationDashboard.totalUnknown")} +

+ ) : ( + + )} + {failed.map(({ query, label }) => ( + + ))} + + ) : ( + <> +

0 ? "text-amber-700 dark:text-amber-400" : "text-foreground"}`} + data-pending-count={c.pendingCount} + > + {c.pendingCount} +

+ {c.pendingCount > 0 && ( + + )} + + )} + {updating && ( +

+ {t("workstationDashboard.refreshing")} +

+ )} +
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-people.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-people.tsx deleted file mode 100644 index d6ddefd..0000000 --- a/modules/companies/src/web/workstations/detail/ui/workstation-people.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { BrowserLink } from "@metis/core/client"; -import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; -import { PersonIcon } from "@repo/rdx-ui/lib/icons"; -import { useTranslation } from "react-i18next"; - -import type { PersonDTO } from "../../../../common"; -import { PersonAvatar } from "../../../people/shared"; - -export function WorkstationPeople({ - companyId, - people, - responsibleId, - loading, - failed, -}: { - companyId: string; - people: readonly PersonDTO[]; - responsibleId: string | null | undefined; - loading: boolean; - failed: boolean; -}) { - const { t, i18n } = useTranslation("assets"); - const ordered = [...people].sort( - (a, b) => - Number(b.id === responsibleId) - Number(a.id === responsibleId) || - a.name.localeCompare(b.name, i18n.language) - ); - - return ( - } - title={t("workstationDetail.people")} - > - {loading && ( -

- {t("loading")} -

- )} - {failed && ( -

- {t("workstationDetail.profilesError")} -

- )} - {ordered.length > 0 && ( -
    - {ordered.map((person) => ( -
  • - -
    - - {person.name} - -

    - {t( - person.id === responsibleId - ? "workstationDetail.assignedPerson" - : "workstationDetail.profilePerson" - )} -

    -
    -
  • - ))} -
- )} - {!(people.length || loading || failed) && ( -

{t("workstationDetail.noPeople")}

- )} -
- ); -} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-profile-configurations.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-profile-configurations.tsx new file mode 100644 index 0000000..d0663f0 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-profile-configurations.tsx @@ -0,0 +1,122 @@ +import { BrowserLink } from "@metis/core/client"; +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; +import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; +import { SettingsIcon } from "@repo/rdx-ui/lib/icons"; +import { Checkbox } from "@repo/shadcn-ui/components/checkbox"; +import { useId } from "react"; +import { useTranslation } from "react-i18next"; + +import { buildEnvironment } from "../../../work-environments/shared/helpers"; +import type { WorkstationDetailController } from "../controllers/use-workstation-detail-controller"; + +import { WorkstationConfigurationFamily } from "./workstation-configuration-family"; +import { WorkstationQueryFeedback } from "./workstation-query-feedback"; + +export function WorkstationProfileConfigurations({ + controller: c, + companyId, + pendingOnly, + onPendingOnly, +}: { + controller: WorkstationDetailController; + companyId: string; + pendingOnly: boolean; + onPendingOnly: (value: boolean) => void; +}) { + const { t } = useTranslation("assets"); + const { t: companies } = useTranslation("companies"); + const id = useId(); + const profile = c.selectedProfile; + const model = profile ? buildEnvironment(profile, c.sources) : undefined; + const rows = model?.rows.filter((row) => !pendingOnly || row.state === "pending") ?? []; + const queries = c.configurationQueries; + + return ( +
+ } + title={t("workstationDashboard.configurations")} + > + {c.dashboard?.computer && ( + + )} + {profile ? ( + <> + { + if (value) { + c.setSelectedProfileId(value); + } + }} + options={(c.dashboard?.activeProfiles ?? []).map((item) => ({ + value: item.id, + label: item.values.name, + }))} + placeholder={t("workstationDashboard.selectProfile")} + value={profile.id} + /> + {c.selectedProfileId && c.selectedProfileId !== profile.id && ( +

+ {t("workstationDashboard.selectionChanged")} +

+ )} +
+ + {t("workstationDashboard.openProfile")} + + +
+ + +
+ {(["folders", "mail", "applications", "printers", "certificates"] as const).map( + (kind) => ( + row.kind === kind)} + /> + ) + )} +
+ {model && model.unlinkedMail > 0 && ( +

+ {companies("environment.unlinkedMail", { count: model.unlinkedMail })} +

+ )} + + ) : ( + !(c.dashboard?.computer && (c.profiles.isPending || c.profiles.isError)) && ( +

+ {t("workstationDashboard.noActiveProfiles")} +

+ ) + )} +
+
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx index c379bd8..92365c0 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-profiles.tsx @@ -1,13 +1,17 @@ import { BrowserLink } from "@metis/core/client"; import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; import { EntityStatus, EntityTable } from "@repo/rdx-ui/components/entity-list"; +import type { EntityListState } from "@repo/rdx-ui/lib/entity-list-state"; import { TriangleAlertIcon, WorkProfileIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import type { PersonDTO } from "../../../../common"; +import type { AccessItemDTO } from "../../../../common/dto/shared/access.dto"; import type { WorkProfileDTO } from "../../../../common/work-profiles"; import { PersonAvatar } from "../../../people/shared"; +import type { ProfileConfigurationKind } from "../../../work-environments/shared/helpers"; export function WorkstationProfiles({ companyId, @@ -15,51 +19,73 @@ export function WorkstationProfiles({ onCreate, profiles, people, - loading, - failed, onRetry, + accounts = [], + configurations, + state, }: { companyId: string; canCreate: boolean; onCreate: () => void; profiles: readonly WorkProfileDTO[]; people: readonly PersonDTO[]; - loading: boolean; - failed: boolean; - onRetry: () => void; + onRetry: () => Promise; + accounts?: readonly AccessItemDTO[]; + configurations?: ReadonlyMap; + state: EntityListState; }) { const { t, i18n } = useTranslation("assets"); const { t: companies } = useTranslation("companies"); + const [archived, setArchived] = useState(false); + const archivedProfiles = profiles.filter((profile) => !profile.values.active); + const visible = profiles.filter((profile) => archived || profile.values.active); + return ( - ); diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-query-feedback.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-query-feedback.tsx new file mode 100644 index 0000000..6703f87 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-query-feedback.tsx @@ -0,0 +1,53 @@ +import { Button } from "@repo/shadcn-ui/components/button"; +import { Skeleton } from "@repo/shadcn-ui/components/skeleton"; +import { useTranslation } from "react-i18next"; + +export function WorkstationQueryFeedback({ + query, + label, +}: { + query: { + data: unknown; + isPending: boolean; + isFetching: boolean; + isError: boolean; + refetch: (options?: { throwOnError?: boolean }) => Promise; + }; + label: string; +}) { + const { t } = useTranslation("assets"); + + if (query.isError) { + return ( +
+

+ {t(query.data ? "workstationDashboard.stale" : "workstationDashboard.loadError", { + section: label, + })} +

+ +
+ ); + } + if (query.isPending) { + return ( +
+ {t("loading")} + + +
+ ); + } + + return query.isFetching ? ( +

+ {t("workstationDashboard.refreshing")} +

+ ) : null; +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-responsible.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-responsible.tsx new file mode 100644 index 0000000..b7fd830 --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-responsible.tsx @@ -0,0 +1,60 @@ +import { BrowserLink } from "@metis/core/client"; +import { DetailField } from "@repo/rdx-ui/components/detail-field"; +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; +import { PersonIcon } from "@repo/rdx-ui/lib/icons"; +import { useTranslation } from "react-i18next"; + +import type { CompanyDTO } from "../../../../common"; +import { workstationOrganization } from "../helpers/workstation-dashboard"; + +export function WorkstationResponsible({ + company, + personId, + siteId, +}: { + company: CompanyDTO; + personId: string | null; + siteId: string | null; +}) { + const { t } = useTranslation("assets"); + const root = `/companies/${company.id}`; + const responsible = company.people.find((person) => person.id === personId); + const site = company.sites.find((item) => item.id === siteId); + const { department, position } = workstationOrganization( + company, + personId, + new Date().toISOString().slice(0, 10) + ); + + return ( + } + title={t("workstationDashboard.responsibleContext")} + > +
+ + {responsible ? ( + {responsible.name} + ) : ( + "—" + )} + + + {department ? ( + {department.name} + ) : ( + "—" + )} + + {position ?? "—"} + + {site ? {site.name} : "—"} + + + {company.name} + +
+
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-software-dialog.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-software-dialog.tsx new file mode 100644 index 0000000..437933b --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-software-dialog.tsx @@ -0,0 +1,37 @@ +import { FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog"; +import { useTranslation } from "react-i18next"; + +import { SoftwareInstallationsPanel } from "../../../software-installations/list/ui/software-installations-panel"; + +export function WorkstationSoftwareDialog({ + companyId, + assetId, + canCreate, + onClose, +}: { + companyId: string; + assetId: string; + canCreate: boolean; + onClose: () => void; +}) { + const { t } = useTranslation("assets"); + + return ( + { + if (!open) { + onClose(); + } + }} + open + size="large" + title={t("workstationDashboard.software")} + > + + + + + ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-software.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-software.tsx new file mode 100644 index 0000000..89c5fef --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-software.tsx @@ -0,0 +1,117 @@ +import { BrowserLink, useListQuery } from "@metis/core/client"; +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; +import { EntityStatus } from "@repo/rdx-ui/components/entity-list"; +import { LayersIcon } from "@repo/rdx-ui/lib/icons"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { softwareApi, softwareKeys } from "../../../software-installations/shared/api/software-api"; +import type { WorkstationDetailController } from "../controllers/use-workstation-detail-controller"; + +import { WorkstationQueryFeedback } from "./workstation-query-feedback"; +import { WorkstationSoftwareDialog } from "./workstation-software-dialog"; + +export function WorkstationSoftware({ + controller: c, + companyId, +}: { + controller: WorkstationDetailController; + companyId: string; +}) { + const { t } = useTranslation("assets"); + const { t: companies } = useTranslation("companies"); + const [managing, setManaging] = useState(false); + const computer = c.dashboard?.computer; + const query = useListQuery({ + queryKey: softwareKeys.company(companyId), + queryFn: () => softwareApi.get(companyId), + enabled: Boolean(computer), + }); + const resources = c.configurationQueries.resources; + const items = + query.data?.configurations.filter( + (item) => + item.companyId === companyId && + item.values.assetId === computer?.id && + !["removed", "never_configured"].includes(item.values.state) + ) ?? []; + + const resource = (id: string) => + resources.data?.resources.find((item) => item.id === id && item.companyId === companyId); + + return ( + setManaging(true)} size="sm" variant="outline"> + {t("workstationDashboard.manageSoftware")} + + ) + } + density="compact" + icon={} + title={t("workstationDashboard.software")} + > + {computer ? ( + <> + + + {query.data && ( +
    5 ? 0 : undefined} + > + {items.map((item) => ( +
  • + + {resource(item.values.resourceId)?.values.name ?? "—"} + +
    + + {companies("applications.version")}: {item.values.version || "—"} + + + {companies(`mail.${item.values.state}`)} + +
    + {item.values.licenseResourceId && ( + + {companies("software.licenseColumn")}:{" "} + {resource(item.values.licenseResourceId)?.values.name ?? "—"} + + )} + {item.needsReview && ( +

    + {companies("software.review")} +

    + )} +
  • + ))} +
+ )} + {query.data && !query.isError && !items.length && ( +

{companies("software.empty")}

+ )} + {managing && ( + setManaging(false)} + /> + )} + + ) : ( +

{companies("software.noComputer")}

+ )} +
+ ); +} diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-support.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-support.tsx new file mode 100644 index 0000000..2e36dba --- /dev/null +++ b/modules/companies/src/web/workstations/detail/ui/workstation-support.tsx @@ -0,0 +1,124 @@ +import { AssetSupport } from "@metis/assets/client"; +import { BrowserLink } from "@metis/core/client"; +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; +import { HeadphonesIcon } from "@repo/rdx-ui/lib/icons"; +import { useTranslation } from "react-i18next"; + +import type { CompanyDTO } from "../../../../common"; +import { RemoteSummary } from "../../../remote-connections"; +import { TeleworkAuthorizationSummary } from "../../../telework-authorizations"; +import type { WorkstationDetailController } from "../controllers/use-workstation-detail-controller"; + +import { WorkstationQueryFeedback } from "./workstation-query-feedback"; + +export function WorkstationSupport({ + controller: c, + company, +}: { + controller: WorkstationDetailController; + company: CompanyDTO; +}) { + const { t } = useTranslation("assets"); + const { t: companies } = useTranslation("companies"); + const computer = c.dashboard?.computer; + const personId = c.selectedProfile?.values.personId; + const person = company.people.find((item) => item.id === personId); + const { remote, telework } = c.configurationQueries; + const connections = + remote.data?.connections.filter( + (item) => + item.companyId === company.id && + item.values.personId === personId && + !["removed", "never_configured"].includes(item.values.state) && + (item.values.sourceAssetId === computer?.id || + (item.values.targetKind === "asset" && item.values.targetId === computer?.id)) + ) ?? []; + const authorization = telework.data?.authorizations.find( + (item) => item.companyId === company.id && item.values.personId === personId + ); + + return ( + } + title={t("workstationDashboard.support")} + > + {computer ? ( + <> +
+

{t("workstationDashboard.recordedSupport")}

+

+ {t("workstationDashboard.documentedOnly")} +

+ +
+
+

{t("workstationDashboard.remoteContext")}

+ {person ? ( + <> + + {person.name} + +

+ {t("workstationDashboard.remoteScope")} +

+ + {connections.map((connection) => ( + + ))} + {remote.data && !remote.isError && !connections.length && ( +

{companies("remote.empty")}

+ )} + + {companies("remote.manage")} + + + {telework.data && ( + + )} + + {companies("telework.manage")} + + + ) : ( +

+ {t("workstationDashboard.noActiveProfiles")} +

+ )} +
+ + ) : ( +

+ {t("workstationDashboard.supportNeedsComputer")} +

+ )} +
+ ); +} diff --git a/packages/rdx-ui/src/components/detail-panel.tsx b/packages/rdx-ui/src/components/detail-panel.tsx index a65f356..9bab1c5 100644 --- a/packages/rdx-ui/src/components/detail-panel.tsx +++ b/packages/rdx-ui/src/components/detail-panel.tsx @@ -10,6 +10,7 @@ export function DetailPanel({ count, actions, children, + density = "default", }: { title: string; description?: string; @@ -17,13 +18,18 @@ export function DetailPanel({ count?: number | undefined; actions?: ReactNode; children: ReactNode; + density?: "default" | "compact"; }) { const id = useId(); return (
- - + +

{actions &&
{actions}
} - {children} + + {children} +

); diff --git a/scripts/workstation-dashboard-fixtures.py b/scripts/workstation-dashboard-fixtures.py new file mode 100644 index 0000000..f39b80d --- /dev/null +++ b/scripts/workstation-dashboard-fixtures.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Prepara escenarios de puestos solo en la instalación desechable de visual-audit.py.""" +import json +import os +from pathlib import Path +import runpy +import tomllib +from urllib.parse import urlparse + +ROOT = Path(__file__).resolve().parent.parent +AUDIT = runpy.run_path(str(ROOT / "scripts/visual-audit.py")) + + +def main(): + state = (ROOT / os.environ["METIS_PLAYWRIGHT_STORAGE_STATE"]).resolve() + if not state.is_relative_to(ROOT / ".artifacts/playwright") or state.stat().st_mode & 0o077: + raise RuntimeError("Requiere la sesión privada del preparador sintético.") + report = json.loads((state.parent / "report.json").read_text()) + if urlparse(report["origin"]).hostname != "localhost" or report.get("ordinaryUser") is not True: + raise RuntimeError("Requiere instalación local sintética con usuario ordinario.") + config = tomllib.loads((ROOT / ".codex/config.toml").read_text())["mcp_servers"]["playwright"] + code = r'''async (page) => { + const origin=__ORIGIN__, companyId=__COMPANY__; + const session=await page.request.get(origin+'/api/auth/session'); + if(session.status()!==200)throw Error('Sesión no disponible'); + const {csrfToken}=await session.json(); + async function get(path){const r=await page.request.get(origin+'/api'+path);if(r.status()!==200)throw Error('GET '+path+': '+r.status());return r.json()} + async function post(path,data){const r=await page.request.post(origin+'/api'+path,{data,headers:{origin,'x-csrf-token':csrfToken}});if(![200,201].includes(r.status())){const j=await r.json();throw Error('POST '+path+': '+r.status()+' '+j.code)}return r.json()} + const base='/companies/'+companyId; + const details=await get(base);let company=details.company??details; + if(company.name!=='Auditoría visual — Datos sintéticos')throw Error('Compañía no sintética'); + const catalog=(await get('/asset-catalog/'+companyId)).items; + const resourceCatalog=(await get('/resource-catalog/'+companyId)).items; + const inventory=await get('/assets/'+companyId); + const resources=(await get('/resources/'+companyId)).resources; + const profileRecords=(await get('/work-profiles/'+companyId)).profiles; + function template(name){const t=catalog.find(x=>x.values.kind==='template'&&x.values.name===name);if(!t)throw Error('Plantilla ausente: '+name);return t.id} + async function refreshCompany(){const d=await get(base);company=d.company??d;return company} + async function person(name){let p=company.people.find(x=>x.name===name);if(!p){await post(base+'/people',{name,position:null,email:null,phone:null,active:true,isContact:false});await refreshCompany();p=company.people.find(x=>x.name===name)}return p} + const angela=await person('Ángela Muñoz de la Peña'), inigo=await person('Íñigo Fernández del Río'); + const siteName='Sede de pruebas — área administrativa'; + if(!company.sites.some(x=>x.name===siteName)){await post(base+'/sites',{name:siteName,active:true,address:null,phone:null});await refreshCompany()} + const site=company.sites.find(x=>x.name===siteName); + const deptName='Administración y gestión documental'; + if(!company.departments.some(x=>x.name===deptName)){await post(base+'/departments',{name:deptName,active:true});await refreshCompany()} + const department=company.departments.find(x=>x.name===deptName); + if(!company.assignments.some(x=>x.personId===angela.id&&x.departmentId===department.id)){await post(base+'/people/'+angela.id+'/assignments',{departmentId:department.id,position:'Responsable de documentación',startDate:'2026-01-01',endDate:null,isPrimary:true});await refreshCompany()} + async function asset(name,kind='Portátil'){let a=inventory.assets.find(x=>x.values.name===name);if(!a){a=(await post('/assets/'+companyId,{values:{name,templateId:template(kind),siteId:site.id,status:'operational',notes:'Fixture sintética del dashboard.',characteristics:{}}})).asset;inventory.assets.push(a)}return a} + const host=await asset('PORT-ADM-01 · Documentación y gestión'); + const monitor=await asset('MON-ADM-01 · Monitor panorámico','Monitor'); + const printer=await asset('IMP-RED-01 · Impresora de planta compartida','Impresora'); + const lab=await asset('PC-LAB-01 · Equipo del laboratorio'); + async function resource(key,name,characteristics={}){let r=resources.find(x=>x.values.name===name);if(!r){const t=resourceCatalog.find(x=>x.builtinKey==='template:'+key);if(!t)throw Error('Plantilla recurso ausente: '+key);r=(await post('/resources/'+companyId,{values:{name,templateId:t.id,active:true,notes:'Fixture sintética del dashboard.',characteristics}})).resource;resources.push(r)}return r} + function field(key){const f=resourceCatalog.find(x=>x.builtinKey==='field:'+key);if(!f)throw Error('Campo ausente: '+key);return f.id} + const app=await resource('application','Gestor documental de ejemplo'); + const os=await resource('os','Sistema operativo de ejemplo'); + const license=await resource('software_license','Licencia documental de ejemplo'); + const certificate=await resource('certificate','Certificado de pruebas de documentación',{[field('expires')]:'2027-12-31'}); + const mailbox=await resource('mailbox','Buzón de Ángela',{[field('email')]:'angela@example.invalid',[field('context')]:'Correo de pruebas'}); + const unc=String.raw`\\srv-doc.example.invalid\documentacion\administracion`; + const share=await resource('shared_folder','Documentación administrativa compartida',{[field('unc')]:unc}); + const tool=await resource('support','Herramienta de soporte de ejemplo'); + function same(actual,expected){return Object.entries(expected).every(([key,value])=>Array.isArray(value)?Array.isArray(actual[key])&&value.length===actual[key].length&&value.every(x=>actual[key].includes(x)):key==='driveLetter'?(actual[key]??'').replace(/:$/,'')===(value??'').replace(/:$/,''):actual[key]===value)} + async function access(values){const before=(await get('/access/'+companyId)).items;const existing=before.find(x=>x.values.kind===values.kind&&(values.name?x.values.name===values.name:values.login?x.values.login===values.login:same(x.values,values)));if(existing)return existing;const saved=await post('/access/'+companyId,{values});return saved.items.find(x=>!before.some(y=>y.id===x.id))} + const system=await access({kind:'system',name:'Identidad corporativa de pruebas',active:true}); + const account=await access({kind:'account',systemId:system.id,environmentId:null,login:'angela.pruebas',usage:'personal',active:true,expiresOn:null,notes:null}); + const period={startDate:'2026-01-01',endDate:null}; + const accountPerson=await access({kind:'account_person',accountId:account.id,personId:angela.id,...period,configuration:'configured'}); + const type=share.values.integration.resourceTypeId; + const permission=await access({kind:'permission',name:'Lectura documental de pruebas',active:true,resourceTypeId:type,description:null}); + const need=await access({kind:'need',personId:angela.id,resourceId:share.id,permissionIds:[permission.id],...period,notes:null}); + const grant=await access({kind:'grant',resourceId:share.id,permissionId:permission.id,accountId:account.id,groupId:null,...period,configuration:'configured'}); + await access({kind:'coverage',needId:need.id,grantId:grant.id,accountPersonId:accountPerson.id,membershipId:null,...period}); + async function station(name,computer,personId,ids){let s=inventory.workstations.find(x=>x.values.name===name);if(!s){s=(await post('/assets/'+companyId+'/workstations',{values:{name,active:true,siteId:site.id,personId,computerAssetId:computer?.id??null,assetIds:ids}})).workstation;inventory.workstations.push(s)}return s} + let main=inventory.workstations.find(x=>x.values.name==='Administración — documentación y atención interna'); + if(!main){const first=await station('Administración — puesto inicial',host,angela.id,[host.id]);main=(await post('/assets/'+companyId+'/workstations/'+first.id+'/update',{values:{...first.values,name:'Administración — documentación y atención interna',assetIds:[host.id,monitor.id]}})).workstation} + const withoutResponsible=await station('Laboratorio — equipo compartido sin responsable',lab,null,[lab.id]); + const empty=await station('Puesto pendiente de equipamiento',null,null,[]); + const retired=await station('Recepción — puesto desmantelado',null,angela.id,[]); + if(retired.values.active)await post('/assets/'+companyId+'/workstations/'+retired.id+'/decommission',{}); + async function profile(name,personId,assetId,active=true,accountId=null){let p=profileRecords.find(x=>x.values.name===name);if(!p){p=(await post('/work-profiles/'+companyId,{values:{name,personId,assetId,active,accountId,notes:null}})).profile;profileRecords.push(p)}return p} + const primary=await profile('Ángela · administración',angela.id,host.id,true,account.id); + const secondary=await profile('Íñigo · consultas de documentación',inigo.id,host.id); + await profile('Ángela · perfil archivado del mismo ordenador',angela.id,host.id,false); + await profile('Ángela · laboratorio',angela.id,lab.id); + async function configuration(path,values,collection='configurations'){const items=(await get('/'+path+'/'+companyId))[collection];if(!items.some(x=>same(x.values,values)))await post('/'+path+'/'+companyId,{values})} + await configuration('software-installations',{assetId:host.id,resourceId:app.id,licenseResourceId:license.id,version:'4.2',state:'configured',notes:null}); + await configuration('software-installations',{assetId:host.id,resourceId:os.id,licenseResourceId:null,version:null,state:'configured',notes:null}); + const mailboxUses=(await get('/mailboxes/'+companyId)).uses; + if(!mailboxUses.some(x=>x.values.personId===angela.id&&x.values.mailboxId===mailbox.id))await post('/mailboxes/'+companyId+'/uses',{values:{mailboxId:mailbox.id,personId:angela.id,...period}}); + await configuration('folder-mappings',{profileId:primary.id,needId:need.id,accessAccountId:account.id,path:unc,driveLetter:'Z',state:'configured',notes:null},'mappings'); + await configuration('mail-configurations',{profileId:primary.id,mailboxId:mailbox.id,method:'imap',state:'configured',notes:null}); + await configuration('application-configurations',{profileId:primary.id,resourceId:app.id,delivery:'installed',version:'4.2',state:'configured',notes:null}); + await configuration('printer-configurations',{profileId:primary.id,printerId:printer.id,connection:'network',isDefault:true,state:'configured',notes:null}); + await configuration('certificate-configurations',{profileId:primary.id,resourceId:certificate.id,location:null,state:'pending',notes:'Instalación pendiente de completar.'}); + await configuration('telework-authorizations',{personId:angela.id,active:true,startsOn:'2026-01-01',expiresOn:'2027-08-31',notes:null},'authorizations'); + await configuration('remote-connections',{personId:angela.id,sourceKind:'company_asset',sourceAssetId:host.id,targetKind:'network',targetId:null,siteId:site.id,method:'VPN corporativa documentada',requiresVpn:true,state:'configured',notes:null},'connections'); + const support=await get('/asset-support/'+companyId+'/'+host.id); + if(!support.connections.length)await post('/asset-support/'+companyId+'/'+host.id,{values:{resourceId:tool.id,destination:'equipo-adm-01.example.invalid',requiresVpn:false,preferred:true,state:'available',notes:''}}); + const denseAssets=[]; + for(let i=0;i<80;i++){denseAssets.push(await asset('DASH-DENSE-'+String(i).padStart(2,'0')+(i%10===0?' — Equipo de documentación e ingeniería con nombre extenso':'')))} + const dense=await station('Puesto de pruebas — alta densidad',denseAssets[0],null,denseAssets.map(x=>x.id)); + let extra=0; + while(company.people.filter(x=>x.active).length<52){await person('DASH-PERSON-ACTIVE-'+String(extra++).padStart(2,'0'))} + const activePeople=company.people.filter(x=>x.active).slice(0,50); + for(let i=0;ix.id!==companyId); + return {companyId,origin,main:main.id,withoutResponsible:withoutResponsible.id,empty:empty.id,retired:retired.id,dense:dense.id,primaryProfile:primary.id,secondaryProfile:secondary.id,host:host.id,lab:lab.id,otherCompany:other.id,counts:{companies:(await get('/companies')).companies.length,people:company.people.length,assets:(await get('/assets/'+companyId)).assets.length,resources:(await get('/resources/'+companyId)).resources.length,denseMembers:dense.values.assetIds.length,denseProfiles:60}}; + }'''.replace("__ORIGIN__", json.dumps(report["origin"])).replace("__COMPANY__", json.dumps(report["companyId"])) + with AUDIT["browser"](config, state) as client: + result = AUDIT["result_json"](AUDIT["tool"](client, "browser_run_code_unsafe", {"code": code})) + (state.parent / "workstation-fixtures.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n") + print(json.dumps(result, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/scripts/workstation-dashboard-ui.test.py b/scripts/workstation-dashboard-ui.test.py new file mode 100644 index 0000000..e2f40ea --- /dev/null +++ b/scripts/workstation-dashboard-ui.test.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +"""Pruebas MCP del dashboard real sobre fixtures HTTP de la instalación desechable.""" +import contextlib +import json +import os +from pathlib import Path +import runpy +import tomllib +import unittest +from urllib.parse import urlparse + +ROOT = Path(__file__).resolve().parent.parent +AUDIT = runpy.run_path(str(ROOT / "scripts/visual-audit.py")) + + +class WorkstationDashboardTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + value = os.environ.get("METIS_PLAYWRIGHT_STORAGE_STATE") + if not value: + raise unittest.SkipTest("Requiere visual-audit.py y workstation-dashboard-fixtures.py.") + state = (ROOT / value).resolve() + if not state.is_relative_to(ROOT / ".artifacts/playwright") or state.stat().st_mode & 0o077: + raise RuntimeError("La sesión debe ser privada y pertenecer al preparador.") + report = json.loads((state.parent / "report.json").read_text()) + if urlparse(report["origin"]).hostname != "localhost" or report.get("ordinaryUser") is not True: + raise RuntimeError("Requiere instalación local sintética de pruebas.") + cls.fixture = json.loads((state.parent / "workstation-fixtures.json").read_text()) + if cls.fixture['origin'] != report['origin'] or cls.fixture['companyId'] != report['companyId']: + raise RuntimeError("Los escenarios no pertenecen a la instalación desechable activa.") + cls.origin = cls.fixture["origin"] + cls.base = f"/companies/{cls.fixture['companyId']}" + cls.output = state.parent / "workstation-dashboard" + cls.output.mkdir(exist_ok=True) + cls.evidence = json.loads((cls.output / "measurements.json").read_text()) if (cls.output / "measurements.json").exists() else [] + cls.stack = contextlib.ExitStack() + cls.addClassCleanup(cls.stack.close) + config = tomllib.loads((ROOT / ".codex/config.toml").read_text())["mcp_servers"]["playwright"] + cls.client = cls.stack.enter_context(AUDIT["browser"](config, state)) + cls.code("page.__workstationErrors=[];page.on('pageerror',error=>page.__workstationErrors.push(error.message));return true;") + + @classmethod + def tearDownClass(cls): + if hasattr(cls, 'client'): + errors=cls.code("return page.__workstationErrors;") + cls.record('uncaught-page-errors',errors) + if errors: + raise AssertionError('Errores JavaScript de página: '+str(errors)) + + @classmethod + def tool(cls, name, args): + return AUDIT["tool"](cls.client, name, args) + + @classmethod + def code(cls, body): + return AUDIT["result_json"](cls.tool("browser_run_code_unsafe", {"code": "async (page) => {" + body + "}"})) + + @classmethod + def record(cls, name, values): + cls.evidence = [entry for entry in cls.evidence if entry["scenario"] != name] + cls.evidence.append({"scenario": name, "values": values}) + (cls.output / "measurements.json").write_text(json.dumps(cls.evidence, ensure_ascii=False, indent=2) + "\n") + + def navigate(self, key="main", pending=True): + url = self.origin + self.base + "/workstations/" + self.fixture[key] + self.tool("browser_navigate", {"url": url}) + self.code("await page.locator('main h1').waitFor();return true;") + if pending: + self.code("await page.locator('[data-pending-count]').waitFor();return true;") + self.settle() + + def settle(self): + self.code("await page.evaluate(async()=>{await document.fonts.ready;await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))});return true;") + + def capture(self, name): + self.record(name, AUDIT["capture"](self.client, self.output, name)) + + def test_01_responsive_and_density(self): + for width, height in [(1600,1000),(1440,900),(1280,900),(1024,768),(768,1024),(390,844)]: + self.tool("browser_resize", {"width":width,"height":height}) + self.navigate() + self.code("await page.getByRole('link',{name:'Gestor documental de ejemplo',exact:true}).first().waitFor();return true;") + values = self.code("""return await page.evaluate(()=>({width:innerWidth,documentWidth:document.documentElement.scrollWidth,pending:document.querySelector('[data-pending-count]').textContent,headings:[...document.querySelectorAll('main h2')].map(x=>x.textContent.trim())}));""") + self.record(f"geometry-{width}", values) + self.assertLessEqual(values["documentWidth"], width) + self.assertEqual(values["pending"], "1") + self.assertNotIn("Activos asociados", values["headings"]) + self.assertNotIn("Perfiles activos", values["headings"]) + self.capture(f"S01-{width}") + if width == 390: + self.code("await page.locator('#workstation-configurations').getByRole('heading',{name:'Configuraciones por perfil',exact:true}).scrollIntoViewIfNeeded();return true;") + self.capture('S01-configurations-390') + self.tool("browser_resize", {"width":1440,"height":900}) + self.navigate("dense") + self.code("await page.getByRole('link',{name:/DASH-PROFILE/}).first().waitFor();return true;") + self.capture("S04-density-1440") + self.assertEqual(self.code("return await page.locator('main').evaluate(e=>e.scrollWidth<=e.clientWidth);"), True) + + def test_02_profiles_refetch_and_search(self): + self.tool("browser_resize", {"width":1440,"height":900}) + self.navigate() + self.code("await page.getByRole('button',{name:'1 perfil archivado',exact:true}).click();await page.getByRole('link',{name:'Ángela · perfil archivado del mismo ordenador',exact:true}).waitFor();return true;") + self.code("await page.getByRole('button',{name:'Ocultar archivados',exact:true}).click();return true;") + self.code("await page.getByRole('combobox',{name:'Seleccionar perfil',exact:true}).click();await page.getByRole('option',{name:'Íñigo · consultas de documentación',exact:true}).click();return true;") + self.assertFalse(self.code("return await page.locator('#workstation-configurations').getByText('Instalación pendiente de completar.',{exact:true}).isVisible();")) + value = self.code("""const selector=page.getByRole('combobox',{name:'Seleccionar perfil',exact:true});await selector.focus();await page.context().setOffline(true);const response=page.waitForResponse(r=>r.url().includes('/api/work-profiles/')&&r.status()===200);await page.context().setOffline(false);await response;return {selection:await selector.textContent(),focused:await selector.evaluate(e=>e===document.activeElement)};""") + self.assertIn("Íñigo", value["selection"]) + self.assertTrue(value["focused"]) + self.record("S07-refetch", value) + self.code("const region=page.getByRole('region',{name:'Personas y perfiles',exact:true});await region.locator('input[data-quick-search]').fill('SIN-COINCIDENCIAS-XYZ');return true;") + self.assertFalse(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();")) + self.capture("S08-no-results") + + def test_03_history_edit_software_and_navigation(self): + self.navigate() + self.code("await page.getByRole('button',{name:'Ver historial',exact:true}).click();await page.getByRole('dialog').waitFor();return true;") + self.assertTrue(self.code("return await page.getByRole('dialog').getByText('Administración — puesto inicial',{exact:true}).isVisible();")) + self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") + self.assertEqual(self.code("return await page.evaluate(()=>document.activeElement?.textContent.trim());"),"Ver historial") + self.code("await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();await page.getByRole('dialog').waitFor();return true;") + self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") + self.code("await page.getByRole('button',{name:'Gestionar software',exact:true}).click();await page.getByRole('dialog').waitFor();return true;") + self.code("await page.getByRole('dialog').getByRole('button',{name:'Añadir instalación',exact:true}).waitFor();return true;") + self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") + self.assertEqual(self.code("return await page.evaluate(()=>document.activeElement?.textContent.trim());"),"Gestionar software") + target = self.base + "/assets/" + self.fixture["host"] + self.code("await page.locator('a[href="+json.dumps(target)+"]').first().click();await page.locator('main h1').waitFor();return true;") + self.assertIn(target,self.code("return page.url();")) + self.navigate() + self.code("await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click();return true;") + self.assertTrue(self.code("return await page.locator('#workstation-configurations').evaluate(e=>e===document.activeElement);")) + self.assertTrue(self.code("return await page.getByRole('checkbox',{name:'Solo pendientes',exact:true}).isChecked();")) + self.capture("S01-configurations") + + def test_04_empty_and_dismantled(self): + for key, name in [("withoutResponsible","S02"),("empty","S03"),("retired","S06")]: + self.navigate(key) + self.capture(name) + if key == "withoutResponsible": + self.assertTrue(self.code("return (await page.locator('main').innerText()).includes('Sin responsable');")) + self.assertTrue(self.code("return await page.getByRole('link',{name:'Ángela · laboratorio',exact:true}).isVisible();")) + else: + self.assertTrue(self.code("return await page.getByRole('button',{name:'Nuevo perfil',exact:true}).isDisabled();")) + self.assertFalse(self.code("return await page.getByRole('button',{name:'Gestionar software',exact:true}).isVisible();")) + + def test_05_partial_errors_and_retry(self): + company = self.fixture["companyId"] + self.code("await page.route('**/api/work-profiles/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));await page.route('**/api/folder-mappings/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;") + try: + self.navigate(pending=False) + self.code("await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;") + self.assertFalse(self.code("return await page.locator('[data-pending-count]').isVisible();")) + self.assertTrue(self.code("return await page.getByRole('heading',{name:'Equipamiento',exact:true}).isVisible();")) + self.code("await page.getByRole('link',{name:'Gestor documental de ejemplo',exact:true}).first().waitFor();return true;") + self.capture("S05-profiles-and-folders-error") + finally: + self.code("await page.unroute('**/api/work-profiles/"+company+"');await page.unroute('**/api/folder-mappings/"+company+"');return true;") + self.code("await page.getByRole('button',{name:'Reintentar',exact:true}).first().click();return true;") + self.navigate() + self.code("await page.route('**/api/folder-mappings/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;") + try: + self.navigate(pending=False) + self.code("await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;") + self.assertTrue(self.code("return await page.getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();")) + self.assertTrue(self.code("return await page.locator('#workstation-configurations').getByText('Instalación pendiente de completar.',{exact:true}).isVisible();")) + self.capture("S05-folders-only-error") + finally: + self.code("await page.unroute('**/api/folder-mappings/"+company+"');return true;") + + def test_06_missing_and_authentication(self): + self.tool("browser_navigate", {"url":self.origin+self.base+"/workstations/019a0a00-0000-7000-8000-000000000999"}) + self.code("await page.getByText('Este puesto de trabajo no existe en esta compañía.',{exact:true}).waitFor();return true;") + status = self.code("const context=await page.context().browser().newContext();try{const response=await context.request.get("+json.dumps(self.origin+"/api/assets/"+self.fixture['companyId'])+");return response.status()}finally{await context.close()}") + self.assertEqual(status,401) + self.record("S09-authentication", {"unauthenticatedStatus":status}) + + + def api(self, path, body=None): + return self.code("""const path=__PATH__,body=__BODY__;const origin=__ORIGIN__; + const session=await page.request.get(origin+'/api/auth/session');const {csrfToken}=await session.json(); + const response=body===null?await page.request.get(origin+'/api'+path):await page.request.post(origin+'/api'+path,{data:body,headers:{origin,'x-csrf-token':csrfToken}}); + if(!response.ok())throw Error('Fixture HTTP '+response.status());return response.json(); + """.replace("__PATH__", json.dumps(path)).replace("__BODY__",json.dumps(body)).replace("__ORIGIN__",json.dumps(self.origin))) + + def test_07_replace_main_computer(self): + company = self.fixture["companyId"] + inventory = self.api('/assets/'+company) + original = next(x for x in inventory['workstations'] if x['id']==self.fixture['main']) + replacement = next((x for x in inventory['assets'] if x['values']['name']=='DASH-S10-SWAP'),None) + if replacement is None: + catalog = self.api('/asset-catalog/'+company) + template = next(x for x in catalog['items'] if x['values'].get('name')=='Portátil' and x['values']['kind']=='template') + replacement = self.api('/assets/'+company,{'values':{'name':'DASH-S10-SWAP','templateId':template['id'],'siteId':None,'status':'operational','notes':'Fixture S10 desechable.','characteristics':{}}})['asset'] + values = dict(original['values'],computerAssetId=replacement['id'],assetIds=[replacement['id']]) + self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':values}) + try: + self.navigate() + self.code("await page.getByRole('link',{name:'DASH-S10-SWAP',exact:true}).first().waitFor();return true;") + self.assertFalse(self.code("return await page.getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();")) + self.assertEqual(self.code("return await page.locator('[data-pending-count]').textContent();"),'0') + self.capture('S10-replaced-computer') + finally: + self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']}) + + def test_08_unknown_documentary_coverage(self): + company = self.fixture['companyId'] + mapping = next(x for x in self.api('/folder-mappings/'+company)['mappings'] if x['values']['profileId']==self.fixture['primaryProfile']) + updated = self.api('/folder-mappings/'+company+'/'+mapping['id']+'/update',{'values':dict(mapping['values'],accessAccountId=None),'expectedRevisionId':mapping['history'][-1]['id']})['mapping'] + try: + self.assertEqual(updated['accessAccountCoverage'],'unknown') + self.navigate() + self.code("await page.locator('#workstation-configurations').scrollIntoViewIfNeeded();return true;") + label=json.loads((ROOT/'modules/companies/src/common/locales/es.json').read_text())['companies']['folders']['accountCoverage']['unknown'] + self.code("await page.locator('#workstation-configurations').getByText("+json.dumps(label)+",{exact:false}).waitFor();return true;") + self.assertEqual(self.code("return await page.locator('[data-pending-count]').textContent();"),'1') + self.capture('S11-unknown-coverage') + finally: + self.api('/folder-mappings/'+company+'/'+mapping['id']+'/update',{'values':mapping['values'],'expectedRevisionId':updated['history'][-1]['id']}) + + def test_09_stale_profiles_and_retry(self): + self.navigate() + company=self.fixture['companyId'] + self.code("await page.route('**/api/work-profiles/"+company+"',route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:'INTERNAL_ERROR'})}));return true;") + try: + self.code("await page.context().setOffline(true);await page.context().setOffline(false);await page.getByText('No se puede confirmar el total de pendientes.',{exact:true}).waitFor();return true;") + self.assertTrue(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('link',{name:'Ángela · administración',exact:true}).isVisible();")) + self.capture('S07-stale-profiles') + finally: + self.code("await page.unroute('**/api/work-profiles/"+company+"');return true;") + self.code("await page.getByRole('button',{name:'Reintentar',exact:true}).first().click();await page.locator('[data-pending-count=\"1\"]').waitFor();return true;") + + + def test_10_keyboard_contrast_and_pagination(self): + self.tool('browser_resize',{'width':1440,'height':900}) + self.navigate() + self.code("await page.getByRole('button',{name:'Ver historial',exact:true}).focus();await page.keyboard.press('Enter');await page.getByRole('dialog').waitFor();await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") + self.assertTrue(self.code("return await page.getByRole('button',{name:'Ver historial',exact:true}).evaluate(e=>e.matches(':focus-visible'));")) + self.code("await page.getByRole('combobox',{name:'Seleccionar perfil',exact:true}).focus();await page.keyboard.press('ArrowDown');await page.getByRole('option').first().waitFor();await page.keyboard.press('End');await page.keyboard.press('Enter');return true;") + self.assertIn('Íñigo',self.code("return await page.getByRole('combobox',{name:'Seleccionar perfil',exact:true}).textContent();")) + ratios=self.code("""return await page.evaluate(()=>{ + const rgb=color=>{const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const context=canvas.getContext('2d');context.fillStyle=color;context.fillRect(0,0,1,1);return [...context.getImageData(0,0,1,1).data].slice(0,3)}; + const lum=c=>c.map(v=>v/255).map(v=>v<=0.04045?v/12.92:((v+0.055)/1.055)**2.4).reduce((s,v,i)=>s+v*[0.2126,0.7152,0.0722][i],0); + return [...document.querySelectorAll('main header button,main a,[data-pending-count]')].filter(e=>e.getBoundingClientRect().width>0).map(e=>{const colors=[];for(let n=e;n;n=n.parentElement){colors.unshift(getComputedStyle(n).backgroundColor)}const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const context=canvas.getContext('2d');context.fillStyle='#fff';context.fillRect(0,0,1,1);for(const color of colors){context.fillStyle=color;context.fillRect(0,0,1,1)}const bg=[...context.getImageData(0,0,1,1).data].slice(0,3);const fg=rgb(getComputedStyle(e).color);const values=[lum(bg),lum(fg)].sort((a,b)=>a-b);return {label:e.textContent.trim()||e.getAttribute('aria-label'),ratio:(values[1]+0.05)/(values[0]+0.05)}}); + });""") + self.record('text-contrast',ratios) + self.assertTrue(all(x['ratio']>=4.5 for x in ratios),ratios) + self.navigate('dense') + self.code("const region=page.getByRole('region',{name:'Personas y perfiles',exact:true});await region.getByRole('button',{name:'Página siguiente',exact:true}).click();return true;") + self.assertTrue(self.code("return await page.getByRole('region',{name:'Personas y perfiles',exact:true}).getByRole('button',{name:'Página 2',exact:true}).evaluate(e=>e.getAttribute('aria-current')==='page');")) + self.record('S04-pagination',{'profilePage':2}) + + + def test_11_save_editor_and_rename(self): + company=self.fixture['companyId'] + original=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==self.fixture['main']) + renamed='Administración — prueba de edición del dashboard' + self.navigate() + try: + self.code("await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();const dialog=page.getByRole('dialog');await dialog.getByRole('textbox',{name:'Nombre',exact:true}).fill("+json.dumps(renamed)+");await dialog.getByRole('button',{name:'Guardar',exact:true}).click();await dialog.waitFor({state:'hidden'});await page.getByRole('heading',{level:1,name:"+json.dumps(renamed)+",exact:true}).waitFor();return true;") + saved=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==original['id']) + self.assertEqual(len(saved['history']),len(original['history'])+1) + label=json.loads((ROOT/'modules/assets/src/common/locales/es.json').read_text())['workstationDetail']['rename'] + self.code("await page.getByRole('button',{name:"+json.dumps(label)+",exact:true}).click();const field=page.getByRole('textbox',{name:'Nombre',exact:true});await field.fill("+json.dumps(original['values']['name'])+");await field.press('Enter');await page.getByRole('heading',{level:1,name:"+json.dumps(original['values']['name'])+",exact:true}).waitFor();return true;") + restored=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==original['id']) + self.assertEqual(restored['values'],original['values']) + self.assertEqual(len(restored['history']),len(original['history'])+2) + self.record('editor-save-and-inline-rename',{'saved':True,'revisionDelta':2}) + finally: + current=next(x for x in self.api('/assets/'+company)['workstations'] if x['id']==original['id']) + if current['values'] != original['values']: + self.api('/assets/'+company+'/workstations/'+original['id']+'/update',{'values':original['values']}) + + +if __name__ == "__main__": + unittest.main(verbosity=2)