* fix(web): simplify inventory administration menu * fix(web): remove legacy imports and function catalog routes * fix(web): remove redundant inspections lifecycle legend * feat(inventory): distinguish physical instances from structural records * fix(dashboard): align inventory and act follow-up metrics * fix(web): align dashboard summary contract * fix(web): clarify dashboard act and report concepts * feat(inventory): mark field-created records as real instances * feat(inventory): map physical instance flag on asset entity * fix(inventory): keep field yacimientos structural * feat(inventory): classify future concrete instances at database level * fix(inventory): count only installation and subinstallation instances * feat(inventory): add authoritative F5 source snapshot * feat(inventory): preload authoritative territory model * feat(inventory): preload authoritative technical catalog * fix(findings): use only authoritative F5 family catalog * fix(inventory): preserve non-hierarchical operator snapshot compatibility * feat(inventory): add inventory-only asset filter * feat(inventory): add inventory-only tree filter * feat(inventory): add inventory browser query contract * feat(inventory): add area-owned inventory browser * feat(inventory): expose area-owned inventory browser * refactor(inventory): remove function catalog and add inventory browser * fix(inventory): make operator relation temporal and non-owning * feat(web): add inventory browser API client * feat(inventory): extend inventory browser filters * feat(inventory): add real inventory list endpoint logic * feat(inventory): expose real inventory list * feat(web): add real inventory list client * refactor(web): make inventory hierarchy area-owned * fix(web): show only real inventory instances * fix(web): style act follow-up tabs and F5 inventory context * fix(web): load F5 flow styles * fix(inventory): apply area-owned operational guard on F5 up * fix(inventory): treat company on asset as non-owning creation snapshot * fix(inventory): resolve field inventory by area hierarchy, not company ownership * fix(inventory): preserve custom catalog and apply authoritative universal findings * fix(inventory): harden authoritative catalog migration checks * fix(inventory): make authoritative territory preload safely reversible * feat(inventory): allow independent company master creation * fix(inventory): make guided creation area-owned and support companies * feat(web): expose independent company master in inventory setup * feat(web): create companies independently from physical inventory hierarchy * fix(inventory): merge by physical area and preserve sealed documents * test(inventory): lock F5 authoritative model and merge invariants * feat(inventory): add family administration DTOs * feat(inventory): administer installation and subinstallation classifications * feat(inventory): expose family classification administration * feat(web): add inventory classification administration API * fix(web): configure finding applicability by inventory classification * fix(web): redefine inventory configuration around hierarchy classifications and columns * chore(release): identify F5 inventory model * chore(release): bump API for F5 inventory model * test(release): expect F5 health metadata * chore(release): align WEB package with F5 inventory cut * chore(release): expose F5 WEB phase * test(dashboard): expect inspector activity and act follow-up metrics * test(dashboard): route F5 summary query mocks explicitly * ci: rehearse all migrations on clean PostGIS before merge * ci: prove F5 migrations revert and reapply cleanly * test(f5): align operational navigation contract * test(f5): align operator lifecycle with area-owned inventory * test(f5): make merge compatibility area-based * test(f5): distinguish literal and normalized yacimiento counts * test(f5): model normalized yacimiento collision explicitly * ci(f5): bootstrap historical admin prerequisite in clean migration rehearsal * ci(f5): bypass irreversible historical reset in clean rehearsal * fix(f5): make territory SQL parameter types explicit * fix(f5): guarantee canonical inventory hierarchy before territory preload * ci(f5): include canonical hierarchy migration in rollback gate * fix(f5): type relation backup markers explicitly * fix(f5): make catalog SQL text parameter types explicit
221 lines
9.0 KiB
TypeScript
221 lines
9.0 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { DataSource } from 'typeorm';
|
|
|
|
export interface DashboardInspectorActivityItem {
|
|
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;
|
|
actsInFollowUp: number | string;
|
|
findingsWithoutControlDate: number | string;
|
|
overdueControls: number | string;
|
|
controlsNext30Days: number | string;
|
|
reportsWorking: number | string;
|
|
reportsOfficialized: number | string;
|
|
sealedActsWithoutReport: 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 is_inventory_instance=true AND information_status <> 'INACTIVE') AS "totalAssets",
|
|
(SELECT COUNT(*) FROM assets WHERE is_inventory_instance=true AND information_status NOT IN ('VALIDATED','INACTIVE')) AS "assetsNeedValidation",
|
|
(
|
|
SELECT COUNT(*) FROM assets asset
|
|
WHERE asset.is_inventory_instance=true
|
|
AND 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_acts act
|
|
WHERE act.status IN ('SEALED','CLOSED','RECTIFIED')
|
|
AND NOT (
|
|
EXISTS (
|
|
SELECT 1 FROM inspection_findings finding
|
|
WHERE finding.act_id=act.id AND finding.status<>'VOIDED'
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM inspection_findings finding
|
|
WHERE finding.act_id=act.id AND finding.status='OPEN'
|
|
)
|
|
)
|
|
) AS "actsInFollowUp",
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM inspection_findings finding
|
|
WHERE finding.status = 'OPEN'
|
|
AND finding.next_control_on IS NULL
|
|
AND COALESCE((
|
|
SELECT verification.outcome
|
|
FROM inspection_finding_verification_visits verification
|
|
WHERE verification.finding_id=finding.id AND verification.outcome IS NOT NULL
|
|
ORDER BY verification.result_recorded_at DESC NULLS LAST, verification.created_at DESC, verification.id DESC
|
|
LIMIT 1
|
|
), '') <> 'RESOLVED'
|
|
) AS "findingsWithoutControlDate",
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM inspection_findings finding
|
|
WHERE finding.status = 'OPEN'
|
|
AND finding.next_control_on < (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date
|
|
AND COALESCE((
|
|
SELECT verification.outcome
|
|
FROM inspection_finding_verification_visits verification
|
|
WHERE verification.finding_id=finding.id AND verification.outcome IS NOT NULL
|
|
ORDER BY verification.result_recorded_at DESC NULLS LAST, verification.created_at DESC, verification.id DESC
|
|
LIMIT 1
|
|
), '') <> 'RESOLVED'
|
|
) AS "overdueControls",
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM inspection_findings finding
|
|
WHERE finding.status = 'OPEN'
|
|
AND finding.next_control_on BETWEEN
|
|
(CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date
|
|
AND (CURRENT_TIMESTAMP AT TIME ZONE 'America/Argentina/Mendoza')::date + 30
|
|
AND COALESCE((
|
|
SELECT verification.outcome
|
|
FROM inspection_finding_verification_visits verification
|
|
WHERE verification.finding_id=finding.id AND verification.outcome IS NOT NULL
|
|
ORDER BY verification.result_recorded_at DESC NULLS LAST, verification.created_at DESC, verification.id DESC
|
|
LIMIT 1
|
|
), '') <> 'RESOLVED'
|
|
) AS "controlsNext30Days",
|
|
(SELECT COUNT(*) FROM inspection_reports WHERE status='WORKING') AS "reportsWorking",
|
|
(SELECT COUNT(*) FROM inspection_reports WHERE status='OFFICIALIZED') AS "reportsOfficialized",
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM inspection_acts act
|
|
WHERE act.status='SEALED'
|
|
AND NOT EXISTS (SELECT 1 FROM inspection_reports report WHERE report.act_id=act.id)
|
|
) AS "sealedActsWithoutReport"
|
|
`)) as DashboardCountsRow[];
|
|
|
|
const recentInspectorActivity = (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
|
|
WHERE event.actor_user_id IS NOT NULL
|
|
AND (
|
|
EXISTS (
|
|
SELECT 1 FROM inspection_visit_members member
|
|
WHERE member.user_id=event.actor_user_id AND member.included=true
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1 FROM inspection_visits visit
|
|
WHERE visit.lead_inspector_user_id=event.actor_user_id
|
|
)
|
|
)
|
|
AND (
|
|
event.action LIKE 'INSPECTION_%'
|
|
OR event.action LIKE 'ASSET_%'
|
|
OR event.action LIKE 'FINDING_%'
|
|
)
|
|
ORDER BY event.occurred_at DESC, event.id DESC
|
|
LIMIT 8
|
|
`)) as DashboardInspectorActivityItem[];
|
|
|
|
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.next_control_on IS NOT NULL
|
|
AND COALESCE((
|
|
SELECT verification.outcome
|
|
FROM inspection_finding_verification_visits verification
|
|
WHERE verification.finding_id=finding.id AND verification.outcome IS NOT NULL
|
|
ORDER BY verification.result_recorded_at DESC NULLS LAST, verification.created_at DESC, verification.id DESC
|
|
LIMIT 1
|
|
), '') <> 'RESOLVED'
|
|
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),
|
|
actsInFollowUp: Number(counts?.actsInFollowUp ?? 0),
|
|
findingsWithoutControlDate: Number(counts?.findingsWithoutControlDate ?? 0),
|
|
overdueControls: Number(counts?.overdueControls ?? 0),
|
|
controlsNext30Days: Number(counts?.controlsNext30Days ?? 0),
|
|
reportsWorking: Number(counts?.reportsWorking ?? 0),
|
|
reportsOfficialized: Number(counts?.reportsOfficialized ?? 0),
|
|
sealedActsWithoutReport: Number(counts?.sealedActsWithoutReport ?? 0),
|
|
},
|
|
recentInspectorActivity,
|
|
upcomingControls,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
});
|
|
}
|
|
}
|