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( sql: string, query: DashboardDetailsQuery, extra: Record = {} ) { return this.database.query(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( `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( `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( `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( `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 } ); } } } }