import { Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; export interface DashboardAuditItem { id: string; occurredAt: Date; actorUsername: string | null; action: string; entityType: string | null; entityId: string | null; } interface DashboardCountsRow { totalAssets: number | string; assetsNeedValidation: number | string; assetsWithoutGeometry: number | string; plannedInspections: number | string; activeUsers: number | string; inactiveUsers: number | string; activeSessions: number | string; openFindings: number | string; awaitingCompanyResponse: number | string; overdueCompanyResponses: number | string; companyResponsesDueNext7Days: number | string; awaitingVerificationSchedule: number | string; overdueControls: number | string; controlsNext30Days: number | string; } export interface DashboardUpcomingControl { id: string; code: string; title: string; nextControlOn: string; actId: string; actCode: string; visitId: string; visitCode: string; assetId: string; assetCode: string; assetName: string; } @Injectable() export class DashboardService { constructor(private readonly dataSource: DataSource) {} async summary() { return this.dataSource.transaction('REPEATABLE READ', async (manager) => { const [counts] = (await manager.query(` SELECT (SELECT COUNT(*) FROM assets WHERE information_status <> 'INACTIVE') AS "totalAssets", (SELECT COUNT(*) FROM assets WHERE information_status NOT IN ('VALIDATED','INACTIVE')) AS "assetsNeedValidation", (SELECT COUNT(*) FROM assets asset WHERE asset.information_status <> 'INACTIVE' AND NOT EXISTS (SELECT 1 FROM asset_geometries geometry WHERE geometry.asset_id=asset.id)) AS "assetsWithoutGeometry", (SELECT COUNT(*) FROM inspection_visits WHERE status='PLANNED') AS "plannedInspections", (SELECT COUNT(*) FROM users WHERE status = 'ACTIVE') AS "activeUsers", (SELECT COUNT(*) FROM users WHERE status = 'INACTIVE') AS "inactiveUsers", ( SELECT COUNT(*) FROM auth_sessions WHERE revoked_at IS NULL AND expires_at > CURRENT_TIMESTAMP ) AS "activeSessions", ( SELECT COUNT(*) FROM inspection_findings WHERE status = 'OPEN' ) AS "openFindings", ( SELECT COUNT(*) FROM inspection_findings WHERE status = 'OPEN' AND company_response_received_on IS NULL ) AS "awaitingCompanyResponse", ( SELECT COUNT(*) FROM inspection_findings WHERE status = 'OPEN' AND company_response_received_on IS NULL AND correction_due_on IS NOT NULL AND correction_due_on < (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date ) AS "overdueCompanyResponses", ( SELECT COUNT(*) FROM inspection_findings WHERE status = 'OPEN' AND company_response_received_on IS NULL AND correction_due_on BETWEEN (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date AND (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date + 7 ) AS "companyResponsesDueNext7Days", ( SELECT COUNT(*) FROM inspection_findings WHERE status = 'OPEN' AND company_response_received_on IS NOT NULL AND next_control_on IS NULL ) AS "awaitingVerificationSchedule", ( SELECT COUNT(*) FROM inspection_findings WHERE status = 'OPEN' AND company_response_received_on IS NOT NULL AND next_control_on < (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date ) AS "overdueControls", ( SELECT COUNT(*) FROM inspection_findings WHERE status = 'OPEN' AND next_control_on BETWEEN (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date AND (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date + 30 ) AS "controlsNext30Days" `)) as DashboardCountsRow[]; const recentAudit = (await manager.query(` SELECT event.id, event.occurred_at AS "occurredAt", event.actor_username AS "actorUsername", event.action, event.entity_type AS "entityType", event.entity_id AS "entityId" FROM audit_events event ORDER BY event.occurred_at DESC, event.id DESC LIMIT 6 `)) as DashboardAuditItem[]; const upcomingControls = (await manager.query(` SELECT finding.id, finding.code, finding.title, finding.next_control_on AS "nextControlOn", act.id AS "actId", act.code AS "actCode", visit.id AS "visitId", visit.code AS "visitCode", asset.id AS "assetId", asset.code AS "assetCode", asset.name AS "assetName" FROM inspection_findings finding INNER JOIN inspection_acts act ON act.id = finding.act_id INNER JOIN inspection_visits visit ON visit.id = act.visit_id INNER JOIN assets asset ON asset.id = finding.asset_id WHERE finding.status = 'OPEN' AND finding.company_response_received_on IS NOT NULL AND finding.next_control_on IS NOT NULL ORDER BY finding.next_control_on, finding.code LIMIT 8 `)) as DashboardUpcomingControl[]; return { counts: { totalAssets: Number(counts?.totalAssets ?? 0), assetsNeedValidation: Number(counts?.assetsNeedValidation ?? 0), assetsWithoutGeometry: Number(counts?.assetsWithoutGeometry ?? 0), plannedInspections: Number(counts?.plannedInspections ?? 0), activeUsers: Number(counts?.activeUsers ?? 0), inactiveUsers: Number(counts?.inactiveUsers ?? 0), activeSessions: Number(counts?.activeSessions ?? 0), openFindings: Number(counts?.openFindings ?? 0), awaitingCompanyResponse: Number(counts?.awaitingCompanyResponse ?? 0), overdueCompanyResponses: Number(counts?.overdueCompanyResponses ?? 0), companyResponsesDueNext7Days: Number(counts?.companyResponsesDueNext7Days ?? 0), awaitingVerificationSchedule: Number(counts?.awaitingVerificationSchedule ?? 0), overdueControls: Number(counts?.overdueControls ?? 0), controlsNext30Days: Number(counts?.controlsNext30Days ?? 0), }, recentAudit, upcomingControls, generatedAt: new Date().toISOString(), }; }); } }