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">) {