276 lines
11 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|