Añadir estadísticas y navegación a la vista de resumen de compañía

This commit is contained in:
David Arranz 2026-10-09 21:01:55 +02:00
parent ef4f41493a
commit 5a7960302e
10 changed files with 368 additions and 83 deletions

View File

@ -295,3 +295,9 @@ La cabecera permanece visible. Un formulario modal utiliza una raíz flex con al
`FormSection` agrupa datos relacionados mediante título accesible y un borde discreto. Activos, recursos y plantillas lo utilizan para sus datos principales. Las características y las secciones del editor se distribuyen en dos bloques por fila cuando el contenedor dispone de al menos 900 px; los controles dentro de cada bloque mantienen una o dos columnas según el espacio. Una plantilla sin secciones puede aprovechar hasta tres columnas de características. Observaciones y ayuda ocupan toda la fila. La disposición responde al espacio del modal, no solo al ancho del navegador. `FormSection` agrupa datos relacionados mediante título accesible y un borde discreto. Activos, recursos y plantillas lo utilizan para sus datos principales. Las características y las secciones del editor se distribuyen en dos bloques por fila cuando el contenedor dispone de al menos 900 px; los controles dentro de cada bloque mantienen una o dos columnas según el espacio. Una plantilla sin secciones puede aprovechar hasta tres columnas de características. Observaciones y ayuda ocupan toda la fila. La disposición responde al espacio del modal, no solo al ancho del navegador.
El desplazamiento vertical se conserva cuando el contenido supera la altura disponible; no se ocultan campos ni se reduce artificialmente su tamaño para eliminarlo. No se introducen pestañas ni páginas nuevas sin una necesidad específica del formulario. El desplazamiento vertical se conserva cuando el contenido supera la altura disponible; no se ocultan campos ni se reduce artificialmente su tamaño para eliminarlo. No se introducen pestañas ni páginas nuevas sin una necesidad específica del formulario.
## Resumen de compañía
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.

View File

