From 0ceeb9658cc85c913a05bebf96a189424a754cea Mon Sep 17 00:00:00 2001 From: David Date: Sun, 25 Oct 2026 10:10:42 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20implementar=20pesta=C3=B1as=20de=20info?= =?UTF-8?q?rmaci=C3=B3n=20e=20historial=20en=20la=20ficha=20del=20puesto?= =?UTF-8?q?=20de=20trabajo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/componentes-de-interfaz.md | 9 ++ docs/workplace-dashboard-implementacion.md | 26 ++++ .../use-workstation-detail-controller.ts | 6 +- .../detail/ui/workstation-detail-page.tsx | 121 +++++++++++------- .../detail/ui/workstation-detail.test.tsx | 13 +- scripts/workstation-dashboard-ui.test.py | 25 +++- 6 files changed, 142 insertions(+), 58 deletions(-) diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index a4b40a40..3ce18527 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -1518,3 +1518,12 @@ software y perfil. `ApplicationResourceUsage`, compuesto desde Companies, consul a demanda las APIs operativas existentes de la compañía y conserva sus query keys. Resources no depende de Companies. Véanse contratos, evidencia y límites en [R2.6](architecture/resources-global-scope/17-r2-6-coexistencia-duplicados.md). + +### Pestañas de información e historial del puesto + +La ficha actual conserva sus paneles en Información e integra `WorkstationHistory` +en Historial, con `Tabs`, `TabsContent` y `ScrollableTabsList`, siguiendo el contrato +genérico descrito arriba. La cabecera común mantiene identidad y acciones de edición. +Información permanece montada para conservar las configuraciones abiertas; el acceso +«Ver historial completo» activa Historial y sitúa el foco en su pestaña. La composición +se detalla en [la implementación del dashboard](workplace-dashboard-implementacion.md#pestañas-de-la-ficha-del-puesto). diff --git a/docs/workplace-dashboard-implementacion.md b/docs/workplace-dashboard-implementacion.md index d9f48972..0007e7c7 100644 --- a/docs/workplace-dashboard-implementacion.md +++ b/docs/workplace-dashboard-implementacion.md @@ -1001,3 +1001,29 @@ No se modificaron vendor ni los ocho documentos históricos de auditoría. El preparador autenticado y sus servicios temporales se cerraron al terminar; el archivo de sesión se retiró. Se conservaron únicamente las evidencias sanitizadas en la ruta estable indicada. No se instalaron dependencias ni se cambió configuración global. + +## Pestañas de la ficha del puesto + +La ficha mantiene la identidad y las acciones del puesto en una cabecera común y +organiza el contenido mediante `Tabs` y `ScrollableTabsList`, siguiendo la ficha de activos: + +- **Información** es la pestaña inicial y conserva los paneles actuales. Su contenido + permanece montado al alternar para mantener el contexto de las configuraciones abiertas. +- **Historial** integra `WorkstationHistory` con revisiones de más reciente a más antigua, + autor y valores de cada momento. No abre un diálogo desde esta ficha. + +La cabecera ya no incluye el botón «Ver historial». «Ver historial completo», en el +resumen del último cambio, selecciona Historial y lleva el foco a su pestaña. +El diálogo de historial sigue disponible en el listado de puestos. + +Validación de este cambio: typecheck de `@metis/companies`, Biome sobre el detalle, +ESLint sobre los tres ficheros TypeScript modificados y las 26 pruebas de +`workstation-detail.test.tsx`, correctos. La prueba MCP +`WorkstationDashboardTests.test_03_history_edit_software_and_navigation` pasó a +1440×900, 390×844 y 320×640: pestañas con flechas y Enter, foco desde el resumen, +conservación de configuración desplegada, ausencia de diálogo de historial, +sin desbordamiento horizontal del historial y regresión de edición/software/navegación. +Se inspeccionaron las seis capturas de ambas pestañas. Las fixtures desechables +contenían 15 compañías y, en la principal, 59 personas, 135 activos, 73 recursos +y 64 perfiles. No se repitieron las suites globales ni una auditoría completa +de estados de red o accesibilidad. diff --git a/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts b/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts index 7b4ac7c6..40f7181c 100644 --- a/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts +++ b/modules/companies/src/web/workstations/detail/controllers/use-workstation-detail-controller.ts @@ -37,7 +37,7 @@ export function useWorkstationDetailController(company: CompanyDTO, workstationI enabled: Boolean(computer), }); const configurationQueries = useWorkstationConfigurationQueries(company.id, Boolean(computer)); - const [viewingHistory, setViewingHistory] = useState(false); + const [activeTab, setActiveTab] = useState<"information" | "history">("information"); const dashboard = workstation ? workstationDashboard( company, @@ -83,8 +83,8 @@ export function useWorkstationDetailController(company: CompanyDTO, workstationI selectedProfile, expandedProfile, profileExpansion, - viewingHistory, - setViewingHistory, + activeTab, + setActiveTab, pendingCount, creatingProfile, setCreatingProfile, diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx index a29ea51a..eeeaca87 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx @@ -1,18 +1,14 @@ import { WorkstationActions, WorkstationEditorDialog, - WorkstationHistoryDialog, + WorkstationHistory, WorkstationNameEditor, } from "@metis/assets/client"; import { BrowserLink, returnToList, useBrowserSearchParams } from "@metis/core/client"; +import { DetailPanel } from "@repo/rdx-ui/components/detail-panel"; import { EntityStatus } from "@repo/rdx-ui/components/entity-list"; -import { - Building2Icon, - ChevronLeftIcon, - ClockIcon, - MapPinIcon, - PersonIcon, -} from "@repo/rdx-ui/lib/icons"; +import { ScrollableTabsList } from "@repo/rdx-ui/components/scrollable-tabs-list"; +import { ChevronLeftIcon, ClockIcon, MapPinIcon } from "@repo/rdx-ui/lib/icons"; import { Breadcrumb, BreadcrumbItem, @@ -21,12 +17,13 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@repo/shadcn-ui/components/breadcrumb"; -import { Button } from "@repo/shadcn-ui/components/button"; +import { Tabs, TabsContent } from "@repo/shadcn-ui/components/tabs"; import type { ComponentProps, MouseEvent } from "react"; import { useRef } from "react"; import { useTranslation } from "react-i18next"; import type { CompanyDTO } from "../../../../common"; +import { PersonAvatar } from "../../../people/shared"; import { WorkProfileEditorDialog } from "../../../work-profiles/shared"; import { useWorkstationDetailController } from "../controllers/use-workstation-detail-controller"; @@ -44,13 +41,13 @@ export function WorkstationDetailPage({ workstationId: string; lookups: ComponentProps["lookups"]; }) { - const { t } = useTranslation("assets"); + const { t, i18n } = useTranslation("assets"); const c = useWorkstationDetailController(company, workstationId); const editReturn = useRef(null); const editFromEmpty = useRef(false); const editSaved = useRef(false); const headerEdit = useRef(null); - const historyReturn = useRef(null); + const tabsRef = useRef(null); const { workstation, inventory, dashboard } = c; const root = `/companies/${company.id}`; const { searchParams } = useBrowserSearchParams(); @@ -73,9 +70,10 @@ export function WorkstationDetailPage({ }; const onHistory = () => { - historyReturn.current = - document.activeElement instanceof HTMLElement ? document.activeElement : null; - c.setViewingHistory(true); + c.setActiveTab("history"); + tabsRef.current + ?.querySelector('[role="tab"][data-value="history"]') + ?.focus(); }; return ( @@ -135,21 +133,12 @@ export function WorkstationDetailPage({ onDeleted={() => returnToList(backHref)} onEdit={onEdit} workstation={workstation} - > - - + />
{t(workstation.values.active ? "active" : "inactive")} - - -
- - {c.viewingHistory && ( - historyReturn.current ?? true} - members={(ids) => - ids - .map( - (id) => - inventory.data?.assets.find((asset) => asset.id === id)?.values.name ?? "—" - ) - .join(", ") || "—" + onValueChange={(value) => { + if (value === "information" || value === "history") { + c.setActiveTab(value); } - onClose={() => c.setViewingHistory(false)} - personName={(id) => company.people.find((person) => person.id === id)?.name ?? "—"} - siteName={(id) => company.sites.find((site) => site.id === id)?.name ?? "—"} - workstation={workstation} + }} + ref={tabsRef} + value={c.activeTab} + > + - )} + + + + + } + title={t("workstationHistory")} + > + + ids + .map( + (id) => + inventory.data?.assets.find((asset) => asset.id === id)?.values.name ?? + "—" + ) + .join(", ") || "—" + } + personName={(id) => + company.people.find((person) => person.id === id)?.name ?? "—" + } + siteName={(id) => company.sites.find((site) => site.id === id)?.name ?? "—"} + workstation={workstation} + /> + + + {c.creatingProfile && ( button) .filter((button) => button.includes('role="tab"')); - assert.equal(tabs.length, 0); - assert.ok(html.includes(labels.workstationDashboard.viewHistory)); + assert.equal(tabs.length, 2); + assert.ok(tabs[0]?.includes(labels.workstationDetail.information)); + assert.ok(tabs[0]?.includes('aria-selected="true"')); + assert.ok(tabs[1]?.includes(labels.history)); + assert.ok(tabs[1]?.includes('aria-selected="false"')); + assert.equal(header.includes(labels.workstationDashboard.viewHistory), false); + assert.ok(html.includes(labels.workstationDashboard.fullHistory)); assert.ok(html.includes(labels.workstationDashboard.softwareTitle)); assert.equal(html.includes(`${labels.workstationDashboard.responsibleContext}`), false); assert.ok(html.includes(labels.noWorkstationAssets)); @@ -850,14 +855,18 @@ test("cabecera es/en: responsable largo o ausente, contexto independiente y seis const labels = language === "es" ? assetsSpanish : assetsEnglish; const header = html.slice(html.indexOf("")); + assert.equal(header.includes(`href="/companies/${companyId}"`), false); + assert.equal(header.includes(company.name), false); assert.equal(html.includes(`${labels.workstationDashboard.responsibleContext}`), false); assert.equal([...html.matchAll(/document.activeElement?.textContent.trim());"),"Ver historial") + for width, height in [(1440, 900), (390, 844), (320, 640)]: + self.tool("browser_resize", {"width": width, "height": height}) + self.navigate() + self.assertEqual(self.code("return await page.getByRole('tab').count();"), 2) + self.assertEqual(self.code("return await page.getByRole('tab',{name:'Información',exact:true}).getAttribute('aria-selected');"), "true") + self.assertEqual(self.code("return await page.locator('main header').getByRole('button',{name:'Ver historial',exact:true}).count();"), 0) + if not self.code("return await page.locator('#workstation-configurations').isVisible();"): + self.expand_primary() + self.capture(f"information-tabs-{width}") + self.code("await page.getByRole('tab',{name:'Información',exact:true}).focus();await page.keyboard.press('ArrowRight');await page.keyboard.press('Enter');await page.getByRole('tabpanel',{name:'Historial',exact:true}).waitFor();return true;") + self.assertTrue(self.code("return await page.getByRole('tabpanel',{name:'Historial',exact:true}).getByText('Administración — puesto inicial',{exact:true}).isVisible();")) + self.assertEqual(self.code("return await page.getByRole('dialog').count();"), 0) + self.capture(f"history-tabs-{width}") + self.assertLessEqual(self.code("return await page.evaluate(()=>document.documentElement.scrollWidth);"), width) + self.code("await page.keyboard.press('ArrowLeft');await page.keyboard.press('Enter');await page.getByRole('tabpanel',{name:'Información',exact:true}).waitFor();return true;") + self.assertTrue(self.code("return await page.locator('#workstation-configurations').isVisible();")) + self.code("await page.getByRole('button',{name:'Ver historial completo',exact:true}).click();await page.getByRole('tabpanel',{name:'Historial',exact:true}).waitFor();return true;") + self.assertEqual(self.code("return await page.evaluate(()=>document.activeElement?.textContent.trim());"), "Historial") + self.code("await page.getByRole('tab',{name:'Información',exact:true}).click();return true;") + self.tool("browser_resize", {"width": 1440, "height": 900}) self.code("await page.getByRole('button',{name:'Editar puesto',exact:true}).first().click();await page.getByRole('dialog').waitFor();return true;") self.code("await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});return true;") self.code("await page.getByRole('button',{name:'Gestionar software',exact:true}).click();await page.getByRole('dialog').waitFor();return true;")