From 68980d9131fe409e7b16c8164290c8dd4c1284e3 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 16 Oct 2026 20:09:02 +0200 Subject: [PATCH] feat: enhance user management and resource catalog UI with icons, improve localization, and add new components - Added UsersIcon to UserManagement for better visual representation. - Updated Spanish localization for "noValue" to use a dash instead of "Sin datos". - Introduced LayoutTemplateIcon in ResourceCatalogPage for improved UI. - Integrated LayersIcon in ResourcesPage to enhance user experience. - Updated package.json test scripts to include new test files. - Enhanced DetailSection component to support accordion functionality. - Modified EntityListHeader to accept an icon prop for better UI consistency. - Added new CountBadge and TimeLine components for better data representation. - Implemented asset workstation action logic and UI components for asset management. - Created tests for CountBadge and TimeLine components to ensure functionality. --- .../modules/assets/src/common/locales/en.json | 4 +- .../modules/assets/src/common/locales/es.json | 4 +- docs/componentes-de-interfaz.md | 142 ++++++- docs/patron-de-listados-de-entidades.md | 19 + docs/puestos-de-trabajo.md | 61 ++- modules/assets/package.json | 2 +- modules/assets/src/common/locales/en.json | 16 +- modules/assets/src/common/locales/es.json | 16 +- .../src/web/assets/detail/ui/asset-detail.tsx | 69 ++-- .../detail/ui/asset-selection-panel.tsx | 52 ++- .../assets/list/asset-workstation.test.tsx | 378 ++++++++++++++++++ .../use-inventory-page-controller.ts | 3 + .../list/helpers/asset-workstation-action.ts | 38 ++ .../list/ui/asset-workstation-action.tsx | 53 +++ .../src/web/assets/list/ui/inventory-page.tsx | 40 +- .../assets/select/asset-selection.test.tsx | 47 +++ .../src/web/assets/shared/inventory.test.tsx | 67 +++- .../src/web/assets/shared/ui/asset-list.tsx | 62 ++- .../shared/ui/asset-template-indicator.tsx | 34 ++ .../src/web/brands/list/ui/brands-page.tsx | 5 +- .../src/web/catalog/list/ui/catalog-page.tsx | 3 + modules/assets/src/web/index.ts | 1 + .../history/ui/workstation-history-dialog.tsx | 45 +-- .../history/ui/workstation-history.tsx | 78 ++++ .../use-workstations-page-controller.ts | 8 +- .../list/types/workstations-page-props.ts | 2 + .../list/ui/workstations-page.tsx | 39 +- .../use-workstation-form-controller.ts | 22 +- .../shared/types/workstation-form-props.ts | 4 +- .../shared/ui/workstation-editor-dialog.tsx | 13 +- modules/companies/src/common/locales/en.json | 4 + modules/companies/src/common/locales/es.json | 4 + .../web/access/list/ui/access-workspace.tsx | 3 + .../ui/application-configurations-page.tsx | 3 + .../ui/certificate-configurations-page.tsx | 3 + .../use-company-detail-controller.ts | 37 +- .../companies/detail/ui/company-detail.tsx | 49 ++- .../companies/detail/ui/company-inventory.tsx | 4 +- .../detail/ui/company-overview-statistics.tsx | 4 +- .../companies/detail/ui/company-section.tsx | 10 +- .../web/companies/list/ui/companies-page.tsx | 7 +- .../shared/helpers/dashboard-kind-icons.ts | 6 +- .../view/ui/dashboard-attention-panel.tsx | 4 +- .../list/ui/department-section.tsx | 9 +- .../list/ui/folder-mappings-page.tsx | 3 + .../list/ui/mail-configurations-page.tsx | 3 + .../web/people/detail/ui/person-details.tsx | 2 + .../src/web/people/list/ui/people-section.tsx | 98 ++++- .../people/shared/ui/person-editor-dialog.tsx | 4 +- .../list/ui/printer-configurations-page.tsx | 3 + .../list/ui/process-templates-page.tsx | 3 + .../web/processes/list/ui/processes-page.tsx | 4 +- .../list/ui/remote-connections-page.tsx | 3 + .../src/web/sites/list/ui/site-section.tsx | 9 +- .../list/ui/software-installations-panel.tsx | 3 + .../list/ui/telework-authorizations-page.tsx | 3 + .../list/ui/work-profiles-page.tsx | 3 + .../shared/ui/work-profile-editor-dialog.tsx | 4 +- .../ui/work-profile-selection-field.tsx | 4 +- .../shared/ui/work-profile-summary.tsx | 4 +- .../detail/ui/workstation-detail-content.tsx | 46 ++- .../detail/ui/workstation-detail-page.tsx | 4 +- .../detail/ui/workstation-detail.test.tsx | 218 +++++++++- .../detail/ui/workstation-people.tsx | 4 +- .../detail/ui/workstation-profiles.tsx | 5 +- .../src/web/users/list/ui/user-management.tsx | 7 +- modules/resources/src/common/locales/en.json | 4 +- modules/resources/src/common/locales/es.json | 4 +- .../src/web/catalog/list/ui/catalog-page.tsx | 3 + .../web/resources/list/ui/resources-page.tsx | 3 + packages/rdx-ui/package.json | 2 +- .../rdx-ui/src/components/count-badge.tsx | 31 ++ .../rdx-ui/src/components/detail-section.tsx | 36 +- .../rdx-ui/src/components/entity-list.tsx | 14 +- .../src/components/entity-table-content.tsx | 6 + .../rdx-ui/src/components/field-sections.tsx | 29 +- packages/rdx-ui/src/components/time-line.tsx | 55 +++ packages/rdx-ui/src/count-badge.test.tsx | 50 +++ packages/rdx-ui/src/lib/icons.ts | 2 + packages/rdx-ui/src/time-line.test.tsx | 51 +++ 80 files changed, 1970 insertions(+), 231 deletions(-) create mode 100644 modules/assets/src/web/assets/list/asset-workstation.test.tsx create mode 100644 modules/assets/src/web/assets/list/helpers/asset-workstation-action.ts create mode 100644 modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx create mode 100644 modules/assets/src/web/assets/shared/ui/asset-template-indicator.tsx create mode 100644 modules/assets/src/web/workstations/history/ui/workstation-history.tsx create mode 100644 packages/rdx-ui/src/components/count-badge.tsx create mode 100644 packages/rdx-ui/src/components/time-line.tsx create mode 100644 packages/rdx-ui/src/count-badge.test.tsx create mode 100644 packages/rdx-ui/src/time-line.test.tsx diff --git a/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/en.json b/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/en.json index d10cc39..b639791 100644 --- a/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/en.json +++ b/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/en.json @@ -108,7 +108,7 @@ "retired": "Retired", "noAssets": "This company has no assets yet.", "noAssignment": "Unassigned", - "noValue": "Not specified", + "noValue": "—", "characteristics": "Characteristics", "history": "History", "recordedBy": "Recorded by {{name}}", @@ -413,4 +413,4 @@ "clear": "Clear selection", "required": "Required" } -} +} \ No newline at end of file diff --git a/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/es.json b/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/es.json index 8fdf3bf..df7cdf2 100644 --- a/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/es.json +++ b/archive/tickets-and-interventions/integration/modules/assets/src/common/locales/es.json @@ -108,7 +108,7 @@ "retired": "Retirado", "noAssets": "Esta compañía todavía no tiene activos.", "noAssignment": "Sin asignar", - "noValue": "Sin indicar", + "noValue": "—", "characteristics": "Características", "history": "Historial", "recordedBy": "Registrado por {{name}}", @@ -413,4 +413,4 @@ "clear": "Quitar selección", "required": "Obligatorio" } -} +} \ No newline at end of file diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index 1853167..576406b 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -827,7 +827,7 @@ si el consumidor todavía no conoce el resultado. El contenido responde al ancho propio contenedor. No incorpora scroll, acciones ni estado de apertura por su cuenta. La ficha del puesto reutiliza el panel en activos, personas y perfiles. Se organiza en -Resumen, Perfiles y Software mediante `Tabs` y `ScrollableTabsList`. El resumen destaca +Información, Software e Historial mediante `Tabs` y `ScrollableTabsList`. Información destaca el ordenador principal y presenta el resto como tarjetas de periféricos sin duplicarlo. Reutiliza `AssetSummaryCard`, que acepta contenido opcional para `name` y permite enlazar el nombre desde el consumidor sin depender de una librería de rutas. Las tarjetas @@ -837,7 +837,7 @@ accesible desde el nombre. Las listas usan `ul`/`li` y una o dos columnas según El panel lateral presenta las personas con avatar y su relación; el responsable aparece primero y cada persona se muestra una sola vez. Los perfiles pertenecen exclusivamente al ordenador principal, incluidos los archivados que ya se mostraban en la ficha. -Su pestaña reutiliza `EntityTable` con búsqueda por nombre o persona, ordenación, +Su panel dentro de Información reutiliza `EntityTable` con búsqueda por nombre o persona, ordenación, paginación, estados y avisos de revisión; omite repetir el mismo dispositivo por fila. La tabla conserva su estado al cambiar a otra pestaña. El software se monta al abrir su pestaña y permanece montado después, conservando sus filtros al cambiar de sección. @@ -872,3 +872,141 @@ vigentes y el nuevo nombre. No añade endpoints ni cambia las reglas del agregad historial. Si el nombre ya coincide, evita una escritura. La respuesta actualiza la caché de inventario y la invalidación posterior consulta el estado definitivo. Se mantiene el tratamiento habitual de CSRF y de errores de inventario. + + +## Iconos de Personas y Perfiles de usuario + +`PersonIcon`, del catálogo `@repo/rdx-ui/lib/icons`, utiliza `IconUsers` de Tabler, +la silueta de dos personas que antes identificaba los perfiles de usuario. Se reutiliza +en navegación, listados y altas de personas, indicadores, actividad, selección y resumen +de personas y responsables de puestos. + +`WorkProfileIcon` utiliza `IconIdBadge2`, una credencial con una silueta individual, +para representar Perfiles de usuario en navegación, listados, alta/edición, paneles del +puesto y actividad. Los controles que seleccionan una persona dentro del formulario +de perfil conservan `PersonIcon`, porque representan la relación con esa entidad. +`UsersIcon` mantiene sus consumidores de usuarios de la aplicación. Los estados +específicos, como persona inactiva, conservan su variante visual. Los iconos decorativos +no sustituyen el nombre accesible. + + +## Iconos en las cabeceras de listados + +`EntityListHeader` admite `icon` como contenido React opcional, independiente de +entidades o consultas. Lo sitúa junto al título `h2` en un área de 24 px, sin encogerse +y oculta para lectores de pantalla. La descripción sigue debajo y las acciones a la +derecha se adaptan al ancho disponible. + +Los listados de activos, recursos, compañías, personas, puestos, perfiles, usuarios, +sedes, departamentos, marcas, catálogos, accesos, procesos y configuraciones presentan +el mismo icono de entidad en el título y en su botón de alta, manteniendo textos, +permisos, estados deshabilitados y callbacks. El consumidor aporta ambos iconos desde +el catálogo compartido; no se infieren a partir de las traducciones ni se añaden a +acciones secundarias como ayuda o reintento. Véase el [patrón de listados](patron-de-listados-de-entidades.md). + + +## Línea temporal vertical + +`TimeLine`, en `@repo/rdx-ui/components/time-line`, presenta una lista ordenada accesible +con línea vertical, hitos y tarjetas adaptables. Recibe `label`, `emptyMessage` e `items`; +cada evento aporta `id`, `title`, `dateTime`, `dateLabel`, `meta` y `content` opcionales. +El consumidor decide el orden, traduce textos y formatea las fechas. `time` conserva +la fecha ISO y las líneas e hitos son decorativos. El componente no conoce entidades, +DTO, endpoints ni consultas. Un array vacío muestra el mensaje de estado aportado. + +`WorkstationHistory` compone la línea temporal con revisiones del puesto en orden +inverso al registro y presenta el alta y las actualizaciones con autor y valores de +cada revisión. Se comparte entre la pestaña Historial de la ficha y el diálogo del +listado. Información reúne activos, personas y el panel de perfiles; mantiene montada +su tabla para conservar filtros y paginación al cambiar de pestaña. + + +## Tipo de activo en la primera columna + +`AssetList` presenta una columna `icon` compacta, no ocultable ni ordenable, antes de +Nombre. `AssetTemplateIndicator` utiliza el icono actual de la plantilla y un tooltip +de shadcn/ui con su nombre registrado; el foco de teclado también permite consultarlo. +Si el catálogo o el icono no están disponibles, usa la caja como alternativa y conserva +el nombre de la instantánea. La columna Plantilla mantiene únicamente su texto. + +`EntityTable` reserva el identificador `icon` para una columna visual compacta con +cabecera solo accesible. En selectores, `selection` sigue siendo la primera columna: +selección, icono, nombre y demás datos. La nueva columna no altera la ordenación +inicial por nombre, la búsqueda, la selección de filas ni las acciones ancladas. + + +## Acciones de puesto desde un activo + +La columna Puesto del inventario compone `AssetWorkstationAction`, fuera del menú de +fila: muestra el nombre de cada puesto asociado como enlace a su ficha o inicia el +alta del puesto con un ordenador disponible +(portátil, sobremesa o servidor). `WorkstationEditorDialog` y `WorkstationForm` admiten +`initialAsset` para inicializar un alta y `onSaved` conserva la navegación al resultado. +La edición mantiene los valores del puesto y tiene prioridad sobre esta inicialización. +El formulario conserva sus cuatro pasos, validación, altura estable y ayuda anclada; +no se crea otro flujo de persistencia ni un formulario incrustado en el listado. + + +## Secciones desplegables en la ficha lateral del activo + +La ficha lateral utiliza `Accordion` de shadcn/ui (Base UI) con `multiple` y +`keepMounted`: se pueden abrir varias secciones a la vez y plegarlas conserva el +contenido y los estados internos de relaciones y asistencia. Todas las secciones +existentes empiezan abiertas. El acordeón se reinicia por UUID al seleccionar otro +activo, sin cambiar el desplazamiento ni las acciones del panel. + +`DetailSection.accordionValue` integra cada grupo en el acordeón padre con el mismo +título común y campos de consulta; sin esa propiedad mantiene su presentación actual +con `details`. `FieldSections` con `layout="rows"` y +`sectionPresentation="accordion"` aplica este criterio a los grupos históricos de +la plantilla y al grupo sin sección, sin duplicar campos ni mostrar grupos vacíos. +Las especificaciones antiguas sin grupos ofrecen un único desplegable Características. +Ubicación/asignación, observaciones, relaciones y asistencia también pertenecen al +mismo acordeón. El detalle completo conserva su presentación y su historial. + + +## Distribución de columnas del inventario + +En el inventario, tras el icono y Nombre, las columnas se ordenan como Plantilla, +Estado, Sede, Puesto de trabajo y Fecha de alta; Acciones permanece al final y anclada. +Plantilla y fecha usan texto secundario; estado y fecha evitan saltos internos, mientras +los nombres de puesto pueden partirse para ajustarse al ancho. Las fechas ausentes +siguen mostrando `—`. El enlace del puesto presenta su nombre real y «Crear puesto de +trabajo» permanece visible cuando corresponde. + + +Al guardar un activo desde el inventario, la selección del resultado reutiliza +`selectAsset`, igual que el clic de fila, conservando los parámetros `template` y `q` +de la URL. El cierre del editor no restablece la pestaña Todo ni elimina la búsqueda. + + +El alta de puesto desde Personas reutiliza el flujo de `CompanyInventory`, incluidas +las altas rápidas relacionadas. `initialPersonId` recorre la página y el diálogo hasta +los valores iniciales del formulario; solo preselecciona una persona activa de sus +lookups. Se aplica como valor inicial editable, sin efectos de sincronización que +puedan sobrescribir cambios manuales y sin modificar un puesto en edición. + + +La tabla de Personas añade «Puestos bajo su responsabilidad» con `ul`/`li` y enlaces +`BrowserLink` a las fichas de los puestos activos. Reutiliza `personWorkstations` y la +consulta compartida de inventario (`useListQuery`, clave por compañía), sin peticiones +por fila ni consultas de perfiles. La columna permite ordenar por los nombres de los +puestos, conserva `—` cuando no hay relación y muestra carga/error de inventario con +reintento junto a la tabla, sin ocultar los datos de las personas. + + +## Contadores en la navegación de compañía + +`CountBadge`, de `@repo/rdx-ui/components/count-badge`, reutiliza `Badge` de shadcn/ui +con ancho mínimo, cifra tabular y formato numérico según `locale`. Recibe `count`, +`loadingLabel`, `errorLabel`, `failed` y `className`. Conserva cero; un total desconocido +muestra `—`, con etiqueta accesible de carga o error. Si falla una actualización y hay +un total anterior, lo conserva con el aviso en el título. + +La navegación de compañía utiliza el contador para Personas, Departamentos, Sedes, +Activos, Recursos, Puestos de trabajo y Perfiles de usuario. Las cifras se alinean a +la derecha y la entrada activa mantiene los colores semánticos. Cuenta todos los +registros que presenta cada listado, incluidos los inactivos, sin aplicar sus filtros. +Los inventarios y perfiles usan `useListQuery` con las claves existentes por compañía: +comparten caché e invalidaciones de altas, cambios y bajas, sin nuevas rutas HTTP ni +estado remoto duplicado. Antes de cargar la compañía, esas consultas están deshabilitadas. diff --git a/docs/patron-de-listados-de-entidades.md b/docs/patron-de-listados-de-entidades.md index a5f0cec..715a317 100644 --- a/docs/patron-de-listados-de-entidades.md +++ b/docs/patron-de-listados-de-entidades.md @@ -12,6 +12,16 @@ respectivas. Los formularios y diálogos comunes de alta y edición pertenecen a La pantalla se divide en cabecera, contenido y pie opcional. La cabecera muestra título y subtítulo breve a la izquierda y acciones globales a la derecha. El alta es la acción primaria, normalmente «+ Añadir [entidad]», y abre un modal; se conservan las etiquetas actuales cuando ya describen una operación concreta. Nunca mostrar el formulario junto al listado. +El título y el botón de alta muestran el mismo icono de entidad antes del texto, tomado +del catálogo `@repo/rdx-ui/lib/icons` y coherente con la navegación. Activos utiliza +`BoxIcon`, Recursos `LayersIcon`, Personas `PersonIcon`, Perfiles de usuario +`WorkProfileIcon` y Puestos `MonitorIcon`. +`EntityListHeader.icon` presenta el icono del título a 24 px, sin encogerse; el botón +reutiliza el tamaño de icono de `Button`. Ambos son decorativos (`aria-hidden`), +conservando el texto y el nombre accesible. No sustituir el icono de entidad por un +signo «+» ni añadir ambos. Esta pauta se aplica también a catálogos, configuraciones +y sublistados con alta propia; las demás acciones conservan su representación. + El contenido contiene una tabla y cuatro bandas, en este orden: 1. Búsqueda rápida a la izquierda y filtros propios de la entidad a la derecha. El texto indica los campos buscables. Cuando hay criterios aplicados, Limpiar filtros restaura los valores iniciales de la pantalla; no cambia el ámbito de compañía ni el perfil desde el que se ha navegado. @@ -140,3 +150,12 @@ Verificación: 204 pruebas correctas, tipos y compilación correctos; los cinco Los formularios que seleccionan activos usan un diálogo con el mismo `AssetList` del inventario. Reutiliza `EntityTable`, búsqueda, pestañas por plantilla, columnas y paginación. La selección provisional no cambia el formulario hasta confirmar, ni altera la URL o filtros del listado padre. Cada consumidor restringe los candidatos y declara selección única o múltiple con límite. Véase [componentes de interfaz](componentes-de-interfaz.md#selección-de-activos-en-diálogo). Tickets, intervenciones y sus plantillas se conservan en [el archivo de funcionalidades](funcionalidades-archivadas.md), fuera del inventario activo. + + +### Columna visual del tipo de activo + +En `AssetList`, el icono de plantilla ocupa una columna `icon` compacta antes de Nombre, +con cabecera accesible y tooltip con el nombre de plantilla, disponible con puntero y +teclado. La columna Plantilla conserva solo el nombre. La columna visual no se ordena +ni se oculta y no sustituye la ordenación inicial por Nombre. En `PickerList`, la +selección permanece siempre por delante del icono. diff --git a/docs/puestos-de-trabajo.md b/docs/puestos-de-trabajo.md index cb64a45..508ca56 100644 --- a/docs/puestos-de-trabajo.md +++ b/docs/puestos-de-trabajo.md @@ -64,7 +64,7 @@ vigentes para no enviar sede, responsable o composición capturados al abrir el La respuesta confirmada actualiza la caché de inventario y después invalida esa consulta; no se invalidan perfiles ni software porque sus referencias no cambian. -La ficha separa Resumen, Perfiles y Software con pestañas accesibles y desplazables en +La ficha separa Información, Software e Historial con pestañas accesibles y desplazables en espacios estrechos. Bajo el nombre, la cabecera muestra estado, sede y responsable si existe, con enlaces a sede y persona. A la derecha aparece Editar puesto y un menú de acciones con Desactivar y Eliminar definitivamente, manteniendo sus confirmaciones. @@ -72,11 +72,12 @@ Nuevo perfil se sitúa en la cabecera del panel Perfiles y se habilita únicamen ordenador principal incluido en su composición y no retirado; el bloqueo explica cómo completar el puesto. -El resumen destaca el ordenador principal y muestra los demás activos como periféricos +Información reúne activos, personas y perfiles. Destaca el ordenador principal y muestra +los demás activos como periféricos mediante `AssetSummaryCard`, sin duplicados. Sus nombres enlazan las fichas de activos, que conservan el detalle técnico completo. Las personas se presentan con avatar, con el responsable primero y una única aparición por persona. La carga o el error de perfiles -no se confunde con ausencia de personas. La pestaña Perfiles reúne los del ordenador +no se confunde con ausencia de personas. El panel Perfiles en Información reúne los del ordenador principal, incluidos los archivados, mediante la tabla común con búsqueda, ordenación y paginación. Conserva los enlaces y avisos de revisión, sin repetir el mismo ordenador en cada fila. El software se carga al abrir su pestaña y mantiene la gestión existente. @@ -85,6 +86,17 @@ Los paneles de activos, personas y perfiles reutilizan `DetailPanel` de `@repo/r El diseño responde al ancho disponible y mantiene accesibles contenidos y acciones en móvil y escritorio. Véase [Componentes de interfaz](componentes-de-interfaz.md). + +La pestaña Historial muestra las revisiones registradas del puesto, de más reciente a +más antigua: fecha y hora, autor, nombre, estado, sede, responsable, ordenador principal +y activos de cada revisión. Los nombres de las referencias se resuelven con los datos +disponibles; las referencias ausentes muestran `—`. Las instantáneas mantienen sus +identificadores y valores originales; no se reconstruye un historial de cambios de +nombre de activos o sedes que el contrato no registra. `WorkstationHistory` comparte +la presentación entre la ficha y el diálogo del listado mediante `TimeLine` de +`@repo/rdx-ui/components/time-line`. No se añaden consultas ni nuevas escrituras. + + ## Desactivación y eliminación El listado y la ficha completa ofrecen **Desactivar** y **Eliminar definitivamente**, mediante un mismo diálogo reutilizable con confirmación y resultado de servidor. @@ -123,3 +135,46 @@ el diálogo grande con altura estable y los mismos grupos, conforme al [patrón Anterior conserva valores y Siguiente valida los requisitos del paso. Guardar solo aparece en el resumen y no se envía el alta desde pasos intermedios. La edición mantiene un formulario único y permite conservar/completar puestos anteriores o desactivados. Cancelar permanece en el pie del modal y la ayuda se mantiene anclada inmediatamente encima, fuera del scroll de campos. Los ejemplos extensos tienen desplazamiento propio limitado al espacio disponible. Se elimina `sharedAssetIds` de contratos, dominio, reservas y vistas. La migración explícita `removeSharedWorkstationAssets`, ejecutada durante la preparación del esquema, elimina las asociaciones de tipo compartido tanto actuales como históricas y limpia sus referencias técnicas cuando no existe ninguna asociación exclusiva histórica. No convierte activos compartidos en exclusivos, no borra activos ni revisiones y no genera reservas conflictivas. Es transaccional e idempotente. Las composiciones exclusivas e identidades se conservan. + + +## Crear y consultar puestos desde el inventario + +La columna Puesto del inventario muestra un botón visible «Crear puesto de trabajo» para portátiles, PC de +sobremesa y servidores operativos y sin asignación. Este acceso rápido identifica +estos tipos mediante el icono configurado en la plantilla (`laptop`, `desktop`, +`server`); no utiliza nombres traducidos ni cambia las restricciones del dominio. +Si falta el catálogo o no se reconoce el tipo, no ofrece crear. + +La acción abre el mismo formulario de cuatro pasos, con el activo incorporado como +ordenador principal y su sede preseleccionada si está activa. Conserva la propuesta +de nombre por sede y ordenador; permite revisar sede, responsable y composición antes +de guardar. Cancelar no crea puestos. Guardar utiliza el caso de uso existente, +invalida el inventario y abre la ficha del nuevo puesto; no transforma ni duplica el activo. + +Si el activo está en la composición vigente de un puesto, la columna muestra su nombre +como enlace a la ficha independientemente de su papel como ordenador principal o periférico y +de su estado técnico. Incluye puestos inactivos que todavía conservan composición; +si hay varios, cada uno conserva su propio enlace. La acción de crear sigue visible +fuera del menú de edición y eliminación. Las asociaciones que solo aparecen en +revisiones históricas no se consideran vigentes. Se preservan las comprobaciones de +compañía, reservas y estado operativo del formulario y del servidor ante concurrencia. + + +## Alta desde una persona + +El menú de cada persona activa ofrece «Nuevo puesto de trabajo». La acción navega al +listado de puestos de la compañía y abre el mismo formulario de alta mediante la +navegación de creación existente, aportando `personId` como contexto. El diálogo y +el formulario reciben `initialPersonId`: el responsable comienza con esa persona si +está activa y aparece en los datos de la compañía. El usuario puede cambiarlo o +retirarlo. Un identificador desconocido o de una persona inactiva no se preselecciona. +La edición conserva siempre el responsable guardado del puesto. Se mantienen las +altas relacionadas de sede y persona, los cuatro pasos y las validaciones de servidor. + + +El listado de Personas muestra la columna «Puestos bajo su responsabilidad», con un +enlace por puesto activo cuyo responsable actual sea esa persona. Reutiliza el mismo +criterio que la ficha de persona: no incluye puestos usados solo mediante un perfil, +responsabilidades históricas, puestos desactivados ni registros de otra compañía. +La ausencia de puestos muestra `—`; el inventario se consulta una vez por compañía +mediante la caché compartida, con estado de carga y recuperación explícita de errores. diff --git a/modules/assets/package.json b/modules/assets/package.json index f7e492c..0d8f5d4 100644 --- a/modules/assets/package.json +++ b/modules/assets/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/asset-catalog.test.ts src/api/domain/asset.test.ts src/api/domain/workstation.test.ts src/api/domain/asset-relationship.test.ts src/web/catalog/shared/catalog.test.tsx src/web/assets/shared/inventory.test.tsx src/api/domain/support.test.ts src/web/support-connections/shared/support.test.tsx src/api/domain/brand.test.ts src/web/brands/shared/brands.test.tsx src/api/domain/template-sections.test.ts src/web/workstations/shared/helpers/workstation-creation.test.ts src/web/workstations/shared/workstation-name.test.ts src/web/workstations/rename/api/rename-workstation.test.ts src/web/assets/select/asset-selection.test.tsx src/web/catalog-fields/select/catalog-field-selection.test.tsx" + "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/domain/asset-catalog.test.ts src/api/domain/asset.test.ts src/api/domain/workstation.test.ts src/api/domain/asset-relationship.test.ts src/web/catalog/shared/catalog.test.tsx src/web/assets/shared/inventory.test.tsx src/api/domain/support.test.ts src/web/support-connections/shared/support.test.tsx src/api/domain/brand.test.ts src/web/brands/shared/brands.test.tsx src/api/domain/template-sections.test.ts src/web/workstations/shared/helpers/workstation-creation.test.ts src/web/workstations/shared/workstation-name.test.ts src/web/workstations/rename/api/rename-workstation.test.ts src/web/assets/list/asset-workstation.test.tsx src/web/assets/select/asset-selection.test.tsx src/web/catalog-fields/select/catalog-field-selection.test.tsx" }, "dependencies": { "@hookform/resolvers": "5.2.2", diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index 46785db..5cbcfa2 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -97,6 +97,12 @@ "deletingAsset": "Deleting asset…", "deleteAssetError": "The asset could not be deleted. Please try again.", "viewAsset": "Details and history", + "assetWorkstation": { + "create": "Create workstation", + "view": "View workstation", + "viewNamed": "View workstation: {{name}}" + }, + "assetType": "Asset type", "template": "Template", "site": "Site", "person": "Assigned person", @@ -108,7 +114,7 @@ "retired": "Retired", "noAssets": "This company has no assets yet.", "noAssignment": "Unassigned", - "noValue": "Not specified", + "noValue": "—", "characteristics": "Characteristics", "history": "History", "recordedBy": "Recorded by {{name}}", @@ -386,7 +392,11 @@ "profilePerson": "Has a profile on the main computer", "needsReview": "References awaiting review", "missingAsset": "Asset unavailable.", - "overview": "Overview", + "information": "Information", + "created": "Workstation created", + "updated": "Workstation updated", + "noHistory": "No workstation revisions.", + "historyHint": "Revisions from newest to oldest, with the data recorded at each change.", "software": "Computer software", "sections": "Workstation sections", "peripherals": "Other assets and peripherals", @@ -466,4 +476,4 @@ "fieldsDescription": "Complete the template details. Fields marked with * are required.", "nameDescription": "Review the suggested name, technical status, site and registration date. Add notes if you need to provide more context." } -} +} \ No newline at end of file diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index c23c5b4..da479a2 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -97,6 +97,12 @@ "deletingAsset": "Eliminando activo…", "deleteAssetError": "No se ha podido eliminar el activo. Inténtalo de nuevo.", "viewAsset": "Ficha e historial", + "assetWorkstation": { + "create": "Crear puesto de trabajo", + "view": "Ver puesto de trabajo", + "viewNamed": "Ver puesto: {{name}}" + }, + "assetType": "Tipo de activo", "template": "Plantilla", "site": "Sede", "person": "Persona asignada", @@ -108,7 +114,7 @@ "retired": "Retirado", "noAssets": "Esta compañía todavía no tiene activos.", "noAssignment": "Sin asignar", - "noValue": "Sin indicar", + "noValue": "—", "characteristics": "Características", "history": "Historial", "recordedBy": "Registrado por {{name}}", @@ -386,7 +392,11 @@ "profilePerson": "Con perfil en el ordenador principal", "needsReview": "Referencias pendientes de revisión", "missingAsset": "Activo no disponible.", - "overview": "Resumen", + "information": "Información", + "created": "Alta del puesto", + "updated": "Actualización del puesto", + "noHistory": "No hay revisiones del puesto.", + "historyHint": "Revisiones de más reciente a más antigua, con los datos registrados en cada cambio.", "software": "Software del ordenador", "sections": "Secciones del puesto de trabajo", "peripherals": "Otros activos y periféricos", @@ -466,4 +476,4 @@ "fieldsDescription": "Completa los datos de la plantilla. Los campos marcados con * son obligatorios.", "nameDescription": "Revisa el nombre propuesto, el estado técnico, la sede y la fecha de alta. Añade notas si necesitas aportar más contexto." } -} +} \ No newline at end of file diff --git a/modules/assets/src/web/assets/detail/ui/asset-detail.tsx b/modules/assets/src/web/assets/detail/ui/asset-detail.tsx index a2ca445..502ae07 100644 --- a/modules/assets/src/web/assets/detail/ui/asset-detail.tsx +++ b/modules/assets/src/web/assets/detail/ui/asset-detail.tsx @@ -29,6 +29,30 @@ export function AssetDetail(props: AssetDetailProps) { ? t(value ? "yes" : "no") : String(value); + const characteristics = ( + ( + + {field.name} + {field.unit ? ` (${field.unit})` : ""} + + } + > + {display(asset.values.characteristics[field.fieldId])} + + )} + sectionPresentation={compact ? "accordion" : "details"} + sections={asset.specification.sections} + ungroupedLabel={t("sections.ungrouped")} + values + /> + ); + return (
{!compact &&

{t("snapshotNotice")}

} @@ -44,7 +68,10 @@ export function AssetDetail(props: AssetDetailProps) { )} - +
{formatAssetRegistrationDate(asset.values.registeredOn, i18n.language) ?? t("noValue")} @@ -75,33 +102,23 @@ export function AssetDetail(props: AssetDetailProps) { )}
-
- {!asset.specification.sections?.length && ( - {t("characteristics")} - )} - ( - - {field.name} - {field.unit ? ` (${field.unit})` : ""} - - } - > - {display(asset.values.characteristics[field.fieldId])} - + {compact && !asset.specification.sections?.length ? ( + + {characteristics} + + ) : ( +
+ {!asset.specification.sections?.length && ( + {t("characteristics")} )} - sections={asset.specification.sections} - ungroupedLabel={t("sections.ungrouped")} - values - /> -
+ {characteristics} +
+ )} {asset.values.notes && ( - +

{asset.values.notes}

)} diff --git a/modules/assets/src/web/assets/detail/ui/asset-selection-panel.tsx b/modules/assets/src/web/assets/detail/ui/asset-selection-panel.tsx index 139a9a2..18ddd25 100644 --- a/modules/assets/src/web/assets/detail/ui/asset-selection-panel.tsx +++ b/modules/assets/src/web/assets/detail/ui/asset-selection-panel.tsx @@ -1,5 +1,6 @@ import { DetailSection } from "@repo/rdx-ui/components/detail-section"; import { SelectionPanel } from "@repo/rdx-ui/components/selection-panel"; +import { Accordion } from "@repo/shadcn-ui/components/accordion"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -71,24 +72,39 @@ export function AssetSelectionPanel({ } > - - - - - - - - + section.id) ?? []), + ]} + keepMounted + key={asset.id} + multiple + > + + + + + + + + + ); } diff --git a/modules/assets/src/web/assets/list/asset-workstation.test.tsx b/modules/assets/src/web/assets/list/asset-workstation.test.tsx new file mode 100644 index 0000000..b660801 --- /dev/null +++ b/modules/assets/src/web/assets/list/asset-workstation.test.tsx @@ -0,0 +1,378 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { createInstance } from "i18next"; +import { renderToStaticMarkup } from "react-dom/server"; +import { I18nextProvider } from "react-i18next"; + +import { CatalogItemSchema, WorkstationSchema } from "../../../common"; +import type { AssetTemplateIconKey } from "../../../common/asset-template-icons"; +import { AssetSchema } from "../../../common/inventory"; +import { catalogKeys } from "../../catalog/shared/api"; +import { assetsEnglish, assetsSpanish } from "../../shared"; +import { inventoryKeys } from "../../shared/queries"; +import { useWorkstationFormController } from "../../workstations/shared/controllers/use-workstation-form-controller"; +import type { WorkstationFormProps } from "../../workstations/shared/types"; + +import { assetWorkstationAction } from "./helpers/asset-workstation-action"; +import { AssetInventoryPage } from "./ui/inventory-page"; + +const id = (n: number) => `0199b200-0000-7000-8000-${String(n).padStart(12, "0")}`; + +const companyId = id(1); +const values = { + name: "PC de Ana", + templateId: id(2), + siteId: id(3), + status: "operational", + notes: "", + characteristics: {}, +}; +const asset = AssetSchema.parse({ + id: id(4), + companyId, + values, + assigned: false, + displayStatus: "available", + specification: { + templateId: id(2), + templateName: "Ordenador", + fields: [ + { fieldId: id(5), name: "Modelo", type: "text", unit: "", choices: [], required: false }, + ], + }, + history: [ + { + id: id(6), + values, + actorId: id(7), + actorName: "David", + recordedAt: "2026-10-16T10:00:00.000Z", + }, + ], +}); + +function template(icon?: AssetTemplateIconKey) { + return CatalogItemSchema.parse({ + id: id(2), + companyId: null, + builtin: false, + active: true, + effectiveActive: true, + overridden: false, + bindings: [], + values: { + kind: "template", + name: "Ordenador", + description: "", + icon, + fields: [{ fieldId: id(5), required: false }], + }, + }); +} + +function station(active = true) { + const stationValues = { + name: "Puesto existente", + siteId: id(3), + personId: null, + assetIds: [asset.id, id(8)], + computerAssetId: id(8), + active, + }; + + return WorkstationSchema.parse({ + id: id(9), + companyId, + values: stationValues, + history: [ + { + id: id(10), + values: stationValues, + actorId: id(7), + actorName: "David", + recordedAt: "2026-10-16T10:00:00.000Z", + }, + ], + }); +} + +test("acciones: crear solo para portátiles, sobremesa y servidores disponibles", () => { + for (const icon of ["laptop", "desktop", "server"] satisfies AssetTemplateIconKey[]) { + assert.equal(assetWorkstationAction(asset, [], template(icon)).kind, "create"); + } + for (const icon of ["printer", "monitor", "box"] satisfies AssetTemplateIconKey[]) { + assert.equal(assetWorkstationAction(asset, [], template(icon)).kind, "none"); + } + assert.equal(assetWorkstationAction(asset, [], template()).kind, "none"); + assert.equal(assetWorkstationAction(asset, [], undefined).kind, "none"); + for (const status of ["broken", "maintenance", "retired"] as const) { + assert.equal( + assetWorkstationAction( + { ...asset, values: { ...asset.values, status } }, + [], + template("laptop") + ).kind, + "none" + ); + } + assert.equal( + assetWorkstationAction({ ...asset, assigned: true }, [], template("laptop")).kind, + "none" + ); +}); + +test("acciones: abrir el puesto asociado aunque el activo sea periférico, no operativo o el puesto esté inactivo", () => { + for (const active of [true, false]) { + const workstation = station(active); + const action = assetWorkstationAction( + { ...asset, values: { ...asset.values, status: "retired" } }, + [workstation], + template("printer") + ); + + assert.equal(action.kind, "view"); + if (action.kind === "view") { + assert.equal(action.workstations[0]?.id, workstation.id); + assert.notEqual(workstation.values.computerAssetId, asset.id); + } + } + const first = station(false); + const second = { ...first, id: id(11) }; + const multiple = assetWorkstationAction(asset, [first, second], undefined); + + assert.equal(multiple.kind, "view"); + if (multiple.kind === "view") { + assert.equal(multiple.workstations.length, 2); + } +}); + +test("acciones: compañía y plantilla correctas; asociaciones solo históricas no bloquean el alta", () => { + const foreign = { ...station(), companyId: id(12) }; + const decommissioned = { + ...station(false), + values: { ...station(false).values, assetIds: [], computerAssetId: null }, + }; + + assert.equal( + assetWorkstationAction(asset, [foreign, decommissioned], template("laptop")).kind, + "create" + ); + assert.equal( + assetWorkstationAction(asset, [], { ...template("laptop"), companyId: id(12) }).kind, + "none" + ); + assert.equal( + assetWorkstationAction(asset, [], { ...template("laptop"), id: id(12) }).kind, + "none" + ); +}); + +function WorkstationSeedPreview(props: WorkstationFormProps) { + const { values: current, isWizard, canContinue } = useWorkstationFormController(props); + + return ( +
+ ); +} + +test("alta desde activo es/en: composición y ordenador preseleccionados, sede activa y nombre propuesto; edición y compañía se respetan", async () => { + for (const language of ["es", "en"]) { + const i18n = createInstance(); + + await i18n.init({ + lng: language, + resources: { es: { assets: assetsSpanish }, en: { assets: assetsEnglish } }, + }); + const client = new QueryClient({ + defaultOptions: { queries: { staleTime: Number.POSITIVE_INFINITY, retry: false } }, + }); + + client.setQueryData(inventoryKeys.company(companyId), { + assets: [asset], + workstations: [], + relationships: [], + }); + client.setQueryData(catalogKeys.scope(companyId), { items: [template("laptop")] }); + + const render = (props: WorkstationFormProps) => + renderToStaticMarkup( + + + + + + ); + + const props: WorkstationFormProps = { + companyId, + initialAsset: asset, + onSaved: () => undefined, + lookups: { + sites: [ + { id: id(13), name: "Oficina", active: true }, + { id: id(3), name: "Fábrica", active: true }, + ], + people: [], + onCreatePerson: () => undefined, + onCreateSite: () => undefined, + }, + }; + + try { + const personProps: WorkstationFormProps = { + ...props, + initialPersonId: id(14), + lookups: { ...props.lookups, people: [{ id: id(14), name: "Ana", active: true }] }, + }; + const withPerson = render(personProps); + + assert.ok(withPerson.includes(`data-person="${id(14)}"`)); + const inactivePerson = render({ + ...personProps, + lookups: { ...personProps.lookups, people: [{ id: id(14), name: "Ana", active: false }] }, + }); + + assert.equal(inactivePerson.includes(`data-person="${id(14)}"`), false); + const missingPerson = render({ ...personProps, initialPersonId: id(15) }); + + assert.equal(missingPerson.includes(`data-person="${id(15)}"`), false); + const existingPerson = render({ ...personProps, workstation: station() }); + + assert.equal(existingPerson.includes(`data-person="${id(14)}"`), false); + const html = render(props); + + assert.ok(html.includes(`data-assets="${asset.id}"`)); + assert.ok(html.includes(`data-computer="${asset.id}"`)); + assert.ok(html.includes(`data-site="${asset.values.siteId}"`)); + assert.ok( + html.includes( + language === "es" + ? 'data-name="Puesto PC de Ana en Fábrica"' + : 'data-name="Workstation for PC de Ana at Fábrica"' + ) + ); + assert.ok(html.includes('data-wizard="true"')); + assert.ok(html.includes('data-can-continue="true"')); + client.setQueryData(inventoryKeys.company(companyId), { + assets: [asset], + workstations: [station()], + relationships: [], + }); + const occupied = render(props); + + assert.ok(occupied.includes('data-can-continue="false"')); + client.setQueryData(inventoryKeys.company(companyId), { + assets: [asset], + workstations: [], + relationships: [], + }); + const foreign = render({ ...props, initialAsset: { ...asset, companyId: id(12) } }); + + assert.ok(foreign.includes('data-assets=""')); + assert.equal(foreign.includes(`data-computer="${asset.id}"`), false); + const edited = render({ ...props, workstation: station() }); + + assert.ok(edited.includes('data-name="Puesto existente"')); + assert.ok(edited.includes(`data-computer="${id(8)}"`)); + assert.ok(edited.includes('data-wizard="false"')); + const inactiveSite = render({ + ...props, + lookups: { + ...props.lookups, + sites: [{ id: id(3), name: "Sede archivada", active: false }], + }, + }); + + assert.equal(inactiveSite.includes(`data-site="${id(3)}"`), false); + assert.ok(inactiveSite.includes('data-can-continue="false"')); + } finally { + client.clear(); + } + } +}); + +test("inventario es/en: crear y ver puesto son acciones visibles fuera del menú", async () => { + for (const language of ["es", "en"]) { + const i18n = createInstance(); + + await i18n.init({ + lng: language, + resources: { es: { assets: assetsSpanish }, en: { assets: assetsEnglish } }, + }); + const client = new QueryClient({ + defaultOptions: { queries: { staleTime: Number.POSITIVE_INFINITY, retry: false } }, + }); + const labels = language === "es" ? assetsSpanish : assetsEnglish; + + client.setQueryData(catalogKeys.scope(companyId), { items: [template("laptop")] }); + client.setQueryData(inventoryKeys.company(companyId), { + assets: [asset], + workstations: [], + relationships: [], + }); + + const renderPage = () => + renderToStaticMarkup( + + + undefined, + onCreateSite: () => undefined, + }} + /> + + + ); + + try { + const creation = renderPage(); + const createButton = [...creation.matchAll(/]*>[\s\S]*?<\/button>/g)].find( + ([button]) => button.includes(labels.assetWorkstation.create) + )?.[0]; + + assert.ok(createButton); + assert.equal(createButton.includes('aria-haspopup="menu"'), false); + assert.ok(creation.includes(labels.workstationInAsset)); + client.setQueryData(inventoryKeys.company(companyId), { + assets: [asset], + workstations: [station()], + relationships: [], + }); + const viewing = renderPage(); + const viewLink = [...viewing.matchAll(/]*>[\s\S]*?<\/a>/g)].find(([link]) => + link.includes(station().values.name) + )?.[0]; + + assert.ok(viewLink); + assert.ok(viewLink.includes(`href="/companies/${companyId}/workstations/${station().id}"`)); + assert.ok(viewLink.includes(station().values.name)); + assert.equal(viewLink.includes(labels.assetWorkstation.view), false); + const headers = [...viewing.matchAll(/]*>[\s\S]*?<\/th>/g)].map(([header]) => header); + + assert.ok(headers[2]?.includes(labels.template)); + assert.ok(headers[3]?.includes(labels.status)); + assert.ok(headers[4]?.includes(labels.site)); + assert.ok(headers[5]?.includes(labels.workstationInAsset)); + assert.ok(headers[6]?.includes(labels.registeredOn)); + assert.ok(headers[7]?.includes(labels.actions)); + assert.equal(viewing.includes(labels.assetWorkstation.create), false); + assert.ok(viewing.includes('aria-haspopup="menu"')); + } finally { + client.clear(); + } + } +}); diff --git a/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts b/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts index 3150772..0611fa7 100644 --- a/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts +++ b/modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts @@ -61,6 +61,7 @@ export function useAssetInventoryPageController({ }); const [editor, setEditor] = useState(initialCreate ? "new" : undefined); + const [workstationAssetId, setWorkstationAssetId] = useState(); const [deleteId, setDeleteId] = useState(); const remove = useMutation({ mutationFn: (id: string) => removeAsset(companyId, id), @@ -129,6 +130,8 @@ export function useAssetInventoryPageController({ setDetailOpen, selected, edited, + workstationAsset: assets.find((asset) => asset.id === workstationAssetId), + setWorkstationAssetId, viewed, }; } diff --git a/modules/assets/src/web/assets/list/helpers/asset-workstation-action.ts b/modules/assets/src/web/assets/list/helpers/asset-workstation-action.ts new file mode 100644 index 0000000..78837a9 --- /dev/null +++ b/modules/assets/src/web/assets/list/helpers/asset-workstation-action.ts @@ -0,0 +1,38 @@ +import type { CatalogItemDTO, WorkstationDTO } from "../../../../common"; +import type { AssetDTO } from "../../../../common/inventory"; +import { canAssignAsset } from "../../../../common/inventory"; + +type AssetWorkstationAction = + | { kind: "view"; workstations: readonly WorkstationDTO[] } + | { kind: "create" } + | { kind: "none" }; + +/** El acceso rápido usa los tipos visuales del catálogo; no cambia las reglas de composición del dominio. */ +export function assetWorkstationAction( + asset: AssetDTO, + workstations: readonly WorkstationDTO[], + template: CatalogItemDTO | undefined +): AssetWorkstationAction { + const related = workstations.filter( + (workstation) => + workstation.companyId === asset.companyId && workstation.values.assetIds.includes(asset.id) + ); + + if (related.length) { + return { kind: "view", workstations: related }; + } + if ( + !canAssignAsset(asset.values.status, asset.assigned) || + template?.id !== asset.values.templateId || + (template.companyId !== null && template.companyId !== asset.companyId) || + template.values.kind !== "template" + ) { + return { kind: "none" }; + } + + return template.values.icon === "laptop" || + template.values.icon === "desktop" || + template.values.icon === "server" + ? { kind: "create" } + : { kind: "none" }; +} diff --git a/modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx b/modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx new file mode 100644 index 0000000..d307b75 --- /dev/null +++ b/modules/assets/src/web/assets/list/ui/asset-workstation-action.tsx @@ -0,0 +1,53 @@ +import { BrowserLink } from "@metis/core/client"; +import { MonitorIcon } from "@repo/rdx-ui/lib/icons"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { useTranslation } from "react-i18next"; + +import type { CatalogItemDTO, WorkstationDTO } from "../../../../common"; +import type { AssetDTO } from "../../../../common/inventory"; +import { assetWorkstationAction } from "../helpers/asset-workstation-action"; + +export function AssetWorkstationAction({ + asset, + workstations, + template, + onCreate, +}: { + asset: AssetDTO; + workstations: readonly WorkstationDTO[]; + template: CatalogItemDTO | undefined; + onCreate: (assetId: string) => void; +}) { + const { t } = useTranslation("assets"); + const action = assetWorkstationAction(asset, workstations, template); + + if (action.kind === "view") { + return ( +
+ {action.workstations.map((workstation) => ( + + + ))} +
+ ); + } + + return action.kind === "create" ? ( + + ) : null; +} diff --git a/modules/assets/src/web/assets/list/ui/inventory-page.tsx b/modules/assets/src/web/assets/list/ui/inventory-page.tsx index 25d6ae9..bbb6591 100644 --- a/modules/assets/src/web/assets/list/ui/inventory-page.tsx +++ b/modules/assets/src/web/assets/list/ui/inventory-page.tsx @@ -1,20 +1,24 @@ -import { BrowserLink, EntityNotFound } from "@metis/core/client"; +import { BrowserLink, EntityNotFound, navigateTo } from "@metis/core/client"; import { EntityAction, EntityListHeader, EntityRowActions, } from "@repo/rdx-ui/components/entity-list"; +import { BoxIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; import { CatalogError } from "../../../catalog"; import { InventoryFeedback } from "../../../shared"; +import { WorkstationEditorDialog } from "../../../workstations/shared/ui/workstation-editor-dialog"; import { AssetDetailDialog, AssetSelectionPanel } from "../../detail"; import { AssetEditorDialog, DeleteAssetDialog } from "../../shared"; import { AssetList } from "../../shared/ui/asset-list"; import { useAssetInventoryPageController } from "../controllers"; import type { AssetInventoryPageProps } from "../types"; +import { AssetWorkstationAction } from "./asset-workstation-action"; + function selectedRowClass(id: string, selectedId: string | undefined, detailOpen: boolean) { return id === selectedId && detailOpen ? "bg-accent hover:bg-accent [&>[data-pinned=right]]:bg-accent [&>[data-pinned=right]]:group-hover/entity-row:bg-accent [&>[data-pinned=right]]:group-has-aria-expanded/entity-row:bg-accent" @@ -33,6 +37,8 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { setSearch, editor, setEditor, + workstationAsset, + setWorkstationAssetId, deleted, deleteId, setDeleteId, @@ -76,11 +82,13 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { )}
{" "} } + icon={} locale={i18n.language} title={t("inventory")} /> @@ -103,6 +111,21 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { assets={query.data.assets} catalog={catalog.data?.items ?? []} extraColumns={[ + { + id: "workstation", + label: t("workstationInAsset"), + hideable: false, + render: (asset) => ( + item.id === asset.values.templateId + )} + workstations={query.data?.workstations ?? []} + /> + ), + }, { id: "actions", label: t("actions"), @@ -117,6 +140,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { > {t("viewAsset")} + setEditor(asset.id)}> {t("edit")} @@ -172,12 +196,22 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { lookups={lookups} onClose={() => setEditor(undefined)} onSaved={(id) => { - setSelectedId(id); - setDetailOpen(true); + selectAsset(id); setEditor(undefined); }} /> )} + {workstationAsset && ( + setWorkstationAssetId(undefined)} + onSaved={(id) => navigateTo(`/companies/${companyId}/workstations/${id}`)} + workstation={undefined} + /> + )} {viewed && ( ]*>([\s\S]*?)<\/td>/g)].map((match) => match[1]); + + assert.ok(cells[1]?.includes('data-slot="tooltip-trigger"')); + assert.ok(cells[2]?.includes("Equipo 2")); assert.ok(html.includes('aria-checked="true"')); assert.ok(html.includes("Ocupado")); assert.ok(html.includes(language === "es" ? "Buscar" : "Search")); @@ -205,3 +210,45 @@ test("picker: búsqueda rápida disponible durante carga y error, sin anunciar u } } }); + +test("inventario es/en: icono compacto antes del nombre, plantilla solo textual y ordenación por nombre", async () => { + const template = CatalogItemSchema.parse({ + id: id(12), + companyId: null, + builtin: false, + active: true, + effectiveActive: true, + overridden: false, + bindings: [], + values: { + kind: "template", + name: "Portátiles", + description: "", + icon: "laptop", + fields: [{ fieldId: id(5), required: false }], + }, + }); + + for (const language of ["es", "en"]) { + const html = await render(language, ); + const table = html.match(//)?.[0]; + + assert.ok(table); + const headers = [...table.matchAll(/]*>[\s\S]*?<\/th>/g)].map(([header]) => header); + const row = table.match(/]*>([\s\S]*?)<\/tr>/)?.[1] ?? ""; + const cells = [...row.matchAll(/]*>([\s\S]*?)<\/td>/g)].map((match) => match[1]); + + assert.ok(headers[0]?.includes(language === "es" ? "Tipo de activo" : "Asset type")); + assert.equal(headers[0]?.includes("aria-sort"), false); + assert.ok(headers[0]?.includes("min-w-0")); + assert.ok(headers[1]?.includes('aria-sort="ascending"')); + assert.ok(cells[0]?.includes('aria-label="Portátiles"')); + assert.ok(cells[0]?.includes('tabindex="0"')); + assert.ok(cells[0]?.includes('data-slot="tooltip-trigger"')); + assert.ok(cells[0]?.includes("tabler-icon-device-laptop")); + assert.ok(cells[1]?.includes("Equipo 2")); + assert.ok(cells[2]?.includes("Portátiles")); + assert.equal(cells[2]?.includes("= 5); assert.ok(html.includes(`href="/companies/${companyId}/sites"`)); assert.ok(html.includes(`href="/companies/${companyId}/catalogs/assets?tab=templates"`)); assert.ok(html.includes("ABC123")); assert.ok(html.includes("Fecha de alta")); assert.ok(html.includes("29 feb 2024")); + + const grouped = AssetSchema.parse({ + ...asset, + specification: { + ...asset.specification, + sections: [ + { id: fieldId, name: "Red", description: "Datos de red" }, + { id: personId, name: "Grupo vacío", description: "" }, + ], + fields: [ + ...asset.specification.fields.map((field) => ({ ...field, sectionId: fieldId })), + { + fieldId: siteId, + name: "Contador", + type: "number", + unit: "", + choices: [], + required: false, + }, + { + fieldId: personId, + name: "Color", + type: "boolean", + unit: "", + choices: [], + required: false, + sectionId: companyId, + }, + ], + }, + values: { + ...asset.values, + characteristics: { ...asset.values.characteristics, [siteId]: 0, [personId]: false }, + }, + }); + + for (const language of ["es", "en"]) { + const labels = language === "es" ? assetsSpanish : assetsEnglish; + const groupedHtml = await render( + language, + undefined} + onDelete={() => undefined} + onEdit={() => undefined} + onView={() => undefined} + open + /> + ); + + assert.ok(groupedHtml.includes('data-slot="accordion"')); + assert.ok((groupedHtml.match(/aria-expanded="true"/g) ?? []).length >= 6); + assert.ok(groupedHtml.includes("Datos de red")); + assert.ok(groupedHtml.includes(labels.sections.ungrouped)); + assert.ok(groupedHtml.includes("ABC123")); + assert.ok(groupedHtml.includes(">0")); + assert.ok(groupedHtml.includes(`>${labels.no}`)); + assert.equal(groupedHtml.includes("Grupo vacío"), false); + } }); test("texto largo: área editable con ayuda accesible", async () => { diff --git a/modules/assets/src/web/assets/shared/ui/asset-list.tsx b/modules/assets/src/web/assets/shared/ui/asset-list.tsx index 40bdeb2..5a34f7c 100644 --- a/modules/assets/src/web/assets/shared/ui/asset-list.tsx +++ b/modules/assets/src/web/assets/shared/ui/asset-list.tsx @@ -7,10 +7,11 @@ import { useTranslation } from "react-i18next"; import type { CatalogItemDTO } from "../../../../common"; import type { AssetDTO } from "../../../../common/inventory"; -import { AssetTemplateIcon } from "../../../catalog-templates/shared/ui/asset-template-icon"; import { assetCharacteristicsSummary } from "../helpers/asset-characteristics-summary"; import { formatAssetRegistrationDate } from "../helpers/format-asset-registration-date"; +import { AssetTemplateIndicator } from "./asset-template-indicator"; + /** Inventario y selectores comparten columnas, búsqueda, pestañas y paginación. */ export function AssetList({ assets, @@ -98,13 +99,31 @@ export function AssetList({ { + const template = catalog.find((item) => item.id === asset.values.templateId); + + return ( + + ); + }, + }, { id: "name", label: t("name"), value: (asset) => asset.values.name, hideable: false, render: (asset) => { - const summary = assetCharacteristicsSummary(asset, { yes: t("yes"), no: t("no") }); + const summary = assetCharacteristicsSummary(asset, { + yes: t("yes"), + no: t("no"), + }); return (
@@ -126,38 +145,39 @@ export function AssetList({ id: "template", label: t("template"), value: (asset) => asset.specification.templateName, - render: (asset) => { - const template = catalog.find((item) => item.id === asset.values.templateId); - - return ( - - - {asset.specification.templateName} - - ); - }, + render: (asset) => ( + {asset.specification.templateName} + ), }, { id: "state", label: t("status"), value: (asset) => t(asset.displayStatus), render: (asset) => ( - - {t(asset.displayStatus)} - + + + {t(asset.displayStatus)} + + ), }, + { id: "site", label: t("site"), value: siteName, render: siteName }, + ...extraColumns.filter((column) => column.id !== "actions"), { id: "registeredOn", label: t("registeredOn"), value: (asset) => asset.values.registeredOn ?? "", - render: (asset) => - formatAssetRegistrationDate(asset.values.registeredOn, i18n.language), + render: (asset) => { + const date = formatAssetRegistrationDate(asset.values.registeredOn, i18n.language); + + return date ? ( + + {date} + + ) : null; + }, }, - { id: "site", label: t("site"), value: siteName, render: siteName }, - ...extraColumns, + ...extraColumns.filter((column) => column.id === "actions"), ]} emptyMessage={t("noAssets")} filterKey={templateId ?? "all"} diff --git a/modules/assets/src/web/assets/shared/ui/asset-template-indicator.tsx b/modules/assets/src/web/assets/shared/ui/asset-template-indicator.tsx new file mode 100644 index 0000000..66c0a02 --- /dev/null +++ b/modules/assets/src/web/assets/shared/ui/asset-template-indicator.tsx @@ -0,0 +1,34 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@repo/shadcn-ui/components/tooltip"; + +import type { AssetTemplateIconKey } from "../../../../common/asset-template-icons"; +import { AssetTemplateIcon } from "../../../catalog-templates/shared/ui/asset-template-icon"; + +/** Identifica la plantilla visualmente y revela su nombre con puntero o foco de teclado. */ +export function AssetTemplateIndicator({ + icon, + name, +}: { + icon: AssetTemplateIconKey | undefined; + name: string; +}) { + return ( + + + } + tabIndex={0} + > + + + {name} + + + ); +} diff --git a/modules/assets/src/web/brands/list/ui/brands-page.tsx b/modules/assets/src/web/brands/list/ui/brands-page.tsx index 127e9b6..c46a8f2 100644 --- a/modules/assets/src/web/brands/list/ui/brands-page.tsx +++ b/modules/assets/src/web/brands/list/ui/brands-page.tsx @@ -6,6 +6,7 @@ import { EntityTable, } from "@repo/rdx-ui/components/entity-list"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; +import { Building2Icon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useId } from "react"; import { useTranslation } from "react-i18next"; @@ -31,10 +32,12 @@ export function BrandCatalogPage() { setOpen(true); }} > - + {t("brands.new")} +
{" "} } description={t(scope === "global" ? "catalogHelp" : "workspaceHelp")} + icon={} locale={i18n.language} title={t(`kinds.${kind}`)} /> diff --git a/modules/companies/src/web/application-configurations/list/ui/application-configurations-page.tsx b/modules/companies/src/web/application-configurations/list/ui/application-configurations-page.tsx index 56e8adb..490ceaa 100644 --- a/modules/companies/src/web/application-configurations/list/ui/application-configurations-page.tsx +++ b/modules/companies/src/web/application-configurations/list/ui/application-configurations-page.tsx @@ -5,6 +5,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { LayoutTemplateIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -53,10 +54,12 @@ export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPa <> {" "} {" "} } + icon={} locale={i18n.language} title={t("applications.title")} /> diff --git a/modules/companies/src/web/certificate-configurations/list/ui/certificate-configurations-page.tsx b/modules/companies/src/web/certificate-configurations/list/ui/certificate-configurations-page.tsx index c6d8b20..6c97cdd 100644 --- a/modules/companies/src/web/certificate-configurations/list/ui/certificate-configurations-page.tsx +++ b/modules/companies/src/web/certificate-configurations/list/ui/certificate-configurations-page.tsx @@ -5,6 +5,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { FileKeyIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -53,10 +54,12 @@ export function CertificateConfigurationsPage(props: CertificateConfigurationsPa <> {" "} {" "} } + icon={} locale={i18n.language} title={t("certificates.title")} /> diff --git a/modules/companies/src/web/companies/detail/controllers/use-company-detail-controller.ts b/modules/companies/src/web/companies/detail/controllers/use-company-detail-controller.ts index a1ecc68..0b33fc3 100644 --- a/modules/companies/src/web/companies/detail/controllers/use-company-detail-controller.ts +++ b/modules/companies/src/web/companies/detail/controllers/use-company-detail-controller.ts @@ -1,6 +1,16 @@ +import { + inventoryApi as assetInventoryApi, + inventoryKeys as assetInventoryKeys, +} from "@metis/assets/client"; +import { useListQuery } from "@metis/core/client"; +import { + inventoryApi as resourceInventoryApi, + inventoryKeys as resourceInventoryKeys, +} from "@metis/resources/client"; import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; +import { profileApi, profileKeys } from "../../../work-profiles/shared/api"; import { companyKeys } from "../../shared/queries"; import type { CompanyTab } from "../../shared/types"; import { getCompany } from "../api"; @@ -19,5 +29,30 @@ export function useCompanyDetailController({ const setTab = onSectionChange ?? setLocalTab; const company = query.data?.company; - return { query, editOpen, setEditOpen, tab, setTab, company }; + const inventory = useListQuery({ + queryKey: assetInventoryKeys.company(id), + queryFn: () => assetInventoryApi.get(id), + enabled: Boolean(company), + }); + const resources = useListQuery({ + queryKey: resourceInventoryKeys.company(id), + queryFn: () => resourceInventoryApi.get(id), + enabled: Boolean(company), + }); + const profiles = useListQuery({ + queryKey: profileKeys.company(id), + queryFn: () => profileApi.get(id), + enabled: Boolean(company), + }); + const counts: Partial> = { + people: { count: company?.people.length, failed: false }, + departments: { count: company?.departments.length, failed: false }, + sites: { count: company?.sites.length, failed: false }, + inventory: { count: inventory.data?.assets.length, failed: inventory.isError }, + workstations: { count: inventory.data?.workstations.length, failed: inventory.isError }, + resources: { count: resources.data?.resources.length, failed: resources.isError }, + profiles: { count: profiles.data?.profiles.length, failed: profiles.isError }, + }; + + return { query, editOpen, setEditOpen, tab, setTab, company, counts }; } diff --git a/modules/companies/src/web/companies/detail/ui/company-detail.tsx b/modules/companies/src/web/companies/detail/ui/company-detail.tsx index 41b7923..72d2216 100644 --- a/modules/companies/src/web/companies/detail/ui/company-detail.tsx +++ b/modules/companies/src/web/companies/detail/ui/company-detail.tsx @@ -1,9 +1,12 @@ import { BrowserLink } from "@metis/core/client"; +import { CountBadge } from "@repo/rdx-ui/components/count-badge"; import { BoxIcon, BriefcaseIcon, ChevronLeftIcon, DatabaseIcon, + FileKeyIcon, + FolderIcon, HomeIcon, KeyRoundIcon, LayersIcon, @@ -11,7 +14,9 @@ import { MailIcon, MapPinIcon, MonitorIcon, - UsersIcon, + PersonIcon, + PrinterIcon, + WorkProfileIcon, } from "@repo/rdx-ui/lib/icons"; import { Avatar, AvatarBadge } from "@repo/shadcn-ui/components/avatar"; import { Button } from "@repo/shadcn-ui/components/button"; @@ -34,8 +39,9 @@ export function CompanyDetail(props: CompanyDetailProps) { initialCreate, initialConfiguration, } = props; - const { query, editOpen, setEditOpen, tab, setTab, company } = useCompanyDetailController(props); - const { t } = useTranslation("companies"); + const { query, editOpen, setEditOpen, tab, setTab, company, counts } = + useCompanyDetailController(props); + const { t, i18n } = useTranslation("companies"); const { t: resourceTranslation } = useTranslation("resources"); const { t: assetTranslation } = useTranslation("assets"); const { t: accessTranslation } = useTranslation("access"); @@ -106,25 +112,21 @@ export function CompanyDetail(props: CompanyDetailProps) { {( [ ["overview", t("companyOverview.navigation"), HomeIcon], - ["people", `${t("people")} (${company.people.length})`, UsersIcon], + ["people", t("people"), PersonIcon], ["workstations", assetTranslation("workstations"), MonitorIcon], - ["profiles", t("profiles.title"), UsersIcon], + ["profiles", t("profiles.title"), WorkProfileIcon], ["inventory", assetTranslation("inventory"), BoxIcon], ["resources", resourceTranslation("inventory"), LayersIcon], - [ - "departments", - `${t("departments")} (${company.departments.length})`, - BriefcaseIcon, - ], - ["sites", `${t("sites")} (${company.sites.length})`, MapPinIcon], + ["departments", t("departments"), BriefcaseIcon], + ["sites", t("sites"), MapPinIcon], ["telework", t("telework.title"), KeyRoundIcon], ["remote", t("remote.title"), KeyRoundIcon], ["processes", t("processes.title"), LayoutTemplateIcon], ["access", accessTranslation("title"), KeyRoundIcon], - ["folders", t("folders.title"), MailIcon], - ["certificates", t("certificates.title"), LayersIcon], - ["applications", t("applications.title"), LayersIcon], - ["printers", t("printers.title"), LayersIcon], + ["folders", t("folders.title"), FolderIcon], + ["certificates", t("certificates.title"), FileKeyIcon], + ["applications", t("applications.title"), LayoutTemplateIcon], + ["printers", t("printers.title"), PrinterIcon], ["mail", t("mail.title"), MailIcon], ["catalogs", t("catalogOverview.companyTitle"), DatabaseIcon], ["assets", assetTranslation("companyTitle"), LayoutTemplateIcon], @@ -135,6 +137,7 @@ export function CompanyDetail(props: CompanyDetailProps) { return null; } const [section, label, Icon] = entry; + const count = counts[section]; return (
); diff --git a/modules/companies/src/web/companies/detail/ui/company-inventory.tsx b/modules/companies/src/web/companies/detail/ui/company-inventory.tsx index 7508f61..d6a5d39 100644 --- a/modules/companies/src/web/companies/detail/ui/company-inventory.tsx +++ b/modules/companies/src/web/companies/detail/ui/company-inventory.tsx @@ -11,11 +11,13 @@ export function CompanyInventory({ workstations = false, initialId, initialCreate, + initialPersonId, }: { company: CompanyDTO; workstations?: boolean; initialId?: string | undefined; initialCreate?: boolean | undefined; + initialPersonId?: string | undefined; }) { const [quick, setQuick] = useState<{ kind: "site" | "person"; @@ -40,7 +42,7 @@ export function CompanyInventory({ /> ) : ( diff --git a/modules/companies/src/web/companies/detail/ui/company-overview-statistics.tsx b/modules/companies/src/web/companies/detail/ui/company-overview-statistics.tsx index 81d667d..63eed5b 100644 --- a/modules/companies/src/web/companies/detail/ui/company-overview-statistics.tsx +++ b/modules/companies/src/web/companies/detail/ui/company-overview-statistics.tsx @@ -6,7 +6,7 @@ import { DatabaseIcon, LaptopIcon, MapPinIcon, - UsersIcon, + PersonIcon, } from "@repo/rdx-ui/lib/icons"; import type { UseQueryResult } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; @@ -69,7 +69,7 @@ export function CompanyOverviewStatistics({ > {( [ - { section: "people", label: "people", value: company.people.length, Icon: UsersIcon }, + { section: "people", label: "people", value: company.people.length, Icon: PersonIcon }, { section: "departments", label: "departments", diff --git a/modules/companies/src/web/companies/detail/ui/company-section.tsx b/modules/companies/src/web/companies/detail/ui/company-section.tsx index ef4eded..6797c8d 100644 --- a/modules/companies/src/web/companies/detail/ui/company-section.tsx +++ b/modules/companies/src/web/companies/detail/ui/company-section.tsx @@ -110,7 +110,15 @@ export function CompanySection({ /> ); case "workstations": - return ; + return ( + + ); case "inventory": return ( diff --git a/modules/companies/src/web/companies/list/ui/companies-page.tsx b/modules/companies/src/web/companies/list/ui/companies-page.tsx index d16137b..b93750b 100644 --- a/modules/companies/src/web/companies/list/ui/companies-page.tsx +++ b/modules/companies/src/web/companies/list/ui/companies-page.tsx @@ -6,6 +6,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { Building2Icon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -52,10 +53,14 @@ export function CompaniesPage(props: CompaniesPageProps) { actions={ <> {" "} - {" "} + {" "} } description={t("help")} + icon={} locale={i18n.language} title={t("title")} /> diff --git a/modules/companies/src/web/dashboard/shared/helpers/dashboard-kind-icons.ts b/modules/companies/src/web/dashboard/shared/helpers/dashboard-kind-icons.ts index 5149108..bc5085c 100644 --- a/modules/companies/src/web/dashboard/shared/helpers/dashboard-kind-icons.ts +++ b/modules/companies/src/web/dashboard/shared/helpers/dashboard-kind-icons.ts @@ -1,8 +1,8 @@ -import { DatabaseIcon, LaptopIcon, UsersIcon } from "@repo/rdx-ui/lib/icons"; +import { DatabaseIcon, LaptopIcon, PersonIcon, WorkProfileIcon } from "@repo/rdx-ui/lib/icons"; export const KIND_ICONS = { asset: LaptopIcon, resource: DatabaseIcon, - person: UsersIcon, - profile: UsersIcon, + person: PersonIcon, + profile: WorkProfileIcon, }; diff --git a/modules/companies/src/web/dashboard/view/ui/dashboard-attention-panel.tsx b/modules/companies/src/web/dashboard/view/ui/dashboard-attention-panel.tsx index dbc85b0..c9675d0 100644 --- a/modules/companies/src/web/dashboard/view/ui/dashboard-attention-panel.tsx +++ b/modules/companies/src/web/dashboard/view/ui/dashboard-attention-panel.tsx @@ -4,7 +4,7 @@ import { FileKeyIcon, TriangleAlertIcon, UserRoundXIcon, - UsersIcon, + WorkProfileIcon, } from "@repo/rdx-ui/lib/icons"; import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; @@ -16,7 +16,7 @@ import { dashboardApi, dashboardKeys } from "../../shared/api"; const ISSUE_ICONS = { certificate: FileKeyIcon, license: FileKeyIcon, - profile: UsersIcon, + profile: WorkProfileIcon, missing_site: BoxIcon, inactive_person: UserRoundXIcon, }; diff --git a/modules/companies/src/web/departments/list/ui/department-section.tsx b/modules/companies/src/web/departments/list/ui/department-section.tsx index 7a83ca0..3a9230e 100644 --- a/modules/companies/src/web/departments/list/ui/department-section.tsx +++ b/modules/companies/src/web/departments/list/ui/department-section.tsx @@ -5,6 +5,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { BriefcaseIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -21,7 +22,13 @@ export function DepartmentSection({ company }: { company: CompanyDTO }) { return (
setEditor("new")}>{t("newDepartment")}} + actions={ + + } + icon={} locale={i18n.language} title={t("departments")} /> diff --git a/modules/companies/src/web/folder-mappings/list/ui/folder-mappings-page.tsx b/modules/companies/src/web/folder-mappings/list/ui/folder-mappings-page.tsx index ceae824..0cd9ca4 100644 --- a/modules/companies/src/web/folder-mappings/list/ui/folder-mappings-page.tsx +++ b/modules/companies/src/web/folder-mappings/list/ui/folder-mappings-page.tsx @@ -5,6 +5,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { FolderIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -49,10 +50,12 @@ export function FolderMappingsPage(props: FolderMappingsPageProps) { <> {" "} {" "} } + icon={} locale={i18n.language} title={t("folders.title")} /> diff --git a/modules/companies/src/web/mail-configurations/list/ui/mail-configurations-page.tsx b/modules/companies/src/web/mail-configurations/list/ui/mail-configurations-page.tsx index 0cd41dc..0620c19 100644 --- a/modules/companies/src/web/mail-configurations/list/ui/mail-configurations-page.tsx +++ b/modules/companies/src/web/mail-configurations/list/ui/mail-configurations-page.tsx @@ -5,6 +5,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { MailIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -52,10 +53,12 @@ export function MailConfigurationsPage(props: MailConfigurationsPageProps) { disabled={!(query.data && inventory.data && profiles.data && mailboxes.data)} onClick={() => setEditor("new")} > +
{" "} } + icon={} locale={i18n.language} title={t("processes.title")} /> diff --git a/modules/companies/src/web/remote-connections/list/ui/remote-connections-page.tsx b/modules/companies/src/web/remote-connections/list/ui/remote-connections-page.tsx index e2e75f4..641dfd2 100644 --- a/modules/companies/src/web/remote-connections/list/ui/remote-connections-page.tsx +++ b/modules/companies/src/web/remote-connections/list/ui/remote-connections-page.tsx @@ -5,6 +5,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { KeyRoundIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -46,10 +47,12 @@ export function RemoteConnectionsPage(props: RemoteConnectionsPageProps) { <> {" "} {" "} } + icon={} locale={i18n.language} title={t("remote.title")} /> diff --git a/modules/companies/src/web/sites/list/ui/site-section.tsx b/modules/companies/src/web/sites/list/ui/site-section.tsx index a8b9881..54c3acf 100644 --- a/modules/companies/src/web/sites/list/ui/site-section.tsx +++ b/modules/companies/src/web/sites/list/ui/site-section.tsx @@ -6,6 +6,7 @@ import { EntityTable, } from "@repo/rdx-ui/components/entity-list"; import { FormLabel } from "@repo/rdx-ui/components/form-label"; +import { MapPinIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { Checkbox } from "@repo/shadcn-ui/components/checkbox"; import { useId } from "react"; @@ -36,7 +37,13 @@ export function SiteSection({ company }: { company: CompanyDTO }) { return (
setCreateOpen(true)}>{t("newSite")}} + actions={ + + } + icon={} locale={i18n.language} title={t("sites")} /> diff --git a/modules/companies/src/web/software-installations/list/ui/software-installations-panel.tsx b/modules/companies/src/web/software-installations/list/ui/software-installations-panel.tsx index 4449fe8..18aed40 100644 --- a/modules/companies/src/web/software-installations/list/ui/software-installations-panel.tsx +++ b/modules/companies/src/web/software-installations/list/ui/software-installations-panel.tsx @@ -12,6 +12,7 @@ import { EntityStatus, EntityTable, } from "@repo/rdx-ui/components/entity-list"; +import { LayersIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { Checkbox } from "@repo/shadcn-ui/components/checkbox"; import { useId, useState } from "react"; @@ -131,10 +132,12 @@ export function SoftwareInstallationsPanel({ size="sm" variant="outline" > +