From 0f1ad8511a600b60c9dee17e2bb6359a7dd85f65 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 12:12:53 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20mejorar=20la=20interacci=C3=B3n=20y=20v?= =?UTF-8?q?isualizaci=C3=B3n=20del=20inventario=20de=20activos=20con=20res?= =?UTF-8?q?=C3=BAmenes=20de=20caracter=C3=ADsticas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/componentes-de-interfaz.md | 13 +++ .../detail/ui/asset-selection-panel.tsx | 55 +++++------- .../use-inventory-page-controller.ts | 38 +++++++-- .../src/web/assets/list/ui/inventory-page.tsx | 6 +- .../helpers/asset-characteristics-summary.ts | 11 +++ .../src/web/assets/shared/inventory.test.tsx | 85 +++++++++++++++++++ .../src/web/assets/shared/ui/asset-list.tsx | 28 ++++-- .../rdx-ui/src/components/entity-list.tsx | 3 + .../src/components/entity-table-content.tsx | 23 ++++- .../rdx-ui/src/components/selection-panel.tsx | 42 +++++++++ .../rdx-ui/src/hooks/use-compact-detail.ts | 8 +- .../rdx-ui/src/lib/characteristics-summary.ts | 21 +++++ packages/shadcn-ui/src/components/table.tsx | 2 +- 13 files changed, 278 insertions(+), 57 deletions(-) create mode 100644 modules/assets/src/web/assets/shared/helpers/asset-characteristics-summary.ts create mode 100644 packages/rdx-ui/src/components/selection-panel.tsx rename modules/assets/src/web/assets/list/controllers/use-compact-inventory.ts => packages/rdx-ui/src/hooks/use-compact-detail.ts (57%) create mode 100644 packages/rdx-ui/src/lib/characteristics-summary.ts diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index e2d0f07..3e5d5b6 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -439,6 +439,19 @@ El nuevo puesto se organiza en sede/responsable, activos, ordenador principal y `AssetList`, en assets/shared/ui, concentra las pestañas por UUID de plantilla (incluidas vacías/inactivas y especificaciones históricas), búsqueda rápida por nombre/plantilla/estado/sede y las columnas Nombre, Plantilla, Estado y Sede. Tanto inventario como selector lo componen con `EntityTable`. El inventario aporta enlaces, acciones y resaltado; el selector aporta checkboxes o radios de shadcn/ui en la columna fija de acciones, accesible también al desplazar horizontalmente la tabla. Se reutilizan ordenación, columnas configurables, rangos y paginación existentes. Este componente conoce activos y permanece en su módulo; `EntityTable`, `FormDialog`, `FormBody`, `FormActions` y `ButtonGroup` siguen siendo los patrones genéricos independientes del dominio. +El inventario no selecciona ningún activo al entrar en el listado. Pulsar el nombre o una zona +sin controles de la fila abre su ficha lateral (en móvil, el diálogo existente), conservando +la pestaña y la búsqueda en la URL. El nombre mantiene su enlace para navegación por teclado; +los menús y demás controles conservan su acción propia. Una URL de activo concreto abre su ficha. +`EntityTable.onRowClick` permite esta interacción desde el consumidor sin conocer rutas o entidades. + +Bajo el nombre, inventario y selector muestran un resumen automático: los cuatro primeros +campos con valor de la especificación capturada del activo, en el orden de su plantilla. +Incluye etiqueta y unidad, conserva cero y valores booleanos traducidos y omite vacíos y texto +largo. Cada tipo utiliza así sus propios campos, incluidos los personalizados, sin reglas +basadas en nombres ni cambios de persistencia. Si no hay características, se muestra solo el +nombre. El resumen ocupa como máximo dos líneas y su texto completo está disponible en `title`. + El selector se aplica a activos del puesto (adición múltiple), ordenador principal (único entre miembros), extremo de relaciones, impresora por perfil, origen/destino de conexiones remotas y activos afectados de tickets (adición múltiple). Cada caso conserva exclusiones, referencias fijas y altas rápidas. Añadir varios activos a un puesto es una sola actualización de composición; solo se propone automáticamente el ordenador cuando la composición resultante contiene un único activo. ## Componentes archivados 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 8cfbb77..d36da3c 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,5 @@ import { DetailSection } from "@repo/rdx-ui/components/detail-section"; -import { XIcon } from "@repo/rdx-ui/lib/icons"; +import { SelectionPanel } from "@repo/rdx-ui/components/selection-panel"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -43,41 +43,24 @@ export function AssetSelectionPanel({ } return ( - + ); } 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 201ce36..4be9d4f 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 @@ -1,4 +1,10 @@ -import { useBrowserSearchParams, useEntityNavigation, useListQuery } from "@metis/core/client"; +import { useCompactDetail } from "@repo/rdx-ui/hooks/use-compact-detail"; +import { + navigateTo, + useBrowserSearchParams, + useEntityNavigation, + useListQuery, +} from "@metis/core/client"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; @@ -8,7 +14,7 @@ import { removeAsset } from "../../shared/api"; import { getAssetInventory } from "../api"; import type { AssetInventoryPageProps } from "../types"; -import { useCompactInventory } from "./use-compact-inventory"; + export function useAssetInventoryPageController({ companyId, @@ -65,19 +71,38 @@ export function useAssetInventoryPageController({ setDeleteId(undefined); }, }); - const compact = useCompactInventory(); + const compact = useCompactDetail(); const [localViewId, setLocalViewId] = useState(); const { selectedId, selectEntity: setSelectedId, - entityHref, + entityHref: baseEntityHref, routed, } = useEntityNavigation(`/companies/${companyId}/assets`, initialId); + const [detailOpen, setDetailOpen] = useState(true); const viewId = compact && routed ? selectedId : localViewId; const setViewId = compact && routed ? setSelectedId : setLocalViewId; - const [detailOpen, setDetailOpen] = useState(true); + + const entityHref = (id: string) => { + const search = searchParams.toString(); + + return `${baseEntityHref(id)}${search ? `?${search}` : ""}`; + }; + + const selectAsset = (id: string) => { + setDetailOpen(true); + if (routed) { + navigateTo(entityHref(id)); + } else { + setSelectedId(id); + if (compact) { + setLocalViewId(id); + } + } + }; + const assets = query.data?.assets ?? []; - const selected = selectedId ? assets.find((asset) => asset.id === selectedId) : assets[0]; + const selected = selectedId ? assets.find((asset) => asset.id === selectedId) : undefined; const edited = assets.find((asset) => asset.id === editor); const viewed = assets.find((asset) => asset.id === viewId); const deleted = assets.find((asset) => asset.id === deleteId); @@ -85,6 +110,7 @@ export function useAssetInventoryPageController({ return { query, entityHref, + selectAsset, missingView: Boolean(selectedId && query.isSuccess && !selected), catalog, templates, 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 e47c3d3..e582a74 100644 --- a/modules/assets/src/web/assets/list/ui/inventory-page.tsx +++ b/modules/assets/src/web/assets/list/ui/inventory-page.tsx @@ -46,6 +46,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { viewed, missingView, entityHref, + selectAsset, } = useAssetInventoryPageController(props); const { t, i18n } = useTranslation("assets"); @@ -65,7 +66,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { <> {" "}
- {!detailOpen && ( + {!detailOpen && selected && ( +
+ {summary} + +
{actions}
+ {children} + + ); +} diff --git a/modules/assets/src/web/assets/list/controllers/use-compact-inventory.ts b/packages/rdx-ui/src/hooks/use-compact-detail.ts similarity index 57% rename from modules/assets/src/web/assets/list/controllers/use-compact-inventory.ts rename to packages/rdx-ui/src/hooks/use-compact-detail.ts index 88a8cfd..6fe6ccf 100644 --- a/modules/assets/src/web/assets/list/controllers/use-compact-inventory.ts +++ b/packages/rdx-ui/src/hooks/use-compact-detail.ts @@ -1,19 +1,19 @@ import { useSyncExternalStore } from "react"; -const COMPACT_INVENTORY = "(max-width: 1099px)"; +const COMPACT_DETAIL = "(max-width: 1099px)"; function subscribe(listener: () => void) { - const media = window.matchMedia(COMPACT_INVENTORY); + const media = window.matchMedia(COMPACT_DETAIL); media.addEventListener("change", listener); return () => media.removeEventListener("change", listener); } -export function useCompactInventory() { +export function useCompactDetail() { return useSyncExternalStore( subscribe, - () => window.matchMedia(COMPACT_INVENTORY).matches, + () => window.matchMedia(COMPACT_DETAIL).matches, () => false ); } diff --git a/packages/rdx-ui/src/lib/characteristics-summary.ts b/packages/rdx-ui/src/lib/characteristics-summary.ts new file mode 100644 index 0000000..ee44af1 --- /dev/null +++ b/packages/rdx-ui/src/lib/characteristics-summary.ts @@ -0,0 +1,21 @@ +/** Resume valores en el orden de los campos sin conocer entidades ni catálogos. */ +export function characteristicsSummary( + fields: readonly { fieldId: string; type: string; unit: string }[], + values: Readonly>, + labels: { yes: string; no: string } +): string { + return fields + .filter((field) => field.type !== "multiline") + .flatMap((field) => { + const value = values[field.fieldId]; + + if (value === undefined || (typeof value === "string" && !value.trim())) { + return []; + } + const text = typeof value === "boolean" ? (value ? labels.yes : labels.no) : String(value).trim(); + + return [`${text}${field.unit ? ` ${field.unit}` : ""}`]; + }) + .slice(0, 4) + .join(" · "); +} diff --git a/packages/shadcn-ui/src/components/table.tsx b/packages/shadcn-ui/src/components/table.tsx index d0baee6..8b43886 100644 --- a/packages/shadcn-ui/src/components/table.tsx +++ b/packages/shadcn-ui/src/components/table.tsx @@ -54,7 +54,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {