diff --git a/TODO.md b/TODO.md
index efd85e0..2f1ebc6 100644
--- a/TODO.md
+++ b/TODO.md
@@ -438,3 +438,13 @@ Quiero que hagas esta reestructuración pero antes pregunta todo lo que necesite
- Quiero que en el navegador web del cliente se muestren rutas para seccion. Por ejemplo: http://192.168.0.2:5173/dashboard, http://192.168.0.2:5173/tickets, http://192.168.0.2:5173/companies/, http://192.168.0.2:5173/companies/xxxx, etc. Analiza qué estructura de ruta es la más adecuada las pantallas de la aplicación y muestrala antes de realizar ningún cambio. Intenta encontar ambiguedades o errores también. Como criterio, guíate por la lógica o por lo más común que se hace en este caso.
+- quiero que el estado plegado/desplegado de la barra lateral izquierda se almacene en el almacenamiento local del navegador para que se conserve entre sesiones.
+
+- revisa el codigo para localizar todos los ficheros en el backend que se refieran a value objetcs de DDD. Debes renombrarlos de esta manera:
+
+email-address.ts => email-address.vo.ts
+unique-id.ts => unique-id.vo.ts
+
+
+
+
diff --git a/apps/web/package.json b/apps/web/package.json
index eae1e60..8275baf 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite --host --clearScreen false",
"typecheck": "tsc -p tsconfig.json",
- "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/app.test.tsx src/i18n.test.ts",
+ "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/app.test.tsx src/i18n.test.ts src/navigation/app-redirect.test.ts",
"build": "vite build",
"preview": "vite preview --host 127.0.0.1"
},
diff --git a/apps/web/src/app.test.tsx b/apps/web/src/app.test.tsx
index 95d3efa..4670389 100644
--- a/apps/web/src/app.test.tsx
+++ b/apps/web/src/app.test.tsx
@@ -143,9 +143,9 @@ test("solo el administrador inicial ve la navegación de gestión de usuarios",
assert.ok(normal.includes("Tu espacio de soporte"));
assert.ok(normal.includes("Mi contraseña"));
assert.ok(normal.includes("Salir"));
- assert.equal(normal.includes(">Usuarios"), false);
+ assert.equal(normal.includes('href="/settings/users"'), false);
const admin = await renderPage("es", { ...snapshot, user: { ...user, isInitialAdmin: true } });
- assert.ok(admin.includes(">Usuarios"));
+ assert.ok(admin.includes('href="/settings/users"'));
});
test("el espacio abre Inicio tras el login y mantiene acceso a las compañías sin formularios visibles", async () => {
diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx
index 5798f78..3e90348 100644
--- a/apps/web/src/app.tsx
+++ b/apps/web/src/app.tsx
@@ -3,6 +3,7 @@ import { IdentityWorkspace, identityApi, identityKeys } from "@metis/identity/cl
import { useQuery } from "@tanstack/react-query";
import { LanguageSwitcher } from "./language-switcher";
+import { useAppNavigation } from "./navigation/use-app-navigation";
export function App() {
const session = useQuery({
@@ -10,6 +11,7 @@ export function App() {
queryFn: identityApi.session,
retry: false,
});
+ useAppNavigation(session.data);
return (
{!session.data?.user && (
diff --git a/apps/web/src/navigation/app-redirect.test.ts b/apps/web/src/navigation/app-redirect.test.ts
new file mode 100644
index 0000000..c1c533c
--- /dev/null
+++ b/apps/web/src/navigation/app-redirect.test.ts
@@ -0,0 +1,53 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+
+import type { SessionResponseDTO } from "@metis/identity/common";
+
+import { appRedirect } from "./app-redirect";
+
+const ANONYMOUS: SessionResponseDTO = {
+ user: null,
+ csrfToken: "a".repeat(64),
+ setupRequired: false,
+};
+const AUTHENTICATED: SessionResponseDTO = {
+ ...ANONYMOUS,
+ user: {
+ id: "019b8838-44af-776d-a406-af74452b6f26",
+ name: "Test",
+ email: "test@example.test",
+ active: true,
+ isInitialAdmin: false,
+ },
+};
+
+test("el acceso restaura el destino solicitado, sin bucles ni redirecciones externas", () => {
+ const destination = "/companies/019b8838-44af-776d-a406-af74452b6f26/people";
+ const login = `/login?${new URLSearchParams({ returnTo: destination })}`;
+ assert.equal(appRedirect(destination, ANONYMOUS), login);
+ assert.equal(appRedirect(login, ANONYMOUS), undefined);
+ assert.equal(appRedirect(login, AUTHENTICATED), destination);
+ for (const returnTo of [
+ "//example.test",
+ "https://example.test",
+ "/\\example.test",
+ "/login",
+ "/setup?returnTo=/login",
+ ]) {
+ assert.equal(
+ appRedirect(`/login?${new URLSearchParams({ returnTo })}`, AUTHENTICATED),
+ "/dashboard"
+ );
+ }
+});
+
+test("setup depende del servidor y las rutas canónicas no conservan barras finales", () => {
+ assert.equal(appRedirect("/login", { ...ANONYMOUS, setupRequired: true }), "/setup");
+ assert.equal(appRedirect("/setup", ANONYMOUS), "/login");
+ assert.equal(appRedirect("/", AUTHENTICATED), "/dashboard");
+ assert.equal(
+ appRedirect("/companies/?activeOnly=true", AUTHENTICATED),
+ "/companies?activeOnly=true"
+ );
+ assert.equal(appRedirect("/catalogs", AUTHENTICATED), undefined);
+});
diff --git a/apps/web/src/navigation/app-redirect.ts b/apps/web/src/navigation/app-redirect.ts
new file mode 100644
index 0000000..7616bc4
--- /dev/null
+++ b/apps/web/src/navigation/app-redirect.ts
@@ -0,0 +1,34 @@
+import { isInternalPath } from "@metis/core/client";
+import type { SessionResponseDTO } from "@metis/identity/common";
+
+function safeReturnTo(value: string | null | undefined): string | undefined {
+ return value && isInternalPath(value) && !/^\/(login|setup)(?:[/?]|$)/u.test(value)
+ ? value
+ : undefined;
+}
+
+function accessHref(target: string, requested: string | null) {
+ const returnTo = safeReturnTo(requested);
+ return `${target}${returnTo && returnTo !== "/" ? `?${new URLSearchParams({ returnTo })}` : ""}`;
+}
+
+export function appRedirect(location: string, session: SessionResponseDTO): string | undefined {
+ const url = new URL(location, "http://metis.invalid");
+ const pathname = url.pathname.replace(/\/+$/u, "") || "/";
+ const accessPage = pathname === "/login" || pathname === "/setup";
+ if (!session.user) {
+ const target = session.setupRequired ? "/setup" : "/login";
+ const requested = accessPage ? url.searchParams.get("returnTo") : location;
+ const destination = accessHref(target, requested);
+ return destination === location ? undefined : destination;
+ }
+ if (accessPage) {
+ const requested = url.searchParams.get("returnTo");
+ return safeReturnTo(requested) ?? "/dashboard";
+ }
+ if (pathname === "/") {
+ return `/dashboard${url.search}`;
+ }
+ const canonical = `${pathname}${url.search}`;
+ return canonical === location ? undefined : canonical;
+}
diff --git a/apps/web/src/navigation/use-app-navigation.ts b/apps/web/src/navigation/use-app-navigation.ts
new file mode 100644
index 0000000..4068e74
--- /dev/null
+++ b/apps/web/src/navigation/use-app-navigation.ts
@@ -0,0 +1,18 @@
+import { navigateTo, useBrowserLocation } from "@metis/core/client";
+import type { SessionResponseDTO } from "@metis/identity/common";
+import { useEffect } from "react";
+
+import { appRedirect } from "./app-redirect";
+
+export function useAppNavigation(session: SessionResponseDTO | undefined) {
+ const location = useBrowserLocation();
+ useEffect(() => {
+ if (!session) {
+ return;
+ }
+ const destination = appRedirect(location, session);
+ if (destination) {
+ navigateTo(destination, { replace: true });
+ }
+ }, [location, session]);
+}
diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md
index b4cdca5..864be3c 100644
--- a/docs/componentes-de-interfaz.md
+++ b/docs/componentes-de-interfaz.md
@@ -17,6 +17,9 @@ El código se genera desde el registro oficial y queda versionado en este reposi
## Convención de navegación
+Los destinos de secciones y fichas existentes siguen [Rutas del cliente](rutas-del-cliente.md).
+Usar enlaces reales para esos destinos y botones para acciones o diálogos locales.
+
Los listados presentan datos, búsqueda y acciones. El botón de alta se coloca en la cabecera, a la derecha. Los formularios se abren en modal como norma general, con anchura adaptable y desplazamiento si el contenido lo requiere. No mostrar formularios junto al listado. Las ediciones se abren mediante una acción explícita. Compañías, personas, departamentos, asignaciones, sedes y usuarios siguen este criterio. Las personas, los departamentos y las asignaciones se crean y editan en modales. El historial se consulta en un diálogo. Catálogos, cuentas, grupos, recursos, permisos y necesidades siguen la misma convención. Los permisos de una necesidad se eligen mediante casillas shadcn/ui.
TanStack Table filtra el listado de compañías por nombre; React Hook Form gestiona los formularios con los contratos Zod compartidos.
@@ -33,6 +36,14 @@ Usar tokens semánticos como bg-background, text-foreground y border-border en v
## Añadir componentes
+`SectionNavigation`, en `modules/companies/src/web/shared/ui`, compone Card de la
+biblioteca y BrowserLink de core para índices de secciones. Recibe una etiqueta
+de navegación y elementos con `href`, título, descripción y valor numérico
+opcionales. No conoce DTO, consultas ni endpoints; sus consumidores aportan los
+textos traducidos y únicamente valores conocidos. Se reutiliza en el resumen de
+compañía y en los índices de catálogos globales y de compañía, con foco visible y
+enlaces que pueden abrirse en otra pestaña. No se modifica el paquete vendor.
+
Para componentes propios de un módulo, consultar primero la
[estructura web por entidades y operaciones](estructura-web-por-entidades-y-operaciones.md)
y los componentes equivalentes existentes. Ubicar cada componente en la entidad y operación
diff --git a/docs/patron-de-listados-de-entidades.md b/docs/patron-de-listados-de-entidades.md
index a03fc5f..493a2dd 100644
--- a/docs/patron-de-listados-de-entidades.md
+++ b/docs/patron-de-listados-de-entidades.md
@@ -50,7 +50,7 @@ Carga inicial y errores se presentan dentro del contenido sin afirmar que no hay
## Inventario de listados y ubicaciones actuales
-La aplicación navega por espacios y secciones en memoria, no mediante rutas URL independientes. Los componentes siguientes se alcanzan desde CompaniesWorkspace, CompanyDetail o IdentityWorkspace.
+La aplicación navega por espacios y secciones mediante rutas URL independientes, según [Rutas del cliente](rutas-del-cliente.md). Los componentes siguientes se componen desde CompaniesWorkspace, CompanyDetail o IdentityWorkspace; altas, ediciones e historiales conservan sus modales.
| Listado / ámbito | Componente y ubicación actual | Aplicación |
| --- | --- | --- |
@@ -102,7 +102,7 @@ La validación debe incluir tipos, estilo, pruebas y compilación, además de in
### Migración realizada
-Se han adaptado las 25 familias de listados del inventario y el sublistado de asignaciones departamentales. Los catálogos y accesos reutilizan sus componentes para las distintas secciones y ámbitos; no son nuevas rutas ni nuevas entidades. El inventario conserva la ficha lateral y su modal móvil. Las fichas, historiales y bloques contextuales de las excepciones mantienen su función.
+Se han adaptado las 25 familias de listados del inventario y el sublistado de asignaciones departamentales. Los catálogos y accesos reutilizan sus componentes para las distintas secciones y ámbitos; las rutas distinguen el ámbito sin crear nuevas entidades. El inventario conserva la ficha lateral y su modal móvil. Las fichas, historiales y bloques contextuales de las excepciones mantienen su función.
Componentes compartidos: EntityListHeader y EntityTable; EntityTableConfiguration para columnas y orden; EntityTableContent para tabla semántica; EntityListPagination, EntityRowActions, EntityAction y EntityStatus para controles. useEntityTable concentra filtros locales, ordenación estable, columnas visibles y paginación. entity-list.ts contiene textos es/en y cálculo de rangos. No se añade selección masiva sin una operación que la consuma.
diff --git a/docs/puesta-en-marcha.md b/docs/puesta-en-marcha.md
index debbb3f..0ddb423 100644
--- a/docs/puesta-en-marcha.md
+++ b/docs/puesta-en-marcha.md
@@ -17,6 +17,11 @@ pnpm db:up genera .env con secretos locales si no existe e inicia MariaDB en un
Abrir http://localhost:5173. La primera apertura permite crear el administrador inicial; las siguientes muestran el acceso. La API escucha en 127.0.0.1:3001 y la base de datos en 127.0.0.1:3307.
+Una vez autenticado, Inicio utiliza `/dashboard` y cada sección tiene URL propia.
+Se pueden abrir o recargar directamente `/tickets`, `/catalogs` o
+`/companies/:companyId`. Consultar [Rutas del cliente](rutas-del-cliente.md) para
+las fichas, los filtros y el requisito de fallback de la SPA en producción.
+
Consultar [Usuarios y acceso](usuarios-y-acceso.md) para la configuración inicial y protección de cuentas. No borrar el volumen para actualizar o reiniciar: contiene usuarios, compañías, contactos y sedes.
Si se cambia PORT, configurar API_PROXY_TARGET al arrancar Vite (por defecto http://127.0.0.1:3001). Si cambia el origen del navegador, actualizar APP_ORIGIN en .env.
diff --git a/docs/rutas-del-cliente.md b/docs/rutas-del-cliente.md
new file mode 100644
index 0000000..59e3865
--- /dev/null
+++ b/docs/rutas-del-cliente.md
@@ -0,0 +1,157 @@
+# Rutas del cliente
+
+La URL del navegador identifica el espacio, la sección y, cuando existe una ficha,
+el registro seleccionado. No cambia los endpoints HTTP de `/api`, los permisos ni
+la persistencia. Los identificadores de compañía y de registros son UUID.
+
+## Destinos generales
+
+| Ruta | Pantalla |
+| --- | --- |
+| `/dashboard` | Inicio y resumen global; admite `companyId` para limitar el ámbito. |
+| `/companies` | Directorio de compañías. |
+| `/companies/:companyId` | Estado y resumen operativo de la compañía. |
+| `/tickets` | Tickets de todas las compañías, con filtros. |
+| `/interventions` | Intervenciones de todas las compañías, con filtros. |
+| `/catalogs` | Índice explicativo de catálogos globales. |
+| `/settings/users` | Gestión de usuarios, reservada al administrador inicial. |
+| `/settings/password` | Cambio de contraseña del usuario conectado. |
+| `/login` | Acceso cuando no hay sesión. |
+| `/setup` | Alta del administrador inicial únicamente cuando el servidor la requiere. |
+
+`/` se normaliza a `/dashboard`. Las barras finales se eliminan conservando la
+consulta. El acceso sin sesión conserva el destino interno en `returnTo`; tras
+autenticar se recupera ese destino. No se aceptan redirecciones a otros orígenes.
+
+## Espacio de compañía
+
+Todas las secciones siguientes cuelgan de `/companies/:companyId`:
+
+```text
+/companies/:companyId → resumen, no inventario
+├── assets
+├── resources
+├── workstations
+├── work-profiles
+├── telework-authorizations
+├── remote-connections
+├── processes
+├── intervention-templates
+├── interventions
+├── tickets
+├── people
+├── departments
+├── sites
+├── access
+├── folder-mappings
+├── certificate-configurations
+├── application-configurations
+├── printer-configurations
+├── mail-configurations
+└── catalogs → índice de catálogos de la compañía
+ ├── assets
+ └── resources
+```
+
+`assets` contiene equipos concretos. `workstations` contiene puestos que agrupan
+activos exclusivos o compartidos; no es un alias de activos. `work-profiles`
+representa el entorno de una persona en un dispositivo. Sus rutas independientes
+respetan las responsabilidades existentes, sin introducir entidades nuevas.
+
+Las fichas existentes de activos, recursos, personas, perfiles, procesos, tickets
+e intervenciones utilizan `//:entityId`. Las de tickets e intervenciones
+se abren en su compañía incluso cuando proceden del listado global. El inventario
+conserva el panel lateral en escritorio y el diálogo de ficha en anchuras menores
+de 1100 px, ambos asociados al UUID de la URL.
+
+Las configuraciones de un perfil tienen rutas contextuales:
+
+```text
+/companies/:companyId/work-profiles/:profileId
+├── folder-mappings
+├── mail-configurations
+├── certificate-configurations
+├── application-configurations
+└── printer-configurations
+```
+
+Los accesos remotos y el teletrabajo de una persona se consultan en sus secciones
+propias con `personId`. Las vistas generales de configuraciones admiten `profileId`.
+No se crean fichas URL para entidades que actualmente solo tienen editor o historial.
+
+## Catálogos globales
+
+```text
+/catalogs
+├── access
+├── assets
+├── resources
+├── brands
+├── intervention-templates
+└── process-templates
+```
+
+El índice ofrece enlaces y explica el ámbito; no inventa estadísticas. Los catálogos
+de compañía representan las variantes de activos y recursos disponibles en ese
+ámbito, no duplicados de las rutas de inventario.
+
+## Filtros y estado temporal
+
+La consulta conserva los filtros que ya están conectados a la URL:
+
+- Dashboard y directorio: `companyId`; directorio también `activeOnly` y `q`.
+- Tickets: `companyId`, `pendingOnly`, `q`, `state`, `priority` y `assignee`.
+- Intervenciones: `companyId`, `pendingOnly`, `q`, `state`, `assignee`, `dateFrom` y `dateTo`.
+- Activos y recursos: `q`.
+- Perfiles y procesos: `q`, con `archived` o `closed`, respectivamente.
+- Catálogos de activos y recursos: `q` y `tab`; `relations` solo en activos.
+- Cuentas y accesos: `q` y `kind`, respetando los tipos disponibles en cada ámbito.
+
+Los booleanos se serializan como `true` o `false`; las fechas como ISO `YYYY-MM-DD`.
+`assignee` admite UUID o `unassigned`. Los estados y prioridades se validan con los
+schemas compartidos del dominio; los UUID, las pestañas y los parámetros repetidos
+o desconocidos se rechazan antes de seleccionar una pantalla. La búsqueda admite
+hasta 200 caracteres. Un `companyId` de consulta no puede contradecir la compañía
+del pathname. Esta validación del navegador no sustituye la autorización del servidor.
+
+Cambiar filtros reemplaza la entrada actual del historial; abrir otra sección o
+ficha añade una entrada. Atrás y Adelante recuperan los destinos y filtros. Cerrar
+una ficha vuelve al destino interno anterior; si se abrió directamente, sustituye
+la entrada por su listado o por el contexto del perfil correspondiente.
+
+Altas, ediciones, confirmaciones, historiales y detalles auxiliares del dashboard
+siguen siendo diálogos locales. No se añaden rutas `/new`, `/edit` ni parámetros
+`view` sin una necesidad concreta. El alta rápida que cambia de sección lleva una
+señal temporal en `history.state`, consumida cuando monta la pantalla que abre el
+formulario, sin introducirla en la URL compartida. Los restantes filtros locales,
+la paginación, ordenación y preferencias de columnas mantienen su estado existente.
+
+## Implementación y futuros destinos
+
+- `@metis/core/client` centraliza la History API, suscripciones con cleanup,
+ enlaces internos, búsqueda y selección de fichas. No se añade una dependencia
+ de routing externa.
+- `workspace/shared/helpers/workspace-routes.ts` es la fuente de rutas de compañía,
+ construcción de destinos y validación del espacio. No repartir condiciones de
+ pathname entre componentes de presentación.
+- `apps/web/src/navigation` coordina los destinos según la sesión. El servidor
+ mantiene su autenticación, autorización y validación HTTP existentes.
+- `BrowserLink` produce enlaces reales y conserva abrir en otra pestaña, clic
+ modificado y navegación por teclado. Las secciones activas indican `aria-current`.
+- Un UUID válido ausente en el ámbito muestra ausencia explícita: no selecciona
+ otro registro silenciosamente. Rutas mal formadas y destinos desconocidos tienen
+ estados diferenciados y acceso de regreso a Inicio.
+
+Para añadir una sección, actualizar los contratos y el mapa del espacio, revisar sus
+consumidores y probar construcción, acceso directo, recarga, historial y casos
+inválidos. No cambiar el modelo de dominio únicamente para reproducir una jerarquía URL.
+La organización de ficheros sigue el [estándar web](estructura-web-por-entidades-y-operaciones.md).
+
+## Servicio de la SPA
+
+Vite en desarrollo y en preview entrega la SPA para estas rutas. Un servidor de
+producción debe servir `index.html` para los destinos del cliente y mantener el
+tratamiento propio de `/api` y de los archivos estáticos. Sin ese fallback, abrir
+o recargar una ficha dará un 404 del alojamiento aunque la navegación interna funcione.
+La API compilada todavía no sirve el frontend estático: no se ha cambiado su
+despliegue ni añadido un fallback a Express en esta adaptación.
diff --git a/modules/assets/package.json b/modules/assets/package.json
index 30af365..4bf87d7 100644
--- a/modules/assets/package.json
+++ b/modules/assets/package.json
@@ -13,6 +13,7 @@
"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"
},
"dependencies": {
+ "@metis/core": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
diff --git a/modules/assets/src/web/assets/list/controllers/use-compact-inventory.ts b/modules/assets/src/web/assets/list/controllers/use-compact-inventory.ts
new file mode 100644
index 0000000..e561019
--- /dev/null
+++ b/modules/assets/src/web/assets/list/controllers/use-compact-inventory.ts
@@ -0,0 +1,15 @@
+import { useSyncExternalStore } from "react";
+
+const COMPACT_INVENTORY = "(max-width: 1099px)";
+function subscribe(listener: () => void) {
+ const media = window.matchMedia(COMPACT_INVENTORY);
+ media.addEventListener("change", listener);
+ return () => media.removeEventListener("change", listener);
+}
+export function useCompactInventory() {
+ return useSyncExternalStore(
+ subscribe,
+ () => window.matchMedia(COMPACT_INVENTORY).matches,
+ () => false
+ );
+}
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 98c3f8b..d2c2ed8 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,3 +1,4 @@
+import { useBrowserSearchParams, useEntityNavigation } from "@metis/core/client";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -7,6 +8,8 @@ import { inventoryKeys } from "../../../shared/queries";
import { getAssetInventory } from "../api";
import type { AssetInventoryPageProps } from "../types";
+import { useCompactInventory } from "./use-compact-inventory";
+
export function useAssetInventoryPageController({
companyId,
lookups,
@@ -22,13 +25,30 @@ export function useAssetInventoryPageController({
queryKey: catalogKeys.scope(companyId),
queryFn: () => catalogApi.get(companyId),
});
- const [search, setSearch] = useState("");
+ const { searchParams, setSearchParams } = useBrowserSearchParams();
+ const search = searchParams.get("q") ?? "";
+ const setSearch = (value: string) =>
+ setSearchParams((params) => {
+ if (value) {
+ params.set("q", value);
+ } else {
+ params.delete("q");
+ }
+ });
const [editor, setEditor] = useState(initialCreate ? "new" : undefined);
- const [viewId, setViewId] = useState();
- const [selectedId, setSelectedId] = useState(initialId);
+ const compact = useCompactInventory();
+ const [localViewId, setLocalViewId] = useState();
+ const {
+ selectedId,
+ selectEntity: setSelectedId,
+ entityHref,
+ routed,
+ } = useEntityNavigation(`/companies/${companyId}/assets`, initialId);
+ const viewId = compact && routed ? selectedId : localViewId;
+ const setViewId = compact && routed ? setSelectedId : setLocalViewId;
const [detailOpen, setDetailOpen] = useState(true);
const assets = query.data?.assets ?? [];
- const selected = assets.find((asset) => asset.id === selectedId) ?? assets[0];
+ const selected = selectedId ? assets.find((asset) => asset.id === selectedId) : assets[0];
const edited = assets.find((asset) => asset.id === editor);
const viewed = assets.find((asset) => asset.id === viewId);
const filtered = assets.filter((asset) => {
@@ -44,6 +64,8 @@ export function useAssetInventoryPageController({
});
return {
query,
+ entityHref,
+ missingView: Boolean(selectedId && query.isSuccess && !selected),
catalog,
search,
setSearch,
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 7dbfe1b..76734b3 100644
--- a/modules/assets/src/web/assets/list/ui/inventory-page.tsx
+++ b/modules/assets/src/web/assets/list/ui/inventory-page.tsx
@@ -1,3 +1,4 @@
+import { BrowserLink, EntityNotFound } from "@metis/core/client";
import { Button } from "@repo/shadcn-ui/components/button";
import {
EntityAction,
@@ -37,8 +38,13 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
edited,
viewed,
filtered,
+ missingView,
+ entityHref,
} = useAssetInventoryPageController(props);
const { t, i18n } = useTranslation("assets");
+ if (missingView) {
+ return ;
+ }
return (