Añadir estadísticas y navegación a la vista de resumen de compañía
This commit is contained in:
parent
ef4f41493a
commit
5a7960302e
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
@ -6,9 +6,10 @@
|
||||
"routeHelp": "Check the address or return to the dashboard."
|
||||
},
|
||||
"companyOverview": {
|
||||
"title": "Company overview",
|
||||
"title": "At a glance",
|
||||
"description": "Status, pending work and alerts for {{name}}.",
|
||||
"directory": "Company information"
|
||||
"directory": "People and organization",
|
||||
"navigation": "Overview"
|
||||
},
|
||||
"catalogOverview": {
|
||||
"globalTitle": "Global catalogs",
|
||||
|
||||
@ -6,9 +6,10 @@
|
||||
"routeHelp": "Comprueba la dirección o vuelve al inicio."
|
||||
},
|
||||
"companyOverview": {
|
||||
"title": "Resumen de la compañía",
|
||||
"title": "De un vistazo",
|
||||
"description": "Estado, trabajo pendiente y avisos de {{name}}.",
|
||||
"directory": "Información de la compañía"
|
||||
"directory": "Equipo y organización",
|
||||
"navigation": "Resumen"
|
||||
},
|
||||
"catalogOverview": {
|
||||
"globalTitle": "Catálogos globales",
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { CompanyTab } from "../../shared/types";
|
||||
|
||||
export type CompanyDetailProps = {
|
||||
@ -7,7 +5,6 @@ export type CompanyDetailProps = {
|
||||
csrfToken: string;
|
||||
onBack: () => void;
|
||||
embedded?: boolean;
|
||||
accountNavigation?: ReactNode;
|
||||
initialSection?: CompanyTab;
|
||||
section?: CompanyTab;
|
||||
onSectionChange?: (section: CompanyTab) => void;
|
||||
|
||||
@ -9,7 +9,9 @@ import {
|
||||
LayoutTemplateIcon,
|
||||
MailIcon,
|
||||
MapPinIcon,
|
||||
TicketIcon,
|
||||
UsersIcon,
|
||||
WrenchIcon,
|
||||
} from "@repo/rdx-ui/lib/icons";
|
||||
import { Button } from "@repo/shadcn-ui/components/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -26,7 +28,6 @@ export function CompanyDetail(props: CompanyDetailProps) {
|
||||
csrfToken,
|
||||
onBack,
|
||||
embedded = false,
|
||||
accountNavigation,
|
||||
initialSection,
|
||||
initialEntityId,
|
||||
initialCreate,
|
||||
@ -39,7 +40,7 @@ export function CompanyDetail(props: CompanyDetailProps) {
|
||||
const { t: accessTranslation } = useTranslation("access");
|
||||
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:
|
||||
"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:
|
||||
@ -71,11 +72,6 @@ export function CompanyDetail(props: CompanyDetailProps) {
|
||||
{t("retry")}
|
||||
</Button>
|
||||
)}
|
||||
{embedded && (
|
||||
<footer className="border-t border-border p-3 [grid-area:account] shell:px-2.5 shell:py-4">
|
||||
{accountNavigation}
|
||||
</footer>
|
||||
)}
|
||||
{company && (
|
||||
<>
|
||||
<div className={layout.heading}>
|
||||
@ -116,24 +112,24 @@ export function CompanyDetail(props: CompanyDetailProps) {
|
||||
<nav aria-label={t("companySections")} className={layout.navigation}>
|
||||
{(
|
||||
[
|
||||
["overview", t("companyOverview.title"), HomeIcon],
|
||||
["inventory", assetTranslation("inventory"), BoxIcon],
|
||||
["resources", resourceTranslation("inventory"), LayersIcon],
|
||||
["overview", t("companyOverview.navigation"), HomeIcon],
|
||||
["tickets", t("tickets.title"), TicketIcon],
|
||||
["interventions", t("interventions.title"), WrenchIcon],
|
||||
["people", `${t("people")} (${company.people.length})`, UsersIcon],
|
||||
["workstations", assetTranslation("workstations"), BriefcaseIcon],
|
||||
["profiles", t("profiles.title"), UsersIcon],
|
||||
["telework", t("telework.title"), KeyRoundIcon],
|
||||
["remote", t("remote.title"), KeyRoundIcon],
|
||||
["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],
|
||||
["inventory", assetTranslation("inventory"), BoxIcon],
|
||||
["resources", resourceTranslation("inventory"), LayersIcon],
|
||||
[
|
||||
"departments",
|
||||
`${t("departments")} (${company.departments.length})`,
|
||||
BriefcaseIcon,
|
||||
],
|
||||
["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],
|
||||
["folders", t("folders.title"), MailIcon],
|
||||
["certificates", t("certificates.title"), LayersIcon],
|
||||
@ -144,28 +140,42 @@ export function CompanyDetail(props: CompanyDetailProps) {
|
||||
["assets", assetTranslation("companyTitle"), LayoutTemplateIcon],
|
||||
["resource-catalog", resourceTranslation("companyTitle"), LayoutTemplateIcon],
|
||||
] as const
|
||||
).map(([section, label, Icon]) => (
|
||||
<Button
|
||||
aria-pressed={tab === section}
|
||||
className={
|
||||
embedded
|
||||
? "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"
|
||||
: undefined
|
||||
}
|
||||
key={section}
|
||||
{...(props.onSectionChange
|
||||
? {
|
||||
nativeButton: false,
|
||||
render: <BrowserLink href={companySectionHref(company.id, section)} />,
|
||||
"aria-current": tab === section ? "page" : undefined,
|
||||
).map((entry) => {
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
const [section, label, Icon] = entry;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
section === "people" || section === "inventory"
|
||||
? "basis-full border-t border-border pt-2 mt-2 shell:basis-auto"
|
||||
: "contents"
|
||||
}
|
||||
key={section}
|
||||
>
|
||||
<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) })}
|
||||
variant="ghost"
|
||||
>
|
||||
<Icon aria-hidden="true" className="size-4 shrink-0" />
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
{...(props.onSectionChange
|
||||
? {
|
||||
nativeButton: false,
|
||||
render: <BrowserLink href={companySectionHref(company.id, section)} />,
|
||||
"aria-current": tab === section ? "page" : undefined,
|
||||
}
|
||||
: { onClick: () => setTab(section) })}
|
||||
variant="ghost"
|
||||
>
|
||||
<Icon aria-hidden="true" className="size-4 shrink-0" />
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<main className={layout.main} key={tab}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,11 +1,21 @@
|
||||
import { navigateTo } from "@metis/core/client";
|
||||
import { Button } from "@repo/shadcn-ui/components/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { CompanyDTO } from "../../../../common";
|
||||
import { DashboardPage } from "../../../dashboard/view/ui/dashboard-page";
|
||||
import { SectionNavigation } from "../../../shared/ui/section-navigation";
|
||||
import { companySectionHref, workspaceHref } from "../../../workspace/shared/helpers";
|
||||
import { DashboardQuickCreate } from "../../../dashboard/create";
|
||||
import { DashboardDetailsDialog } from "../../../dashboard/detail";
|
||||
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({
|
||||
company,
|
||||
csrfToken,
|
||||
@ -14,37 +24,78 @@ export function CompanyOverview({
|
||||
csrfToken: string;
|
||||
}) {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<SectionNavigation
|
||||
items={[
|
||||
{
|
||||
href: companySectionHref(company.id, "people"),
|
||||
title: t("people"),
|
||||
value: company.people.length,
|
||||
},
|
||||
{
|
||||
href: companySectionHref(company.id, "departments"),
|
||||
title: t("departments"),
|
||||
value: company.departments.length,
|
||||
},
|
||||
{
|
||||
href: companySectionHref(company.id, "sites"),
|
||||
title: t("sites"),
|
||||
value: company.sites.length,
|
||||
},
|
||||
]}
|
||||
label={t("companyOverview.directory")}
|
||||
/>
|
||||
<DashboardPage
|
||||
companyId={company.id}
|
||||
csrfToken={csrfToken}
|
||||
description={t("companyOverview.description", { name: company.name })}
|
||||
onNavigate={(destination) =>
|
||||
navigateTo(workspaceHref(destination), { create: destination.create === true })
|
||||
}
|
||||
title={t("companyOverview.title")}
|
||||
<div className="space-y-6" data-slot="company-overview">
|
||||
<header className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||
{t("companyOverview.title")}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t("companyOverview.description", { name: company.name })}
|
||||
</p>
|
||||
</div>
|
||||
<nav aria-label={t("dashboard.quickActions")} className="flex flex-wrap gap-2">
|
||||
{(["ticket", "intervention", "asset", "resource"] as const).map((kind) => {
|
||||
const Icon = KIND_ICONS[kind];
|
||||
return (
|
||||
<Button
|
||||
key={kind}
|
||||
onClick={() => setQuick(kind)}
|
||||
size="sm"
|
||||
variant={kind === "ticket" ? "default" : "outline"}
|
||||
>
|
||||
<Icon aria-hidden="true" className="size-4" />
|
||||
{t(`dashboard.create.${kind}`)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</header>
|
||||
<CompanyOverviewStatistics
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
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 { createInstance } from "i18next";
|
||||
import type { ReactNode } from "react";
|
||||
@ -14,6 +16,7 @@ import { companiesEnglish, companiesSpanish } from "../../shared";
|
||||
import { SiteForm } from "../../sites";
|
||||
import { CatalogOverview } from "../../workspace/catalogs/ui/catalog-overview";
|
||||
import { CompanyDetail } from "../detail";
|
||||
import { CompanyOverview } from "../detail/ui/company-overview";
|
||||
import { CompaniesPage } from "../list";
|
||||
|
||||
import { companyKeys } from "./queries";
|
||||
@ -28,7 +31,10 @@ async function renderPage(
|
||||
await i18n.init({
|
||||
lng: language,
|
||||
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 },
|
||||
});
|
||||
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"]) {
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@ -70,7 +70,6 @@ export function CompaniesWorkspace(props: CompaniesWorkspaceProps) {
|
||||
)}
|
||||
{current.area === "company" ? (
|
||||
<CompanyDetail
|
||||
accountNavigation={accountNavigation}
|
||||
csrfToken={csrfToken}
|
||||
embedded
|
||||
id={current.id}
|
||||
|
||||
@ -23,7 +23,9 @@ export function StatisticCard({
|
||||
tone = "teal",
|
||||
onClick,
|
||||
status,
|
||||
compact = false,
|
||||
}: {
|
||||
compact?: boolean;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
value?: number | undefined;
|
||||
@ -33,7 +35,7 @@ export function StatisticCard({
|
||||
}) {
|
||||
return (
|
||||
<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}
|
||||
type="button"
|
||||
>
|
||||
@ -44,8 +46,18 @@ export function StatisticCard({
|
||||
{icon}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-xs font-medium text-foreground">{label}</span>
|
||||
<strong className="mt-2 block text-3xl font-semibold text-foreground">
|
||||
<span
|
||||
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 ?? "—"}
|
||||
</strong>
|
||||
{status && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user