From d7eb91ee68cfb87028ce812b5e9092bf4f688d92 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 9 Oct 2026 21:18:40 +0200 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=20secciones=20de=20detalle=20y=20me?= =?UTF-8?q?jorar=20la=20presentaci=C3=B3n=20de=20activos=20y=20recursos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/componentes-de-interfaz.md | 8 ++ modules/assets/src/common/locales/en.json | 5 + modules/assets/src/common/locales/es.json | 5 + .../list/types/asset-relationships-props.ts | 1 + .../list/ui/asset-relationships.tsx | 12 +- .../src/web/assets/detail/ui/asset-detail.tsx | 112 +++++++++++------- .../detail/ui/asset-selection-panel.tsx | 40 ++++--- .../src/web/assets/list/ui/inventory-page.tsx | 2 +- .../src/web/assets/shared/inventory.test.tsx | 79 ++++++++++++ .../list/types/asset-support-props.ts | 1 + .../list/ui/asset-support.tsx | 6 +- .../resources/detail/ui/resource-detail.tsx | 21 ++-- .../rdx-ui/src/components/detail-field.tsx | 13 ++ .../rdx-ui/src/components/detail-section.tsx | 13 ++ .../rdx-ui/src/components/field-sections.tsx | 16 ++- packages/rdx-ui/src/lib/icons.ts | 1 + 16 files changed, 259 insertions(+), 76 deletions(-) create mode 100644 packages/rdx-ui/src/components/detail-field.tsx create mode 100644 packages/rdx-ui/src/components/detail-section.tsx diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index 8c690d4..fefe15f 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -301,3 +301,11 @@ El desplazamiento vertical se conserva cuando el contenido supera la altura disp La pantalla `CompanyOverview` tiene composición independiente de `DashboardPage` (Inicio). Comparte tarjetas, bloques de trabajo/avisos/actividad y consultas existentes, siempre fijando el ámbito al UUID de la compañía. El encabezado «De un vistazo» precede las acciones rápidas (ticket, intervención, activo y recurso), cuatro indicadores operativos y una franja navegable de personas, departamentos y sedes. No muestra compañías activas ni el resumen por compañías. La variante compacta de `StatisticCard` mejora la jerarquía del valor y la etiqueta sin modificar la presentación de Inicio. La navegación de compañía empieza por Resumen, Tickets e Intervenciones; después agrupa Personas, Puestos de trabajo y Perfiles de usuario y finalmente las demás secciones. Los grupos tienen separación visual. Las operaciones de usuario permanecen en el menú superior y dejan de repetirse al pie del menú de compañía. + +## Fichas de consulta y resumen lateral + +Las secciones de consulta evitan recuadros anidados y rellenos laterales: usan títulos compactos y un separador tenue entre grupos. Las agrupaciones de características siguen este mismo criterio en lectura; los formularios conservan su organización propia. + +`DetailSection` ofrece agrupación plegable nativa (`details/summary`), abierta inicialmente, accesible por teclado. `DetailField` devuelve una fila semántica `dt/dd` para incluir en un `dl`: etiqueta y valor se alinean horizontalmente cuando el contenedor dispone de al menos 240 px y se apilan en espacios menores. El consumidor aporta los enlaces; el componente común les aplica cursor de enlace, color semántico y subrayado al pasar el puntero. Los valores ausentes permanecen como texto. + +La ficha lateral de activos utiliza un ancho adaptable entre 320 y 400 px, acciones debajo del título y bloques de identificación, ubicación/asignación, características, observaciones, relaciones y asistencia. Conserva las secciones capturadas de la plantilla, incluyendo la compatibilidad con especificaciones antiguas sin agrupación; no modifica valores ni instantáneas. Las relaciones llevan a destinos existentes: persona y activo relacionado a su ficha; sede, puesto y plantilla a sus listados/catálogos actuales. La vista completa e historial mantienen sus funciones. Las fichas de recursos reutilizan las filas de características. No se crean nuevas fichas laterales ni nuevas rutas. diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index 8f7c8dd..f1c148b 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -303,5 +303,10 @@ "formLayout": { "identity": "Identification and assignment", "template": "Template details" + }, + "detailSections": { + "identification": "Identification", + "assignment": "Location and assignment", + "notes": "Notes" } } diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index 4a1a15b..55efe09 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -303,5 +303,10 @@ "formLayout": { "identity": "Identificación y asignación", "template": "Datos de la plantilla" + }, + "detailSections": { + "identification": "Identificación", + "assignment": "Ubicación y asignación", + "notes": "Observaciones" } } diff --git a/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts b/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts index a77a4ea..44caa4f 100644 --- a/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts +++ b/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts @@ -1,4 +1,5 @@ export type AssetRelationshipsProps = { + compact?: boolean; companyId: string; csrfToken: string; assetId: string; diff --git a/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx b/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx index 2cae0d2..949bd72 100644 --- a/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx +++ b/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx @@ -1,3 +1,4 @@ +import { BrowserLink } from "@metis/core/client"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -8,7 +9,7 @@ import { useAssetRelationshipsController } from "../controllers"; import type { AssetRelationshipsProps } from "../types"; export function AssetRelationships(props: AssetRelationshipsProps) { - const { companyId, csrfToken, assetId } = props; + const { companyId, csrfToken, assetId, compact = false } = props; const { query, adding, setAdding, setEndingId, ending, active, ended, dateOf } = useAssetRelationshipsController(props); const { t } = useTranslation("assets"); @@ -20,7 +21,12 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
  • {source ? link.values.name : link.values.inverseName} ·{" "} - {nameOf(source ? link.values.targetId : link.values.sourceId)} + + {nameOf(source ? link.values.targetId : link.values.sourceId)} +

    {link.values.notes && (

    {link.values.notes}

    @@ -49,7 +55,7 @@ export function AssetRelationships(props: AssetRelationshipsProps) { return (
    -

    {t("relations")}

    + {!compact &&

    {t("relations")}

    } 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 ccf7c33..dc60a55 100644 --- a/modules/assets/src/web/assets/detail/ui/asset-detail.tsx +++ b/modules/assets/src/web/assets/detail/ui/asset-detail.tsx @@ -1,3 +1,6 @@ +import { BrowserLink } from "@metis/core/client"; +import { DetailField } from "@repo/rdx-ui/components/detail-field"; +import { DetailSection } from "@repo/rdx-ui/components/detail-section"; import { FieldSections } from "@repo/rdx-ui/components/field-sections"; import { useTranslation } from "react-i18next"; @@ -22,49 +25,78 @@ export function AssetDetail(props: AssetDetailProps) { ? t(value ? "yes" : "no") : String(value); return ( -
    +
    {!compact &&

    {t("snapshotNotice")}

    } -
    -
    -
    {t("template")}
    -
    {asset.specification.templateName}
    -
    -
    -
    {t("status")}
    -
    {t(asset.values.status)}
    -
    -
    -
    {t("site")}
    -
    {nameOf(asset.values.siteId, lookups.sites)}
    -
    -
    -
    {t("person")}
    -
    {nameOf(asset.values.personId, lookups.people)}
    -
    -
    -
    {t("workstationInAsset")}
    -
    {workstations?.map((item) => item.values.name).join(" · ") || t("noWorkstation")}
    -
    -
    - ( -
    -
    - {field.name} - {field.unit ? ` (${field.unit})` : ""} -
    -
    + +
    + + + {asset.specification.templateName} + + + {t(asset.values.status)} +
    +
    + +
    + + {asset.values.siteId ? ( + + {nameOf(asset.values.siteId, lookups.sites)} + + ) : ( + t("noAssignment") + )} + + + {asset.values.personId ? ( + + {nameOf(asset.values.personId, lookups.people)} + + ) : ( + t("noAssignment") + )} + + + {workstations?.length + ? workstations.map((item) => ( + + {item.values.name} + + )) + : t("noWorkstation")} + +
    +
    + + ( + + {field.name} + {field.unit ? ` (${field.unit})` : ""} + + } + > {display(asset.values.characteristics[field.fieldId])} -
    -
    - )} - sections={asset.specification.sections} - ungroupedLabel={t("sections.ungrouped")} - values - /> + + )} + sections={asset.specification.sections} + ungroupedLabel={t("sections.ungrouped")} + values + /> + {asset.values.notes && ( -

    {asset.values.notes}

    + +

    {asset.values.notes}

    +
    )} {children} {!compact && ( 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 ef9b2e1..d1099c7 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,3 +1,5 @@ +import { DetailSection } from "@repo/rdx-ui/components/detail-section"; +import { XIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -36,31 +38,41 @@ export function AssetSelectionPanel({ return ( ); } 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 88dc2b2..3bf697d 100644 --- a/modules/assets/src/web/assets/list/ui/inventory-page.tsx +++ b/modules/assets/src/web/assets/list/ui/inventory-page.tsx @@ -47,7 +47,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { } return (
    diff --git a/modules/assets/src/web/assets/shared/inventory.test.tsx b/modules/assets/src/web/assets/shared/inventory.test.tsx index 08bb058..649d122 100644 --- a/modules/assets/src/web/assets/shared/inventory.test.tsx +++ b/modules/assets/src/web/assets/shared/inventory.test.tsx @@ -13,6 +13,7 @@ import { buildCatalogResponse } from "../../../api/application/asset-catalog-res import { AssetCatalog } from "../../../api/domain/asset-catalog.aggregate"; import { seedAssetCatalog } from "../../../api/domain/asset-catalog-seed"; import type { RelationshipDTO } from "../../../common"; +import { AssetSchema } from "../../../common/inventory"; import { AssetRelationships } from "../../asset-relationships"; import { catalogKeys } from "../../catalog/shared/api"; import { CatalogRelationForm } from "../../catalog-relations"; @@ -20,6 +21,7 @@ import { CatalogTemplateForm } from "../../catalog-templates"; import { assetsEnglish, assetsSpanish } from "../../shared"; import { inventoryKeys } from "../../shared/queries"; import { WorkstationForm, WorkstationsPage } from "../../workstations"; +import { AssetSelectionPanel } from "../detail/ui/asset-selection-panel"; import { AssetInventoryPage } from "../list"; import { AssetForm } from "./ui"; @@ -218,3 +220,80 @@ test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no assert.ok(!nested.includes(`aria-label="${label}"`)); } }); + +test("la ficha lateral agrupa información y ofrece acciones superiores y enlaces reales", async () => { + const fieldId = UniqueID.generateNewID().toPrimitive(); + const personId = UniqueID.generateNewID().toPrimitive(); + const siteId = UniqueID.generateNewID().toPrimitive(); + const asset = AssetSchema.parse({ + id: UniqueID.generateNewID().toPrimitive(), + companyId, + specification: { + templateId: fieldId, + templateName: "Impresora", + fields: [ + { fieldId, name: "Número de serie", type: "text", unit: "", choices: [], required: false }, + ], + }, + values: { + name: "Impresora de administración", + templateId: fieldId, + siteId, + personId, + status: "in_use", + notes: "Uso compartido", + characteristics: { [fieldId]: "ABC123" }, + }, + history: [ + { + id: fieldId, + actorId: personId, + actorName: "Técnico", + recordedAt: "2026-10-09T10:00:00.000Z", + values: { + name: "Impresora de administración", + templateId: fieldId, + siteId, + personId, + status: "in_use", + notes: "Uso compartido", + characteristics: { [fieldId]: "ABC123" }, + }, + }, + ], + }); + const html = await render( + "es", + undefined} + onEdit={() => undefined} + onView={() => undefined} + open + /> + ); + assert.ok(html.indexOf("Ficha e historial") < html.indexOf("Identificación")); + assert.ok(html.indexOf(">Editar<") < html.indexOf("Identificación")); + for (const group of [ + "Identificación", + "Ubicación y asignación", + "Características", + "Observaciones", + ]) { + assert.ok(html.includes(group)); + } + assert.ok(html.includes(" +
    -

    {t("support.title")}

    + {!compact &&

    {t("support.title")}

    }