-- ============================================================ -- Materialized view : reporting.mv_interventions -- Scope : interventions des 12 derniers mois -- Refresh : via management command refresh_reporting_views (daily) -- -- Pour recréer manuellement : -- DROP MATERIALIZED VIEW IF EXISTS reporting.mv_interventions; -- \i mv_interventions.sql -- ============================================================ DROP MATERIALIZED VIEW IF EXISTS reporting.mv_interventions; CREATE MATERIALIZED VIEW reporting.mv_interventions AS WITH -- ── Moment de passage à 'à traiter' ────────────────────────────────────── tbe AS ( SELECT intervention_id, MIN(event_time) AS dt FROM django.interventions_interventiontimeline WHERE to_status = 'to_be_processed' GROUP BY intervention_id ), -- ── Premier passage à 'pris en charge' ─────────────────────────────────── first_assigned AS ( SELECT intervention_id, MIN(event_time) AS dt FROM django.interventions_interventiontimeline WHERE to_status = 'assigned' GROUP BY intervention_id ), -- ── Premier passage à 'démarré' ────────────────────────────────────────── first_ip AS ( SELECT intervention_id, MIN(event_time) AS dt FROM django.interventions_interventiontimeline WHERE to_status = 'in_progress' GROUP BY intervention_id ), -- ── Premier passage à 'terminé' ────────────────────────────────────────── first_finished AS ( SELECT intervention_id, MIN(event_time) AS dt FROM django.interventions_interventiontimeline WHERE to_status = 'finished' GROUP BY intervention_id ), -- ── Premier passage à 'en pause' ou 'terminé' ─────────────────────────── first_paused_or_finished AS ( SELECT intervention_id, MIN(event_time) AS dt FROM django.interventions_interventiontimeline WHERE to_status IN ('on_pause', 'finished') GROUP BY intervention_id ), -- ── Dernier passage à 'terminé' ────────────────────────────────────────── last_finished AS ( SELECT intervention_id, MAX(event_time) AS dt FROM django.interventions_interventiontimeline WHERE to_status = 'finished' GROUP BY intervention_id ), -- ── Temps d'exécution : somme des segments actifs en 'démarré' ─────────── -- La n-ième entrée en in_progress est appariée à la n-ième sortie de in_progress -- grâce au ROW_NUMBER, ce qui gère correctement les pauses successives. ip_starts AS ( SELECT intervention_id, event_time, ROW_NUMBER() OVER (PARTITION BY intervention_id ORDER BY event_time) AS rn FROM django.interventions_interventiontimeline WHERE to_status = 'in_progress' ), ip_ends AS ( SELECT intervention_id, event_time, ROW_NUMBER() OVER (PARTITION BY intervention_id ORDER BY event_time) AS rn FROM django.interventions_interventiontimeline WHERE from_status = 'in_progress' ), exec_time AS ( SELECT s.intervention_id, ROUND( SUM(EXTRACT(EPOCH FROM (e.event_time - s.event_time)) / 60.0) )::INTEGER AS total_minutes FROM ip_starts s JOIN ip_ends e ON e.intervention_id = s.intervention_id AND e.rn = s.rn AND e.event_time >= s.event_time GROUP BY s.intervention_id ), -- ── Première localisation associée à l'intervention ────────────────────── -- On prend la première par id (ordre d'insertion). first_loc AS ( SELECT DISTINCT ON (il.intervention_id) il.intervention_id, il.content_type_id AS loc_content_type_id, il.object_id AS loc_object_id, ct.app_label AS loc_app_label, ct.model AS loc_model FROM django.interventions_interventionlocation il JOIN django.django_content_type ct ON ct.id = il.content_type_id ORDER BY il.intervention_id, il.id ) SELECT -- ── Identité ───────────────────────────────────────────────────────────── i.id, i.code, i.title, i.lon, i.lat, -- ── Thématique ─────────────────────────────────────────────────────────── i.thematic_id, t.name_fr AS thematic_name_fr, t.name_nl AS thematic_name_nl, -- ── Catégorie d'asset ──────────────────────────────────────────────────── i.asset_category_id, ac.name_fr AS asset_category_name_fr, ac.name_nl AS asset_category_name_nl, -- ── Statut & priorité ──────────────────────────────────────────────────── i.status, i.priority, -- ── Catégorie source ───────────────────────────────────────────────────── i.source_category_id, sc.name_fr AS source_category_name_fr, -- ── Contrat & ordre ────────────────────────────────────────────────────── i.contract_id, c.contract_number, i.order_id, i.created_by_provider, -- ── Dimensions maintenance ─────────────────────────────────────────────── i.maintain_type AS maintenance_type, i.origin_type, i.type AS intervention_type, -- ── Date de création ───────────────────────────────────────────────────── i.creation_time, -- ── Horodatages bruts des jalons (utiles pour calculs complémentaires) ─── tbe.dt AS dt_to_be_processed, fa.dt AS dt_first_assigned, fip.dt AS dt_first_inprogress, ffin.dt AS dt_first_finished, lfin.dt AS dt_last_finished, -- ── Métriques de temps en minutes entières (NULL si statut non atteint) ── -- Temps de prise en charge : 'à traiter' → 'pris en charge' CASE WHEN fa.dt IS NOT NULL AND tbe.dt IS NOT NULL THEN ROUND(EXTRACT(EPOCH FROM (fa.dt - tbe.dt)) / 60.0)::INTEGER END AS response_time_min, -- Temps d'intervention : 'à traiter' → premier 'démarré' CASE WHEN fip.dt IS NOT NULL AND tbe.dt IS NOT NULL THEN ROUND(EXTRACT(EPOCH FROM (fip.dt - tbe.dt)) / 60.0)::INTEGER END AS time_to_inprogress_min, -- Premier temps de réalisation : 'à traiter' → premier 'en pause' ou 'terminé' CASE WHEN fpof.dt IS NOT NULL AND tbe.dt IS NOT NULL THEN ROUND(EXTRACT(EPOCH FROM (fpof.dt - tbe.dt)) / 60.0)::INTEGER END AS first_resolution_min, -- Temps de réalisation final : 'à traiter' → dernier 'terminé' CASE WHEN lfin.dt IS NOT NULL AND tbe.dt IS NOT NULL THEN ROUND(EXTRACT(EPOCH FROM (lfin.dt - tbe.dt)) / 60.0)::INTEGER END AS final_resolution_min, -- Temps d'exécution : somme des durées actives en 'démarré' exec_t.total_minutes AS execution_time_min, -- ── Localisation (GenericFK décodée) ───────────────────────────────────── fl.loc_content_type_id, fl.loc_object_id, fl.loc_app_label, fl.loc_model FROM django.interventions_intervention i LEFT JOIN django.common_thematic t ON t.id = i.thematic_id LEFT JOIN django.assets_assetcategory ac ON ac.id = i.asset_category_id LEFT JOIN django.interventions_sourcecategory sc ON sc.id = i.source_category_id LEFT JOIN django.contracts_contract c ON c.id = i.contract_id LEFT JOIN tbe ON tbe.intervention_id = i.id LEFT JOIN first_assigned fa ON fa.intervention_id = i.id LEFT JOIN first_ip fip ON fip.intervention_id = i.id LEFT JOIN first_finished ffin ON ffin.intervention_id = i.id LEFT JOIN first_paused_or_finished fpof ON fpof.intervention_id = i.id LEFT JOIN last_finished lfin ON lfin.intervention_id = i.id LEFT JOIN exec_time exec_t ON exec_t.intervention_id = i.id LEFT JOIN first_loc fl ON fl.intervention_id = i.id WHERE ( SELECT MIN(tl.event_time) FROM django.interventions_interventiontimeline tl WHERE tl.intervention_id = i.id ) > NOW() - INTERVAL '12 months' WITH DATA; -- ── Index unique requis pour REFRESH MATERIALIZED VIEW CONCURRENTLY ───────── CREATE UNIQUE INDEX reporting_mv_interventions_pk ON reporting.mv_interventions (id); -- ── Index opérationnels ────────────────────────────────────────────────────── CREATE INDEX reporting_mv_interventions_creation ON reporting.mv_interventions (creation_time); CREATE INDEX reporting_mv_interventions_thematic ON reporting.mv_interventions (thematic_id); CREATE INDEX reporting_mv_interventions_status ON reporting.mv_interventions (status); CREATE INDEX reporting_mv_interventions_contract ON reporting.mv_interventions (contract_id); CREATE INDEX reporting_mv_interventions_loc ON reporting.mv_interventions (loc_content_type_id, loc_object_id);