metis-desk/apps/server/src/dashboard.integration.ts
David bada13a50e feat(dashboard): implement dashboard schemas and API integration
- Added Zod schemas for dashboard queries, entries, summaries, and companies.
- Created dashboard API functions for fetching summary, work, attention, activity, and companies data.
- Developed dashboard components including BlockState, DashboardDetailsDialog, and DashboardPage for rendering dashboard information.
- Implemented quick create functionality for companies, tickets, interventions, assets, and resources.
- Introduced a new ResourceDashboardReader for querying resource-related data.
- Added a StatisticCard component for displaying key performance indicators in the dashboard.
2026-10-08 13:27:27 +02:00

228 lines
9.1 KiB
TypeScript

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import {
CompanyResponseSchema,
DashboardAttentionSchema,
DashboardCompaniesSchema,
DashboardEntriesSchema,
DashboardSummarySchema,
InterventionListSchema,
RemoteConnectionResponseSchema,
TicketListSchema,
} from "@metis/companies/common";
import { CatalogResponseSchema, ResourceResponseSchema } from "@metis/resources/common";
type Request = (path: string, body?: unknown) => Promise<Response>;
export async function verifyDashboard(request: Request, companyId: string, otherId: string) {
async function get(path: string) {
const response = await request(path);
assert.equal(response.status, 200, await response.clone().text());
return response.json();
}
const summary = DashboardSummarySchema.parse(await get("/dashboard/summary"));
const companies = DashboardCompaniesSchema.parse(await get("/dashboard/companies"));
assert.equal(
summary.assets,
companies.items.reduce((sum, item) => sum + item.assets, 0)
);
assert.equal(
summary.resources,
companies.items.reduce((sum, item) => sum + item.resources, 0)
);
const tickets = TicketListSchema.parse(await get("/tickets"));
const interventions = InterventionListSchema.parse(await get("/interventions"));
assert.equal(
summary.tickets,
tickets.tickets.filter((item) => !["resolved", "cancelled"].includes(item.values.state)).length
);
assert.equal(
summary.interventions,
interventions.interventions.filter(
(item) => !["finished", "cancelled"].includes(item.values.state)
).length
);
for (const scope of [companyId, otherId]) {
const scoped = DashboardSummarySchema.parse(await get(`/dashboard/summary?companyId=${scope}`));
const row = companies.items.find((item) => item.id === scope);
assert.ok(row);
assert.equal(scoped.assets, row.assets);
assert.equal(scoped.resources, row.resources);
for (const block of [
"work",
"activity",
"details?kind=asset",
"details?kind=resource",
"details?issue=certificate",
"details?issue=license",
"details?issue=profile",
"details?issue=missing_site",
"details?issue=inactive_person",
]) {
const result = DashboardEntriesSchema.parse(
await get(`/dashboard/${block}${block.includes("?") ? "&" : "?"}companyId=${scope}`)
);
assert.ok(result.items.every((item) => item.companyId === scope));
assert.ok(result.items.length <= 10);
}
}
const work = DashboardEntriesSchema.parse(await get("/dashboard/work"));
assert.equal(work.total, summary.tickets + summary.interventions);
assert.ok(
work.items.every((item) => !["resolved", "cancelled", "finished"].includes(item.state ?? ""))
);
const priorityRank = (value: string | null) =>
["urgent", "high", "normal", "low"].indexOf(value ?? "normal");
for (let index = 1; index < work.items.length; index++) {
const previous = work.items[index - 1];
const current = work.items[index];
assert.ok(previous && current);
assert.ok(priorityRank(previous.priority) <= priorityRank(current.priority));
if (priorityRank(previous.priority) === priorityRank(current.priority)) {
assert.ok((previous.recordedAt ?? "") <= (current.recordedAt ?? ""));
}
}
const activity = DashboardEntriesSchema.parse(await get("/dashboard/activity"));
assert.equal(new Set(activity.items.map((item) => item.eventId)).size, activity.items.length);
assert.ok(activity.items.every((item) => item.eventId));
assert.equal(activity.total, activity.items.length);
const attention = DashboardAttentionSchema.parse(await get("/dashboard/attention"));
assert.equal(attention.items.length, 5);
for (const alert of attention.items) {
const details = DashboardEntriesSchema.parse(
await get(`/dashboard/details?issue=${alert.kind}`)
);
assert.equal(alert.count, details.total);
}
const beyond = DashboardEntriesSchema.parse(
await get("/dashboard/details?kind=asset&page=100000")
);
assert.equal(beyond.items.length, 0);
assert.equal(beyond.total, summary.assets);
for (const path of [
"/dashboard/summary?companyId=bad",
"/dashboard/details?page=0",
"/dashboard/details?issue=unknown",
"/dashboard/summary?unknown=true",
]) {
assert.equal((await request(path)).status, 400);
}
assert.equal((await request(`/dashboard/summary?companyId=${randomUUID()}`)).status, 404);
}
export async function verifyDashboardBoundaries(request: Request) {
const created = await request("/companies", { name: "Dashboard vacío", active: true });
assert.equal(created.status, 201, await created.clone().text());
const companyId = CompanyResponseSchema.parse(await created.json()).company.id;
const empty = await request(`/dashboard/summary?companyId=${companyId}`);
const zero = DashboardSummarySchema.parse(await empty.json());
assert.deepEqual(zero, { companies: 1, tickets: 0, interventions: 0, assets: 0, resources: 0 });
const catalogResponse = await request("/resource-catalog/global");
const catalog = CatalogResponseSchema.parse(await catalogResponse.json());
const expiry = catalog.items.find((item) => item.builtinKey === "field:expires");
assert.ok(expiry);
const date = (offset: number) =>
new Date(Date.now() + offset * 86400000).toISOString().slice(0, 10);
for (const [issue, key] of [
["certificate", "certificate"],
["license", "software_license"],
]) {
const template = catalog.items.find((item) => item.builtinKey === `template:${key}`);
assert.ok(template);
const expected: string[] = [];
for (const [offset, active] of [
[0, true],
[30, true],
[31, true],
[-1, true],
[5, false],
[null, true],
] as const) {
const response = await request(`/resources/${companyId}`, {
values: {
name: `Dashboard ${key} ${offset} ${active}`,
templateId: template.id,
active,
notes: "",
characteristics: offset === null ? {} : { [expiry.id]: date(offset) },
},
});
assert.equal(response.status, 201, await response.clone().text());
const resource = ResourceResponseSchema.parse(await response.json()).resource;
if (active && (offset === 0 || offset === 30)) {
expected.push(resource.id);
}
}
const response = await request(`/dashboard/details?companyId=${companyId}&issue=${issue}`);
assert.equal(response.status, 200, await response.clone().text());
const result = DashboardEntriesSchema.parse(await response.json());
assert.equal(result.total, 2);
assert.deepEqual(new Set(result.items.map((item) => item.id)), new Set(expected));
}
const first = DashboardEntriesSchema.parse(
await (await request(`/dashboard/details?companyId=${companyId}&kind=resource`)).json()
);
const second = DashboardEntriesSchema.parse(
await (await request(`/dashboard/details?companyId=${companyId}&kind=resource&page=2`)).json()
);
assert.equal(first.total, 12);
assert.equal(first.items.length, 10);
assert.equal(second.items.length, 2);
assert.equal(second.total, 12);
assert.equal(new Set([...first.items, ...second.items].map((item) => item.id)).size, 12);
}
export async function verifyDashboardInactiveAccess(request: Request, companyId: string) {
const personValues = {
name: "Dashboard acceso personal",
position: null,
email: null,
phone: null,
active: true,
isContact: false,
};
const created = await request(`/companies/${companyId}/people`, personValues);
assert.equal(created.status, 201, await created.clone().text());
const company = CompanyResponseSchema.parse(await created.json()).company;
const person = company.people.find((item) => item.name === personValues.name);
assert.ok(person);
const values = {
personId: person.id,
sourceKind: "personal_computer",
sourceAssetId: null,
targetKind: "network",
targetId: null,
siteId: null,
method: "VPN",
requiresVpn: false,
state: "configured",
notes: null,
};
const response = await request(`/remote-connections/${companyId}`, { values });
assert.equal(response.status, 201, await response.clone().text());
const connection = RemoteConnectionResponseSchema.parse(await response.json()).connection;
const deactivated = await request(`/companies/${companyId}/people/${person.id}/update`, {
...personValues,
active: false,
});
assert.equal(deactivated.status, 200, await deactivated.clone().text());
async function count() {
const response = await request(
`/dashboard/details?companyId=${companyId}&issue=inactive_person`
);
assert.equal(response.status, 200, await response.clone().text());
return DashboardEntriesSchema.parse(await response.json());
}
assert.ok((await count()).items.some((item) => item.id === person.id));
for (const state of ["removal_pending", "removed"]) {
const updated = await request(`/remote-connections/${companyId}/${connection.id}/update`, {
values: { ...values, state },
});
assert.equal(updated.status, 200, await updated.clone().text());
assert.equal(
(await count()).items.some((item) => item.id === person.id),
state !== "removed"
);
}
}