- 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.
228 lines
9.1 KiB
TypeScript
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"
|
|
);
|
|
}
|
|
}
|