@ -6,9 +6,10 @@
"routeHelp": "Check the address or return to the dashboard." "routeHelp": "Check the address or return to the dashboard."
}, },
"companyOverview": { "companyOverview": {
"title": "Company overview", "title": "At a glance",
"description": "Status, pending work and alerts for {{name}}.", "description": "Status, pending work and alerts for {{name}}.",
"directory": "Company information" "directory": "People and organization",
"navigation": "Overview"
}, },
"catalogOverview": { "catalogOverview": {
"globalTitle": "Global catalogs", "globalTitle": "Global catalogs",

View File

@ -6,9 +6,10 @@
"routeHelp": "Comprueba la dirección o vuelve al inicio." "routeHelp": "Comprueba la dirección o vuelve al inicio."
}, },
"companyOverview": { "companyOverview": {
"title": "Resumen de la compañía", "title": "De un vistazo",
"description": "Estado, trabajo pendiente y avisos de {{name}}.", "description": "Estado, trabajo pendiente y avisos de {{name}}.",
"directory": "Información de la compañía" "directory": "Equipo y organización",
"navigation": "Resumen"
}, },
"catalogOverview": { "catalogOverview": {
"globalTitle": "Catálogos globales", "globalTitle": "Catálogos globales",

View File

@ -1,5 +1,3 @@
import type { ReactNode } from "react";
import type { CompanyTab } from "../../shared/types"; import type { CompanyTab } from "../../shared/types";
export type CompanyDetailProps = { export type CompanyDetailProps = {
@ -7,7 +5,6 @@ export type CompanyDetailProps = {
csrfToken: string; csrfToken: string;
onBack: () => void; onBack: () => void;
embedded?: boolean; embedded?: boolean;
accountNavigation?: ReactNode;
initialSection?: CompanyTab; initialSection?: CompanyTab;
section?: CompanyTab; section?: CompanyTab;
onSectionChange?: (section: CompanyTab) => void; onSectionChange?: (section: CompanyTab) => void;

View File

@ -9,7 +9,9 @@ import {
LayoutTemplateIcon, LayoutTemplateIcon,
MailIcon, MailIcon,
MapPinIcon, MapPinIcon,
TicketIcon,
UsersIcon, UsersIcon,
WrenchIcon,
} from "@repo/rdx-ui/lib/icons"; } from "@repo/rdx-ui/lib/icons";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -26,7 +28,6 @@ export function CompanyDetail(props: CompanyDetailProps) {
csrfToken, csrfToken,
onBack, onBack,
embedded = false, embedded = false,
accountNavigation,
initialSection, initialSection,
initialEntityId, initialEntityId,
initialCreate, initialCreate,
@ -39,7 +40,7 @@ export function CompanyDetail(props: CompanyDetailProps) {
const { t: accessTranslation } = useTranslation("access"); const { t: accessTranslation } = useTranslation("access");
const layout = embedded const layout = embedded
? { ? {
root: "grid min-h-[calc(100dvh-140px)] grid-cols-[minmax(0,1fr)] grid-rows-[auto_auto_1fr_auto] [grid-template-areas:'heading'_'navigation'_'main'_'account'] shell:min-h-[calc(100dvh-var(--spacing-shell-header))] shell:grid-cols-[240px_minmax(0,1fr)] shell:grid-rows-[auto_1fr_auto] shell:[grid-template-areas:'heading_main'_'navigation_main'_'account_main']", root: "grid min-h-[calc(100dvh-140px)] grid-cols-[minmax(0,1fr)] grid-rows-[auto_auto_1fr] [grid-template-areas:'heading'_'navigation'_'main'] shell:min-h-[calc(100dvh-var(--spacing-shell-header))] shell:grid-cols-[240px_minmax(0,1fr)] shell:grid-rows-[auto_1fr] shell:[grid-template-areas:'heading_main'_'navigation_main']",
heading: heading:
"flex items-center justify-between gap-3 border-b border-border p-3 [grid-area:heading] shell:block shell:px-4 shell:py-5", "flex items-center justify-between gap-3 border-b border-border p-3 [grid-area:heading] shell:block shell:px-4 shell:py-5",
navigation: navigation:
@ -71,11 +72,6 @@ export function CompanyDetail(props: CompanyDetailProps) {
{t("retry")} {t("retry")}
</Button> </Button>
)} )}
{embedded && (
<footer className="border-t border-border p-3 [grid-area:account] shell:px-2.5 shell:py-4">
{accountNavigation}
</footer>
)}
{company && ( {company && (
<> <>
<div className={layout.heading}> <div className={layout.heading}>
@ -116,24 +112,24 @@ export function CompanyDetail(props: CompanyDetailProps) {
<nav aria-label={t("companySections")} className={layout.navigation}> <nav aria-label={t("companySections")} className={layout.navigation}>
{( {(
[ [
["overview", t("companyOverview.title"), HomeIcon], ["overview", t("companyOverview.navigation"), HomeIcon],
["inventory", assetTranslation("inventory"), BoxIcon], ["tickets", t("tickets.title"), TicketIcon],
["resources", resourceTranslation("inventory"), LayersIcon], ["interventions", t("interventions.title"), WrenchIcon],
["people", `${t("people")} (${company.people.length})`, UsersIcon],
["workstations", assetTranslation("workstations"), BriefcaseIcon], ["workstations", assetTranslation("workstations"), BriefcaseIcon],
["profiles", t("profiles.title"), UsersIcon], ["profiles", t("profiles.title"), UsersIcon],
["telework", t("telework.title"), KeyRoundIcon], ["inventory", assetTranslation("inventory"), BoxIcon],
["remote", t("remote.title"), KeyRoundIcon], ["resources", resourceTranslation("inventory"), LayersIcon],
["processes", t("processes.title"), LayoutTemplateIcon],
["intervention-templates", t("interventionTemplates.title"), LayoutTemplateIcon],
["interventions", t("interventions.title"), LayoutTemplateIcon],
["tickets", t("tickets.title"), LayoutTemplateIcon],
["people", `${t("people")} (${company.people.length})`, UsersIcon],
[ [
"departments", "departments",
`${t("departments")} (${company.departments.length})`, `${t("departments")} (${company.departments.length})`,
BriefcaseIcon, BriefcaseIcon,
], ],
["sites", `${t("sites")} (${company.sites.length})`, MapPinIcon], ["sites", `${t("sites")} (${company.sites.length})`, MapPinIcon],
["telework", t("telework.title"), KeyRoundIcon],
["remote", t("remote.title"), KeyRoundIcon],
["processes", t("processes.title"), LayoutTemplateIcon],
["intervention-templates", t("interventionTemplates.title"), LayoutTemplateIcon],
["access", accessTranslation("title"), KeyRoundIcon], ["access", accessTranslation("title"), KeyRoundIcon],
["folders", t("folders.title"), MailIcon], ["folders", t("folders.title"), MailIcon],
["certificates", t("certificates.title"), LayersIcon], ["certificates", t("certificates.title"), LayersIcon],
@ -144,28 +140,42 @@ export function CompanyDetail(props: CompanyDetailProps) {
["assets", assetTranslation("companyTitle"), LayoutTemplateIcon], ["assets", assetTranslation("companyTitle"), LayoutTemplateIcon],
["resource-catalog", resourceTranslation("companyTitle"), LayoutTemplateIcon], ["resource-catalog", resourceTranslation("companyTitle"), LayoutTemplateIcon],
] as const ] as const
).map(([section, label, Icon]) => ( ).map((entry) => {
<Button if (!entry) {
aria-pressed={tab === section} return null;
className={ }
embedded const [section, label, Icon] = entry;
? "h-auto min-h-10 justify-start whitespace-normal text-left text-[13px] leading-[1.4285714286] aria-pressed:bg-accent aria-pressed:hover:bg-accent dark:aria-pressed:hover:bg-accent aria-pressed:text-accent-foreground shell:text-sm" return (
: undefined <div
} className={
key={section} section === "people" || section === "inventory"
{...(props.onSectionChange ? "basis-full border-t border-border pt-2 mt-2 shell:basis-auto"
? { : "contents"
nativeButton: false, }
render: <BrowserLink href={companySectionHref(company.id, section)} />, key={section}
"aria-current": tab === section ? "page" : undefined, >
<Button
aria-pressed={tab === section}
className={
embedded
? "h-auto min-h-10 w-full justify-start whitespace-normal text-left text-[13px] leading-[1.4285714286] aria-pressed:bg-accent aria-pressed:hover:bg-accent dark:aria-pressed:hover:bg-accent aria-pressed:text-accent-foreground shell:text-sm"
: undefined
} }
: { onClick: () => setTab(section) })} {...(props.onSectionChange
variant="ghost" ? {
> nativeButton: false,
<Icon aria-hidden="true" className="size-4 shrink-0" /> render: <BrowserLink href={companySectionHref(company.id, section)} />,
{label} "aria-current": tab === section ? "page" : undefined,
</Button> }
))} : { onClick: () => setTab(section) })}
variant="ghost"
>
<Icon aria-hidden="true" className="size-4 shrink-0" />
{label}
</Button>
</div>
);
})}
</nav> </nav>
<main className={layout.main} key={tab}> <main className={layout.main} key={tab}>

View File

@ -0,0 +1,114 @@
import { BrowserLink } from "@metis/core/client";
import { StatisticCard } from "@repo/rdx-ui/components/statistic-card";
import {
BriefcaseIcon,
ChevronRightIcon,
DatabaseIcon,
LaptopIcon,
MapPinIcon,
TicketIcon,
UsersIcon,
WrenchIcon,
} from "@repo/rdx-ui/lib/icons";
import type { UseQueryResult } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import type { CompanyDTO } from "../../../../common";
import type { DashboardSummaryDTO } from "../../../../common/dashboard";
import type { DashboardNavigation } from "../../../dashboard/shared/helpers";
import { companySectionHref } from "../../../workspace/shared/helpers";
export function CompanyOverviewStatistics({
company,
summary,
onNavigate,
onDetails,
}: {
company: CompanyDTO;
summary: UseQueryResult<DashboardSummaryDTO>;
onNavigate: (destination: DashboardNavigation) => void;
onDetails: (kind: "asset" | "resource") => void;
}) {
const { t } = useTranslation("companies");
const status = summary.isPending
? t("loading")
: summary.isError
? t("dashboard.blockError")
: undefined;
const indicators = [
{
key: "tickets",
label: "openTickets",
Icon: TicketIcon,
tone: "rose",
action: () => onNavigate({ area: "tickets", companyId: company.id, pendingOnly: true }),
},
{
key: "interventions",
label: "pendingInterventions",
Icon: WrenchIcon,
tone: "amber",
action: () => onNavigate({ area: "interventions", companyId: company.id, pendingOnly: true }),
},
{
key: "assets",
label: "registeredAssets",
Icon: LaptopIcon,
tone: "green",
action: () => onDetails("asset"),
},
{
key: "resources",
label: "registeredResources",
Icon: DatabaseIcon,
tone: "violet",
action: () => onDetails("resource"),
},
] as const;
return (
<section aria-label={t("dashboard.indicators")} className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 2xl:grid-cols-4">
{indicators.map(({ key, label, Icon, tone, action }) => (
<StatisticCard
compact
icon={<Icon />}
key={key}
label={t(`dashboard.${label}`)}
onClick={action}
status={status}
tone={tone}
value={summary.isError ? undefined : summary.data?.[key]}
/>
))}
</div>
<nav
aria-label={t("companyOverview.directory")}
className="grid overflow-hidden rounded-xl border bg-card sm:grid-cols-3"
>
{(
[
{ section: "people", label: "people", value: company.people.length, Icon: UsersIcon },
{
section: "departments",
label: "departments",
value: company.departments.length,
Icon: BriefcaseIcon,
},
{ section: "sites", label: "sites", value: company.sites.length, Icon: MapPinIcon },
] as const
).map(({ section, label, value, Icon }) => (
<BrowserLink
className="flex items-center gap-3 border-b px-4 py-4 transition-colors last:border-0 hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring sm:border-b-0 sm:border-r"
href={companySectionHref(company.id, section)}
key={section}
>
<Icon aria-hidden="true" className="size-5 text-muted-foreground" />
<span className="text-sm font-medium">{t(label)}</span>
<strong className="ml-auto text-xl font-semibold tabular-nums">{value}</strong>
<ChevronRightIcon aria-hidden="true" className="size-4 text-muted-foreground" />
</BrowserLink>
))}
</nav>
</section>
);
}

View File

@ -1,11 +1,21 @@
import { navigateTo } from "@metis/core/client"; import { navigateTo } from "@metis/core/client";
import { Button } from "@repo/shadcn-ui/components/button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { CompanyDTO } from "../../../../common"; import type { CompanyDTO } from "../../../../common";
import { DashboardPage } from "../../../dashboard/view/ui/dashboard-page"; import { DashboardQuickCreate } from "../../../dashboard/create";
import { SectionNavigation } from "../../../shared/ui/section-navigation"; import { DashboardDetailsDialog } from "../../../dashboard/detail";
import { companySectionHref, workspaceHref } from "../../../workspace/shared/helpers"; import type { DashboardNavigation } from "../../../dashboard/shared/helpers";
import { KIND_ICONS } from "../../../dashboard/shared/helpers";
import { useDashboardPageController } from "../../../dashboard/view/controllers";
import { DashboardActivityPanel } from "../../../dashboard/view/ui/dashboard-activity-panel";
import { DashboardAttentionPanel } from "../../../dashboard/view/ui/dashboard-attention-panel";
import { DashboardWorkPanel } from "../../../dashboard/view/ui/dashboard-work-panel";
import { workspaceHref } from "../../../workspace/shared/helpers";
import { CompanyOverviewStatistics } from "./company-overview-statistics";
/** Composición propia de compañía; comparte bloques y consultas, no la pantalla Inicio. */
export function CompanyOverview({ export function CompanyOverview({
company, company,
csrfToken, csrfToken,
@ -14,37 +24,78 @@ export function CompanyOverview({
csrfToken: string; csrfToken: string;
}) { }) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const { summary, details, setDetails, quick, setQuick } = useDashboardPageController(company.id);
const onNavigate = (destination: DashboardNavigation) =>
navigateTo(workspaceHref(destination), { create: destination.create === true });
return ( return (
<div className="space-y-6"> <div className="space-y-6" data-slot="company-overview">
<SectionNavigation <header className="space-y-4">
items={[ <div>
{ <h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
href: companySectionHref(company.id, "people"), {t("companyOverview.title")}
title: t("people"), </h1>
value: company.people.length, <p className="mt-1 text-sm text-muted-foreground">
}, {t("companyOverview.description", { name: company.name })}
{ </p>
href: companySectionHref(company.id, "departments"), </div>
title: t("departments"), <nav aria-label={t("dashboard.quickActions")} className="flex flex-wrap gap-2">
value: company.departments.length, {(["ticket", "intervention", "asset", "resource"] as const).map((kind) => {
}, const Icon = KIND_ICONS[kind];
{ return (
href: companySectionHref(company.id, "sites"), <Button
title: t("sites"), key={kind}
value: company.sites.length, onClick={() => setQuick(kind)}
}, size="sm"
]} variant={kind === "ticket" ? "default" : "outline"}
label={t("companyOverview.directory")} >
/> <Icon aria-hidden="true" className="size-4" />
<DashboardPage {t(`dashboard.create.${kind}`)}
companyId={company.id} </Button>
csrfToken={csrfToken} );
description={t("companyOverview.description", { name: company.name })} })}
onNavigate={(destination) => </nav>
navigateTo(workspaceHref(destination), { create: destination.create === true }) </header>
} <CompanyOverviewStatistics
title={t("companyOverview.title")} company={company}
onDetails={(kind) => setDetails({ kind })}
onNavigate={onNavigate}
summary={summary}
/> />
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<div className="min-w-0 space-y-6">
<DashboardWorkPanel
companyId={company.id}
onDetails={() => setDetails({ work: true })}
onNavigate={onNavigate}
/>
<DashboardActivityPanel
companyId={company.id}
onDetails={() => setDetails({ activity: true })}
onNavigate={onNavigate}
/>
</div>
<DashboardAttentionPanel
companyId={company.id}
onDetails={(issue) => setDetails({ issue })}
/>
</div>
{details && (
<DashboardDetailsDialog
companyId={company.id}
details={details}
onClose={() => setDetails(undefined)}
onNavigate={onNavigate}
/>
)}
{quick && (
<DashboardQuickCreate
companyId={company.id}
csrfToken={csrfToken}
kind={quick}
onClose={() => setQuick(undefined)}
onNavigate={onNavigate}
/>
)}
</div> </div>
); );
} }

View File

@ -1,6 +1,8 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { test } from "node:test"; import { test } from "node:test";
import { assetsEnglish, assetsSpanish } from "@metis/assets/client";
import { resourcesEnglish, resourcesSpanish } from "@metis/resources/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createInstance } from "i18next"; import { createInstance } from "i18next";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@ -14,6 +16,7 @@ import { companiesEnglish, companiesSpanish } from "../../shared";
import { SiteForm } from "../../sites"; import { SiteForm } from "../../sites";
import { CatalogOverview } from "../../workspace/catalogs/ui/catalog-overview"; import { CatalogOverview } from "../../workspace/catalogs/ui/catalog-overview";
import { CompanyDetail } from "../detail"; import { CompanyDetail } from "../detail";
import { CompanyOverview } from "../detail/ui/company-overview";
import { CompaniesPage } from "../list"; import { CompaniesPage } from "../list";
import { companyKeys } from "./queries"; import { companyKeys } from "./queries";
@ -28,7 +31,10 @@ async function renderPage(
await i18n.init({ await i18n.init({
lng: language, lng: language,
fallbackLng: "es", fallbackLng: "es",
resources: { es: { companies: companiesSpanish }, en: { companies: companiesEnglish } }, resources: {
es: { companies: companiesSpanish, assets: assetsSpanish, resources: resourcesSpanish },
en: { companies: companiesEnglish, assets: assetsEnglish, resources: resourcesEnglish },
},
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
}); });
const client = new QueryClient({ const client = new QueryClient({
@ -93,7 +99,7 @@ test("el resumen de compañía es la entrada por defecto y ofrece destinos reale
}, },
}) })
); );
assert.ok(markup.includes(language === "es" ? "Resumen de la compañía" : "Company overview")); assert.ok(markup.includes(language === "es" ? "De un vistazo" : "At a glance"));
for (const section of ["people", "departments", "sites"]) { for (const section of ["people", "departments", "sites"]) {
assert.ok(markup.includes(`href="/companies/${companyId}/${section}"`)); assert.ok(markup.includes(`href="/companies/${companyId}/${section}"`));
} }
@ -260,3 +266,91 @@ test("asignar un departamento permite añadir el primero y conserva el selector
); );
assert.equal(edited.includes('aria-label="Nuevo departamento"'), false); assert.equal(edited.includes('aria-label="Nuevo departamento"'), false);
}); });
test("el tablero de compañía tiene composición propia, acciones arriba y métricas limitadas a su ámbito", async () => {
const company = {
id: companyId,
name: "Empresa",
active: true,
people: [],
contacts: [],
departments: [],
assignments: [],
sites: [],
};
for (const language of ["es", "en"]) {
const html = await renderPage(
language,
<CompanyOverview company={company} csrfToken="test" />,
(client) => {
client.setQueryData(dashboardKeys.block("summary"), {
companies: 999,
tickets: 999,
interventions: 999,
assets: 999,
resources: 999,
});
client.setQueryData(dashboardKeys.block("summary", companyId), {
companies: 1,
tickets: 7,
interventions: 3,
assets: 24,
resources: 11,
});
client.setQueryData(dashboardKeys.block("work", companyId), { items: [], total: 0 });
client.setQueryData(dashboardKeys.block("activity", companyId), { items: [], total: 0 });
client.setQueryData(dashboardKeys.block("attention", companyId), { items: [] });
}
);
assert.ok(html.includes('data-slot="company-overview"'));
assert.equal(/>999<\//.test(html), false);
for (const count of [7, 3, 24, 11]) {
assert.match(html, new RegExp(`<strong[^>]*>${count}</strong>`));
}
assert.equal(
html.includes(language === "es" ? "Compañías activas" : "Active companies"),
false
);
assert.equal(
html.includes(language === "es" ? "Resumen por compañías" : "Company summary"),
false
);
assert.equal(html.includes(language === "es" ? "Nueva compañía" : "New company"), false);
assert.ok(
html.indexOf(language === "es" ? "Nuevo ticket" : "New ticket") <
html.indexOf(language === "es" ? "Tickets abiertos" : "Open tickets")
);
for (const section of ["people", "departments", "sites"]) {
assert.ok(html.includes(`href="/companies/${companyId}/${section}"`));
}
}
});
test("el menú de compañía ordena trabajo y personas por grupos sin repetir las acciones de usuario", async () => {
const html = await renderPage(
"es",
<CompanyDetail csrfToken="test" embedded id={companyId} onBack={() => undefined} />,
(client) =>
client.setQueryData(companyKeys.detail(companyId), {
company: {
id: companyId,
name: "Empresa",
active: true,
people: [],
contacts: [],
departments: [],
assignments: [],
sites: [],
},
})
);
const nav = html.match(/<nav aria-label="Información de la compañía"[\s\S]*?<\/nav>/)?.[0];
assert.ok(nav);
assert.ok(nav.indexOf("Resumen") < nav.indexOf("Tickets"));
assert.ok(nav.indexOf("Tickets") < nav.indexOf("Intervenciones"));
assert.ok(nav.indexOf("Intervenciones") < nav.indexOf("Personas"));
assert.ok(nav.indexOf("Personas") < nav.indexOf("Puestos de trabajo"));
assert.ok(nav.indexOf("Puestos de trabajo") < nav.indexOf("Perfiles de usuario"));
assert.equal(html.includes("[grid-area:account]"), false);
assert.equal(html.includes("Mi contraseña"), false);
});

View File

@ -70,7 +70,6 @@ export function CompaniesWorkspace(props: CompaniesWorkspaceProps) {
)} )}
{current.area === "company" ? ( {current.area === "company" ? (
<CompanyDetail <CompanyDetail
accountNavigation={accountNavigation}
csrfToken={csrfToken} csrfToken={csrfToken}
embedded embedded
id={current.id} id={current.id}

View File

@ -23,7 +23,9 @@ export function StatisticCard({
tone = "teal", tone = "teal",
onClick, onClick,
status, status,
compact = false,
}: { }: {
compact?: boolean;
icon: ReactNode; icon: ReactNode;
label: string; label: string;
value?: number | undefined; value?: number | undefined;
@ -33,7 +35,7 @@ export function StatisticCard({
}) { }) {
return ( return (
<button <button
className={`flex min-h-28 items-start gap-3 rounded-xl border border-border p-4 text-left shadow-sm transition-colors hover:border-primary focus-visible:outline-2 focus-visible:outline-ring ${TONES[tone]}`} className={`flex items-start gap-3 rounded-xl border border-border text-left transition-colors hover:border-primary focus-visible:outline-2 focus-visible:outline-ring ${compact ? "min-h-28 p-5" : "min-h-28 p-4 shadow-sm"} ${TONES[tone]}`}
onClick={onClick} onClick={onClick}
type="button" type="button"
> >
@ -44,8 +46,18 @@ export function StatisticCard({
{icon} {icon}
</span> </span>
<span className="min-w-0"> <span className="min-w-0">
<span className="block text-xs font-medium text-foreground">{label}</span> <span
<strong className="mt-2 block text-3xl font-semibold text-foreground"> className={
compact
? "block text-sm font-medium text-foreground"
: "block text-xs font-medium text-foreground"
}
>
{label}
</span>
<strong
className={`mt-2 block font-semibold tabular-nums text-foreground ${compact ? "text-4xl tracking-tight" : "text-3xl"}`}
>
{value ?? "—"} {value ?? "—"}
</strong> </strong>
{status && ( {status && (