metis-desk/modules/companies/src/api/infrastructure/persistence/sequelize-dashboard.repository.ts
2026-10-10 16:40:51 +02:00

276 lines
11 KiB
TypeScript

import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
import type { DashboardDetailsQuery, DashboardEntryDTO } from "../../../common/dashboard";
import type {
DashboardBlock,
IAssetDashboardReader,
IDashboardRepository,
IInventoryDashboardReader,
InventoryEntry,
} from "../../application/dashboard.use-cases";
const OPEN_TICKET = "t.state IN ('open','in_progress','waiting')";
const PENDING_INTERVENTION = "i.state IN ('pending','in_progress')";
const PROFILE_REVIEW = `p.active=true AND (person.active=0 OR a.status='retired' OR (p.accountId IS NOT NULL AND (
account.ownerId IS NULL OR account.active<>true OR account.usage='technical'
OR account.expiresOn < :today
OR NOT EXISTS (SELECT 1 FROM access_current_values s WHERE s.ownerId=account.systemId AND s.kind='system' AND s.active=true)
OR (NULLIF(account.environmentId,'null') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM access_current_values e WHERE e.ownerId=account.environmentId AND e.kind='environment' AND e.active=true))
OR NOT EXISTS (SELECT 1 FROM access_current_values link WHERE link.kind='account_person' AND link.personId=p.personId AND link.accountId=p.accountId AND link.startDate<=:today AND (NULLIF(link.endDate,'null') IS NULL OR link.endDate>:today) AND link.configuration IN ('pending','configured'))
)))`;
export class SequelizeDashboardRepository implements IDashboardRepository {
constructor(
private readonly database: Sequelize,
private readonly assets: IAssetDashboardReader,
private readonly resources: IInventoryDashboardReader
) {}
private select<T extends object>(
sql: string,
query: DashboardDetailsQuery,
extra: Record<string, string | number | null | string[]> = {}
) {
return this.database.query<T>(sql, {
type: QueryTypes.SELECT,
replacements: {
companyId: query.companyId ?? null,
today: new Date().toISOString().slice(0, 10),
offset: (query.page - 1) * 10,
...extra,
},
});
}
async companyExists(id: string) {
const rows = await this.select<{ id: string }>("SELECT id FROM companies WHERE id=:companyId", {
companyId: id,
page: 1,
});
return rows.length > 0;
}
private async companies(query: DashboardDetailsQuery) {
const [rows, assets, resources] = await Promise.all([
this.select<{
id: string;
name: string;
active: number;
tickets: number;
interventions: number;
people: number;
}>(
`SELECT c.id,c.name,c.active,(SELECT COUNT(*) FROM tickets t WHERE t.companyId=c.id AND ${OPEN_TICKET}) AS tickets,(SELECT COUNT(*) FROM interventions i WHERE i.companyId=c.id AND ${PENDING_INTERVENTION}) AS interventions,(SELECT COUNT(*) FROM company_contacts p WHERE p.companyId=c.id) AS people FROM companies c WHERE (:companyId IS NULL OR c.id=:companyId) ORDER BY c.name,c.id`,
query
),
this.assets.stats(query.companyId),
this.resources.stats(query.companyId),
]);
return rows.map((row) => ({
id: row.id,
name: row.name,
companies: Number(Boolean(row.active)),
tickets: Number(row.tickets),
interventions: Number(row.interventions),
people: Number(row.people),
assets: Number(assets.find((item) => item.companyId === row.id)?.count ?? 0),
resources: Number(resources.find((item) => item.companyId === row.id)?.count ?? 0),
}));
}
private workQuery() {
return `SELECT t.id,t.companyId,c.name AS companyName,'ticket' AS kind,t.reference,t.subject AS name,t.state AS state,t.priority AS priority,(SELECT MIN(recordedAt) FROM ticket_revisions WHERE ticketId=t.id) AS recordedAt,NULL AS actorName FROM tickets t JOIN companies c ON c.id=t.companyId WHERE ${OPEN_TICKET} AND (:companyId IS NULL OR t.companyId=:companyId)
UNION ALL SELECT i.id,i.companyId,c.name,'intervention',i.reference,i.subject,i.state,t.priority,(SELECT MIN(recordedAt) FROM intervention_revisions WHERE interventionId=i.id),NULL FROM interventions i JOIN companies c ON c.id=i.companyId LEFT JOIN tickets t ON t.id=i.ticketId WHERE ${PENDING_INTERVENTION} AND (:companyId IS NULL OR i.companyId=:companyId)`;
}
private async work(query: DashboardDetailsQuery) {
const counts = await this.select<{ total: number }>(
`SELECT COUNT(*) AS total FROM (${this.workQuery()}) work`,
query
);
const items = await this.select<DashboardEntryDTO>(
`SELECT * FROM (${this.workQuery()}) work ORDER BY FIELD(COALESCE(priority,'normal'),'urgent','high','normal','low'),recordedAt,id LIMIT 10 OFFSET :offset`,
query
);
return { items, total: Number(counts[0]?.total ?? 0) };
}
private async issues(query: DashboardDetailsQuery) {
if (query.issue === "profile") {
const from =
"FROM work_profiles p JOIN companies c ON c.id=p.companyId JOIN company_contacts person ON person.id=p.personId JOIN assets a ON a.id=p.assetId LEFT JOIN access_current_values account ON account.ownerId=p.accountId";
const where = `WHERE (:companyId IS NULL OR p.companyId=:companyId) AND ${PROFILE_REVIEW}`;
const count = await this.select<{ total: number }>(
`SELECT COUNT(*) AS total ${from} ${where}`,
query
);
const items = await this.select<InventoryEntry>(
`SELECT p.id,p.companyId,c.name AS companyName,p.name AS name,NULL AS recordedAt,NULL AS actorName ${from} ${where} ORDER BY name,p.id LIMIT 10 OFFSET :offset`,
query
);
return {
items: items.map((item) => ({
...item,
kind: "profile" as const,
reference: null,
state: null,
priority: null,
})),
total: Number(count[0]?.total ?? 0),
};
}
if (query.issue === "inactive_person") {
const assets = await this.assets.inactivePeople(query.companyId);
const ids = assets.map((item) => item.id);
const profileConfigurations = [
"application_configurations",
"certificate_configurations",
"folder_mappings",
"printer_configurations",
]
.map(
(table) =>
`EXISTS (SELECT 1 FROM ${table} cfg JOIN work_profiles w ON w.id=cfg.profileId WHERE cfg.companyId=p.companyId AND w.personId=p.id AND cfg.state NOT IN ('removed','never_configured'))`
)
.join(" OR ");
const where = `WHERE p.active=0 AND (:companyId IS NULL OR p.companyId=:companyId) AND (p.id IN (:ids) OR EXISTS (SELECT 1 FROM work_profiles w WHERE w.personId=p.id AND w.active=true) OR EXISTS (SELECT 1 FROM access_current_values link JOIN access_items li ON li.id=link.ownerId JOIN access_workspaces ws ON ws.id=li.workspaceId WHERE ws.companyId=p.companyId AND link.personId=p.id AND ((link.kind='need' AND (NULLIF(link.endDate,'null') IS NULL OR link.endDate>:today)) OR (link.kind='account_person' AND link.configuration IN ('pending','configured','removal_pending'))))
OR EXISTS (SELECT 1 FROM remote_connections r WHERE r.companyId=p.companyId AND r.personId=p.id AND r.state NOT IN ('removed','never_configured'))
OR EXISTS (SELECT 1 FROM mail_configurations m LEFT JOIN work_profiles w ON w.id=m.profileId WHERE m.companyId=p.companyId AND (m.personId=p.id OR w.personId=p.id) AND m.state NOT IN ('removed','never_configured'))
OR ${profileConfigurations}
OR EXISTS (SELECT 1 FROM telework_authorizations ta WHERE ta.companyId=p.companyId AND ta.personId=p.id AND ta.active=true AND (ta.expiresOn IS NULL OR ta.expiresOn>=:today)))`;
const extra = { ids: ids.length ? ids : [""] };
const count = await this.select<{ total: number }>(
`SELECT COUNT(*) AS total FROM company_contacts p ${where}`,
query,
extra
);
const items = await this.select<InventoryEntry>(
`SELECT p.id,p.companyId,c.name AS companyName,p.name,NULL AS recordedAt,NULL AS actorName FROM company_contacts p JOIN companies c ON c.id=p.companyId ${where} ORDER BY p.name,p.id LIMIT 10 OFFSET :offset`,
query,
extra
);
return {
items: items.map((item) => ({
...item,
kind: "person" as const,
reference: null,
state: null,
priority: null,
})),
total: Number(count[0]?.total ?? 0),
};
}
const kind = query.issue === "missing_site" || query.kind === "asset" ? "asset" : "resource";
const data = await (kind === "asset" ? this.assets : this.resources).entries(
query.companyId,
query.issue,
query.page
);
return {
total: data.total,
items: data.items.map((item) => ({
...item,
kind,
reference: null,
state: null,
priority: null,
})),
};
}
private async activity(query: DashboardDetailsQuery) {
const items: DashboardEntryDTO[] = [];
for (const [table, revisions, foreignKey, kind] of [
["tickets", "ticket_revisions", "ticketId", "ticket"],
["interventions", "intervention_revisions", "interventionId", "intervention"],
] as const) {
items.push(
...(await this.select<DashboardEntryDTO>(
`SELECT t.id,h.id AS eventId,t.companyId,c.name AS companyName,'${kind}' AS kind,t.reference,h.subject AS name,h.state AS state,NULL AS priority,h.recordedAt,u.name AS actorName FROM ${revisions} h JOIN ${table} t ON t.id=h.${foreignKey} JOIN companies c ON c.id=t.companyId LEFT JOIN users u ON u.id=h.actorId WHERE (:companyId IS NULL OR t.companyId=:companyId) ORDER BY h.recordedAt DESC,h.id DESC LIMIT 6`,
query
))
);
}
for (const [service, kind] of [
[this.assets, "asset"],
[this.resources, "resource"],
] as const) {
items.push(
...(await service.activity(query.companyId)).map((item) => ({
...item,
kind,
reference: null,
state: null,
priority: null,
}))
);
}
return {
total: Math.min(items.length, 6),
items: items
.sort(
(a, b) =>
(b.recordedAt ?? "").localeCompare(a.recordedAt ?? "") || a.id.localeCompare(b.id)
)
.slice(0, 6),
};
}
async read(block: DashboardBlock, query: DashboardDetailsQuery) {
switch (block) {
case "work":
return this.work(query);
case "activity":
return this.activity(query);
case "details":
return this.issues(query);
case "attention": {
const kinds = [
"certificate",
"license",
"profile",
"missing_site",
"inactive_person",
] as const;
return {
items: await Promise.all(
kinds.map(async (kind) => ({
kind,
count: (await this.issues({ ...query, issue: kind })).total,
}))
),
};
}
case "companies": {
const items = await this.companies(query);
return { total: items.length, items };
}
case "summary": {
const items = await this.companies(query);
return items.reduce(
(sum, item) => ({
companies: sum.companies + item.companies,
tickets: sum.tickets + item.tickets,
interventions: sum.interventions + item.interventions,
assets: sum.assets + item.assets,
resources: sum.resources + item.resources,
}),
{ companies: 0, tickets: 0, interventions: 0, assets: 0, resources: 0 }
);
}
}
}
}