BEGIN; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_candidates_v1 AS WITH stop_base AS ( SELECT s.*, COALESCE( s.duration_s, EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at))::numeric ) AS effective_duration_s, COALESCE( s.cost_eur, ROUND(( EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at)) / 3600.0 * s.value_hour_eur )::numeric, 2) ) AS effective_capacity_cost_eur FROM mv_hot.edge_oee_machine_stops s WHERE s.tenant = 'ucepsa' AND s.site = 'ucepsa_onpremise' ), enriched AS ( SELECT s.*, d.id AS deployment_id, d.scope_code, d.mode AS deployment_mode, d.planned_go_live_date, d.actual_go_live_at, p.noise_below_s, p.aggregate_below_s, p.operator_individual_from_s, p.aggregation_window_min, p.require_valid_order, p.require_closed_event, o.id AS order_state_event_id, o.effective_from AS order_context_from, o.effective_to AS order_context_to, o.odoo_state, o.order_present, o.order_coherent, o.workorder_id AS odoo_workorder_id, o.workorder_name, o.workorder_state, o.production_id AS odoo_production_id, o.production_order AS order_ref, o.product_id, o.product AS product_name, o.qty_production, o.date_start AS odoo_date_start, o.date_finished AS odoo_date_finished, pc.units_per_cycle, pc.target_cycle_rate_ppm, ep.parameter_value AS configured_capacity_eur_h, ep.confidence AS capacity_cost_confidence FROM stop_base s LEFT JOIN LATERAL ( SELECT x.* FROM mv_loss_intelligence.deployment_config x WHERE x.tenant = s.tenant AND x.site = s.site AND x.scope_code = 'CUTTERS' ORDER BY x.id DESC LIMIT 1 ) d ON true LEFT JOIN LATERAL ( SELECT x.* FROM mv_loss_intelligence.loss_policies x WHERE x.tenant = s.tenant AND x.site = s.site AND x.policy_code = 'CUTTER_STOPS_V1' AND x.is_active = true ORDER BY x.id DESC LIMIT 1 ) p ON true LEFT JOIN LATERAL ( SELECT x.* FROM mv_reports_ucepsa_prod.li_order_state_intervals_v1 x WHERE x.tenant = s.tenant AND x.site = s.site AND x.machine_id = s.machine_id AND s.started_at >= x.effective_from AND (x.effective_to IS NULL OR s.started_at < x.effective_to) ORDER BY x.effective_from DESC, x.id DESC LIMIT 1 ) o ON true LEFT JOIN LATERAL ( SELECT x.* FROM mv_loss_intelligence.product_cycle_parameters x WHERE x.tenant = s.tenant AND x.site = s.site AND x.is_active = true AND x.product_id = o.product_id AND (x.machine_id IS NULL OR x.machine_id = s.machine_id) AND s.started_at >= x.valid_from AND (x.valid_to IS NULL OR s.started_at < x.valid_to) ORDER BY (x.machine_id IS NOT NULL) DESC, x.valid_from DESC, x.id DESC LIMIT 1 ) pc ON true LEFT JOIN LATERAL ( SELECT x.* FROM mv_loss_intelligence.economic_parameters x WHERE x.tenant = s.tenant AND x.site = s.site AND x.parameter_code = 'CAPACITY_HOUR_EUR' AND x.is_active = true AND (x.machine_id IS NULL OR x.machine_id = s.machine_id) AND s.started_at >= x.valid_from AND (x.valid_to IS NULL OR s.started_at < x.valid_to) ORDER BY (x.machine_id IS NOT NULL) DESC, x.valid_from DESC, x.id DESC LIMIT 1 ) ep ON true ) SELECT e.id AS source_stop_id, e.tenant, e.site, e.deployment_id, e.scope_code, e.deployment_mode, e.planned_go_live_date, e.actual_go_live_at, CASE WHEN e.deployment_mode = 'ACTIVE' AND e.actual_go_live_at IS NOT NULL AND e.started_at >= e.actual_go_live_at THEN 'ACTIVE' ELSE 'SHADOW' END AS ledger_phase, e.machine_id, e.asset, e.started_at, e.ended_at, e.status AS source_status, e.classification_status, e.cause_code, c.cause_name, c.is_planned, e.operator_note, e.start_event_name, e.end_event_name, e.effective_duration_s, ROUND((e.effective_duration_s / 60.0)::numeric, 2) AS effective_duration_min, e.value_hour_eur AS source_value_hour_eur, e.configured_capacity_eur_h, COALESCE(e.configured_capacity_eur_h, e.value_hour_eur) AS applied_capacity_eur_h, ROUND(( e.effective_duration_s / 3600.0 * COALESCE(e.configured_capacity_eur_h, e.value_hour_eur) )::numeric, 2) AS effective_capacity_cost_eur, e.capacity_cost_confidence, e.order_state_event_id, e.order_context_from, e.order_context_to, e.odoo_state, e.order_present, e.order_coherent, e.odoo_workorder_id, e.workorder_name, e.workorder_state, e.odoo_production_id, e.order_ref, e.product_id, e.product_name, e.qty_production, e.odoo_date_start, e.odoo_date_finished, e.units_per_cycle, e.target_cycle_rate_ppm, CASE WHEN e.deployment_id IS NULL THEN 'CONFIG_MISSING' WHEN e.noise_below_s IS NULL THEN 'POLICY_MISSING' WHEN e.status = 'OPEN' THEN 'OPEN_MONITORING' WHEN e.require_closed_event AND e.status <> 'CLOSED' THEN 'EXCLUDED_NOT_CLOSED' WHEN e.order_state_event_id IS NULL THEN 'EXCLUDED_NO_ORDER_CONTEXT' WHEN e.require_valid_order AND COALESCE(e.order_present, false) = false THEN 'EXCLUDED_NO_VALID_ORDER' WHEN e.require_valid_order AND COALESCE(e.order_coherent, false) = false THEN 'EXCLUDED_ORDER_INCOHERENT' WHEN e.effective_duration_s < e.noise_below_s THEN 'EXCLUDED_NOISE' WHEN e.effective_duration_s < e.operator_individual_from_s THEN 'CANDIDATE_GROUP' ELSE 'CANDIDATE_INDIVIDUAL' END AS eligibility_status, CASE WHEN e.status = 'CLOSED' AND e.order_state_event_id IS NOT NULL AND (NOT e.require_valid_order OR (e.order_present = true AND e.order_coherent = true)) AND e.effective_duration_s >= e.noise_below_s THEN true ELSE false END AS technically_eligible, CASE WHEN e.deployment_mode = 'ACTIVE' AND e.actual_go_live_at IS NOT NULL AND e.started_at >= e.actual_go_live_at AND e.status = 'CLOSED' AND e.order_state_event_id IS NOT NULL AND (NOT e.require_valid_order OR (e.order_present = true AND e.order_coherent = true)) AND e.effective_duration_s >= e.noise_below_s THEN true ELSE false END AS official_ledger_eligible, jsonb_build_object( 'source_stop_id', e.id, 'source_start_payload', e.start_payload_json, 'source_end_payload', e.end_payload_json, 'order_state_event_id', e.order_state_event_id, 'deployment_mode', e.deployment_mode, 'policy', jsonb_build_object( 'noise_below_s', e.noise_below_s, 'aggregate_below_s', e.aggregate_below_s, 'operator_individual_from_s', e.operator_individual_from_s, 'aggregation_window_min', e.aggregation_window_min ) ) AS evidence_json FROM enriched e LEFT JOIN mv_hot.edge_oee_stop_causes c ON c.cause_code = e.cause_code; COMMENT ON VIEW mv_reports_ucepsa_prod.li_stop_candidates_v1 IS 'Clasifica cada paro raw como excluido, agrupable, individual u abierto. Mantiene separados el modo SHADOW y la elegibilidad oficial.'; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_groups_v1 AS WITH candidates AS ( SELECT c.*, COALESCE((c.evidence_json #>> '{policy,aggregation_window_min}')::int, 15) AS aggregation_window_min FROM mv_reports_ucepsa_prod.li_stop_candidates_v1 c WHERE c.eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL') ), bucketed AS ( SELECT c.*, date_bin( make_interval(mins => c.aggregation_window_min), c.started_at, TIMESTAMPTZ '2000-01-01 00:00:00+00' ) AS bucket_start FROM candidates c ), grouped_micro AS ( SELECT tenant, site, deployment_id, ledger_phase, machine_id, asset, order_ref, odoo_production_id, NULLIF(odoo_workorder_id, '')::bigint AS odoo_workorder_id, product_id, product_name, units_per_cycle, target_cycle_rate_ppm, COALESCE(cause_code, 'PENDING') AS cause_code, COALESCE(cause_name, 'Pendiente de clasificar') AS cause_name, classification_status, bucket_start AS group_window_start, COUNT(*)::int AS event_count, MIN(started_at) AS started_at, MAX(ended_at) AS ended_at, SUM(effective_duration_s)::numeric AS duration_s, ROUND(SUM(effective_capacity_cost_eur)::numeric, 2) AS capacity_loss_eur, array_agg(source_stop_id ORDER BY started_at) AS source_stop_ids, bool_and(official_ledger_eligible) AS official_ledger_eligible FROM bucketed WHERE eligibility_status = 'CANDIDATE_GROUP' GROUP BY tenant, site, deployment_id, ledger_phase, machine_id, asset, order_ref, odoo_production_id, odoo_workorder_id, product_id, product_name, units_per_cycle, target_cycle_rate_ppm, COALESCE(cause_code, 'PENDING'), COALESCE(cause_name, 'Pendiente de clasificar'), classification_status, bucket_start ), individual AS ( SELECT tenant, site, deployment_id, ledger_phase, machine_id, asset, order_ref, odoo_production_id, NULLIF(odoo_workorder_id, '')::bigint AS odoo_workorder_id, product_id, product_name, units_per_cycle, target_cycle_rate_ppm, COALESCE(cause_code, 'PENDING') AS cause_code, COALESCE(cause_name, 'Pendiente de clasificar') AS cause_name, classification_status, started_at AS group_window_start, 1::int AS event_count, started_at, ended_at, effective_duration_s::numeric AS duration_s, effective_capacity_cost_eur::numeric AS capacity_loss_eur, ARRAY[source_stop_id]::bigint[] AS source_stop_ids, official_ledger_eligible FROM bucketed WHERE eligibility_status = 'CANDIDATE_INDIVIDUAL' ), combined AS ( SELECT * FROM grouped_micro UNION ALL SELECT * FROM individual ) SELECT CASE WHEN event_count = 1 THEN 'STOP:' || source_stop_ids[1]::text ELSE 'STOPGROUP:' || machine_id || ':' || COALESCE(odoo_production_id::text, COALESCE(order_ref, 'NO_ORDER')) || ':' || to_char(group_window_start AT TIME ZONE 'Europe/Madrid', 'YYYYMMDDHH24MI') || ':' || cause_code END AS source_key, *, ROUND((duration_s / 60.0)::numeric, 2) AS duration_min, CASE WHEN duration_s >= 900 THEN 'HIGH' WHEN duration_s >= 300 THEN 'MEDIUM' ELSE 'LOW' END AS preliminary_priority FROM combined; COMMENT ON VIEW mv_reports_ucepsa_prod.li_stop_groups_v1 IS 'Agrupa microparos por máquina, orden, ventana y causa. Los paros largos permanecen individuales.'; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_shadow_review_queue_v1 AS SELECT row_number() OVER ( ORDER BY capacity_loss_eur DESC, duration_s DESC, started_at DESC ) AS shadow_rank, source_key, machine_id, order_ref, product_name, cause_code, cause_name, event_count, started_at, ended_at, duration_min, capacity_loss_eur, preliminary_priority, 'SHADOW_ONLY_NOT_OFFICIAL'::text AS queue_status, official_ledger_eligible, source_stop_ids FROM mv_reports_ucepsa_prod.li_stop_groups_v1 WHERE ledger_phase = 'SHADOW' ORDER BY shadow_rank; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_operator_stop_queue_v1 AS SELECT source_key, machine_id, order_ref, product_name, cause_code, cause_name, classification_status, event_count, started_at, ended_at, duration_min, capacity_loss_eur, source_stop_ids FROM mv_reports_ucepsa_prod.li_stop_groups_v1 WHERE ledger_phase = 'ACTIVE' AND official_ledger_eligible = true AND classification_status = 'PENDING' ORDER BY capacity_loss_eur DESC, duration_s DESC, started_at DESC; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_daily_review_queue_v1 AS SELECT row_number() OVER ( ORDER BY COALESCE(l.priority_score, 0) DESC, l.review_due_at NULLS LAST, l.detected_at ) AS review_rank, l.loss_id, l.machine_id, l.order_ref, l.product_name, l.loss_type, l.cause_code, ROUND((l.duration_s / 60.0)::numeric, 2) AS duration_min, l.capacity_loss_eur, l.energy_waste_eur, l.priority_level, l.action_candidate, l.action_final, l.review_owner, l.review_due_at, l.status, t.trial_id, t.trial_code, t.result AS trial_result, t.decision AS trial_decision FROM mv_loss_intelligence.loss_ledger l LEFT JOIN LATERAL ( SELECT x.* FROM mv_loss_intelligence.action_trials x WHERE x.loss_id = l.loss_id ORDER BY x.created_at DESC, x.trial_id DESC LIMIT 1 ) t ON true WHERE l.tenant = 'ucepsa' AND l.site = 'ucepsa_onpremise' AND l.ledger_phase = 'ACTIVE' AND l.status NOT IN ('CLOSED', 'DISCARDED') ORDER BY review_rank LIMIT 5; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_shadow_summary_v1 AS SELECT ledger_phase, eligibility_status, machine_id, COUNT(*) AS stop_count, ROUND(SUM(effective_duration_s / 60.0)::numeric, 2) AS total_minutes, ROUND(SUM(effective_capacity_cost_eur)::numeric, 2) AS capacity_cost_eur FROM mv_reports_ucepsa_prod.li_stop_candidates_v1 GROUP BY ledger_phase, eligibility_status, machine_id; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_product_cycle_coverage_v1 AS SELECT c.machine_id, c.odoo_production_id, c.order_ref, c.product_id, c.product_name, c.qty_production, c.units_per_cycle, c.target_cycle_rate_ppm, CASE WHEN c.product_id IS NULL THEN 'MISSING_PRODUCT_ID' WHEN c.units_per_cycle IS NULL THEN 'MISSING_UNITS_PER_CYCLE' WHEN c.target_cycle_rate_ppm IS NULL THEN 'MISSING_TARGET_CYCLE_RATE' ELSE 'READY' END AS cycle_parameter_status, MAX(c.started_at) AS latest_stop_started_at, COUNT(*) AS related_stop_count FROM mv_reports_ucepsa_prod.li_stop_candidates_v1 c WHERE c.order_present = true AND c.order_coherent = true GROUP BY c.machine_id, c.odoo_production_id, c.order_ref, c.product_id, c.product_name, c.qty_production, c.units_per_cycle, c.target_cycle_rate_ppm; CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_data_quality_v1 AS WITH deployment AS ( SELECT COUNT(*) AS n FROM mv_loss_intelligence.deployment_config WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND scope_code = 'CUTTERS' ), capacity_cost AS ( SELECT COUNT(*) AS n FROM mv_loss_intelligence.economic_parameters WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND parameter_code = 'CAPACITY_HOUR_EUR' AND is_active = true ), energy_cost AS ( SELECT COUNT(*) AS n FROM mv_loss_intelligence.economic_parameters WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND parameter_code = 'ENERGY_VARIABLE_EUR_KWH' AND is_active = true ), current_loaded AS ( SELECT COUNT(*) AS n FROM mv_hot.odoo_order_state_current WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND order_present = true AND order_coherent = true ), cycle_missing AS ( SELECT COUNT(*) AS n FROM mv_hot.odoo_order_state_current o WHERE o.tenant = 'ucepsa' AND o.site = 'ucepsa_onpremise' AND o.order_present = true AND o.order_coherent = true AND NOT EXISTS ( SELECT 1 FROM mv_loss_intelligence.product_cycle_parameters p WHERE p.tenant = o.tenant AND p.site = o.site AND p.product_id = o.product_id AND p.is_active = true AND (p.machine_id IS NULL OR p.machine_id = o.machine_id) ) ), stale_open AS ( SELECT COUNT(*) AS n FROM mv_hot.edge_oee_machine_stops WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND status = 'OPEN' AND started_at < now() - interval '12 hours' ) SELECT 'DEPLOYMENT_CONFIG'::text AS check_code, CASE WHEN n = 1 THEN 'OK' ELSE 'BLOCKER' END AS status, n AS affected_count, 'Debe existir una única configuración CUTTERS.'::text AS description FROM deployment UNION ALL SELECT 'CAPACITY_COST', CASE WHEN n >= 1 THEN 'OK' ELSE 'BLOCKER' END, n, 'Debe existir coste de capacidad €/h activo.' FROM capacity_cost UNION ALL SELECT 'ENERGY_COST', CASE WHEN n >= 1 THEN 'OK' ELSE 'BLOCKER' END, n, 'Debe existir coste eléctrico variable €/kWh activo.' FROM energy_cost UNION ALL SELECT 'CURRENT_VALID_ORDERS', CASE WHEN n >= 1 THEN 'OK' ELSE 'INFO' END, n, 'Órdenes actualmente cargadas y coherentes.' FROM current_loaded UNION ALL SELECT 'MISSING_CYCLE_PARAMETERS', CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END, n, 'Productos activos sin bolsas por golpe o cadencia configurada.' FROM cycle_missing UNION ALL SELECT 'STALE_OPEN_STOPS', CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END, n, 'Paros abiertos desde hace más de 12 horas; no equivalen automáticamente a pérdida.' FROM stale_open; DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_candidates_v1 TO grafana_ucepsa_ro; GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_groups_v1 TO grafana_ucepsa_ro; GRANT SELECT ON mv_reports_ucepsa_prod.li_shadow_review_queue_v1 TO grafana_ucepsa_ro; GRANT SELECT ON mv_reports_ucepsa_prod.li_operator_stop_queue_v1 TO grafana_ucepsa_ro; GRANT SELECT ON mv_reports_ucepsa_prod.li_daily_review_queue_v1 TO grafana_ucepsa_ro; GRANT SELECT ON mv_reports_ucepsa_prod.li_shadow_summary_v1 TO grafana_ucepsa_ro; GRANT SELECT ON mv_reports_ucepsa_prod.li_product_cycle_coverage_v1 TO grafana_ucepsa_ro; GRANT SELECT ON mv_reports_ucepsa_prod.li_data_quality_v1 TO grafana_ucepsa_ro; END IF; END $$; COMMIT;