CREATE OR REPLACE FUNCTION mv_reports_ucepsa_demo.oee_kpis( p_from timestamptz, p_to timestamptz, p_machine_id text DEFAULT 'CORT-01', p_asset text DEFAULT 'revpi_oee_node_01' ) RETURNS TABLE ( oee_pct numeric, availability_pct numeric, performance_pct numeric, quality_pct numeric ) LANGUAGE sql STABLE AS $$ WITH dashboard_bounds AS ( SELECT p_from AS dashboard_from, LEAST(p_to, now()) AS dashboard_to ), telemetry_span AS ( SELECT MIN(ingest_ts) AS first_ts, MAX(ingest_ts) AS last_ts FROM mv_hot.edge_oee_live_telemetry WHERE asset = p_asset AND ingest_ts >= (SELECT dashboard_from FROM dashboard_bounds) AND ingest_ts <= (SELECT dashboard_to FROM dashboard_bounds) ), bounds AS ( SELECT CASE WHEN first_ts IS NULL THEN dashboard_from ELSE GREATEST(dashboard_from, first_ts) END AS t_from, CASE WHEN last_ts IS NULL THEN dashboard_to ELSE LEAST(dashboard_to, last_ts) END AS t_to FROM dashboard_bounds CROSS JOIN telemetry_span ), target AS ( SELECT COALESCE(( SELECT mt.target_cycle_rate_ppm FROM mv_hot.edge_oee_machine_targets mt WHERE mt.machine_id = p_machine_id LIMIT 1 ), 10.00)::numeric AS target_cycle_rate_ppm, COALESCE(( SELECT mt.quality_pct_default FROM mv_hot.edge_oee_machine_targets mt WHERE mt.machine_id = p_machine_id LIMIT 1 ), 100.00)::numeric AS quality_pct_default ), planned AS ( SELECT GREATEST(EXTRACT(EPOCH FROM (t_to - t_from))::numeric, 0) AS planned_s FROM bounds ), stops AS ( SELECT COALESCE( SUM( GREATEST( EXTRACT(EPOCH FROM ( LEAST(COALESCE(s.ended_at, b.t_to), b.t_to) - GREATEST( CASE WHEN s.started_at > s.created_at + interval '5 seconds' THEN s.created_at ELSE s.started_at END, b.t_from ) ))::numeric, 0 ) ) FILTER ( WHERE s.started_at < b.t_to AND COALESCE(s.ended_at, now()) > b.t_from ), 0 )::numeric AS stop_s FROM mv_hot.edge_oee_machine_stops s CROSS JOIN bounds b WHERE s.machine_id = p_machine_id ), cycles AS ( SELECT COALESCE( GREATEST( MAX(cycle_pulse_count) - MIN(cycle_pulse_count), 0 ), 0 )::numeric AS actual_cycles FROM mv_hot.edge_oee_live_telemetry l CROSS JOIN bounds b WHERE l.asset = p_asset AND l.ingest_ts >= b.t_from AND l.ingest_ts <= b.t_to AND l.cycle_pulse_count IS NOT NULL ), calc AS ( SELECT p.planned_s, st.stop_s, GREATEST(p.planned_s - st.stop_s, 0)::numeric AS running_s, c.actual_cycles, t.target_cycle_rate_ppm, t.quality_pct_default, CASE WHEN p.planned_s > 0 THEN ROUND(((GREATEST(p.planned_s - st.stop_s, 0) / p.planned_s) * 100)::numeric, 2) ELSE 0 END AS availability_pct, CASE WHEN GREATEST(p.planned_s - st.stop_s, 0) > 0 AND t.target_cycle_rate_ppm > 0 THEN ROUND( LEAST( 100::numeric, ( c.actual_cycles / NULLIF( t.target_cycle_rate_ppm * (GREATEST(p.planned_s - st.stop_s, 0) / 60.0), 0 ) ) * 100 )::numeric, 2 ) ELSE 0 END AS performance_pct, t.quality_pct_default AS quality_pct FROM planned p CROSS JOIN stops st CROSS JOIN cycles c CROSS JOIN target t ) SELECT ROUND(((calc.availability_pct * calc.performance_pct * calc.quality_pct) / 10000.0)::numeric, 2) AS oee_pct, calc.availability_pct, calc.performance_pct, calc.quality_pct FROM calc; $$; CREATE OR REPLACE FUNCTION mv_reports_ucepsa_demo.oee_operational_data( p_from timestamptz, p_to timestamptz, p_machine_id text DEFAULT 'CORT-01', p_asset text DEFAULT 'revpi_oee_node_01' ) RETURNS TABLE ( running_min numeric, stop_min numeric, actual_cycles numeric ) LANGUAGE sql STABLE AS $$ WITH dashboard_bounds AS ( SELECT p_from AS dashboard_from, LEAST(p_to, now()) AS dashboard_to ), telemetry_span AS ( SELECT MIN(ingest_ts) AS first_ts, MAX(ingest_ts) AS last_ts FROM mv_hot.edge_oee_live_telemetry WHERE asset = p_asset AND ingest_ts >= (SELECT dashboard_from FROM dashboard_bounds) AND ingest_ts <= (SELECT dashboard_to FROM dashboard_bounds) ), bounds AS ( SELECT CASE WHEN first_ts IS NULL THEN dashboard_from ELSE GREATEST(dashboard_from, first_ts) END AS t_from, CASE WHEN last_ts IS NULL THEN dashboard_to ELSE LEAST(dashboard_to, last_ts) END AS t_to FROM dashboard_bounds CROSS JOIN telemetry_span ), planned AS ( SELECT GREATEST(EXTRACT(EPOCH FROM (t_to - t_from))::numeric, 0) AS planned_s FROM bounds ), stops AS ( SELECT COALESCE( SUM( GREATEST( EXTRACT(EPOCH FROM ( LEAST(COALESCE(s.ended_at, b.t_to), b.t_to) - GREATEST( CASE WHEN s.started_at > s.created_at + interval '5 seconds' THEN s.created_at ELSE s.started_at END, b.t_from ) ))::numeric, 0 ) ) FILTER ( WHERE s.started_at < b.t_to AND COALESCE(s.ended_at, now()) > b.t_from ), 0 )::numeric AS stop_s FROM mv_hot.edge_oee_machine_stops s CROSS JOIN bounds b WHERE s.machine_id = p_machine_id ), cycles AS ( SELECT COALESCE( GREATEST( MAX(cycle_pulse_count) - MIN(cycle_pulse_count), 0 ), 0 )::numeric AS actual_cycles FROM mv_hot.edge_oee_live_telemetry l CROSS JOIN bounds b WHERE l.asset = p_asset AND l.ingest_ts >= b.t_from AND l.ingest_ts <= b.t_to AND l.cycle_pulse_count IS NOT NULL ), calc AS ( SELECT p.planned_s, st.stop_s, GREATEST(p.planned_s - st.stop_s, 0)::numeric AS running_s, c.actual_cycles FROM planned p CROSS JOIN stops st CROSS JOIN cycles c ) SELECT ROUND((running_s / 60.0)::numeric, 2) AS running_min, ROUND((stop_s / 60.0)::numeric, 2) AS stop_min, actual_cycles FROM calc; $$; GRANT EXECUTE ON FUNCTION mv_reports_ucepsa_demo.oee_kpis(timestamptz, timestamptz, text, text) TO grafana_ucepsa_ro; GRANT EXECUTE ON FUNCTION mv_reports_ucepsa_demo.oee_operational_data(timestamptz, timestamptz, text, text) TO grafana_ucepsa_ro;