CREATE SCHEMA IF NOT EXISTS mv_hot; DROP VIEW IF EXISTS mv_hot.edge_oee_quality_case_demo CASCADE; DROP VIEW IF EXISTS mv_hot.edge_oee_actions_demo CASCADE; DROP VIEW IF EXISTS mv_hot.edge_oee_data_quality_dashboard_demo CASCADE; DROP VIEW IF EXISTS mv_hot.edge_oee_order_timeline_demo CASCADE; DROP VIEW IF EXISTS mv_hot.edge_oee_pareto_demo CASCADE; DROP VIEW IF EXISTS mv_hot.edge_oee_kpi_cards_demo CASCADE; DROP VIEW IF EXISTS mv_hot.edge_oee_executive_summary_demo CASCADE; CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_machines ( machine_id TEXT PRIMARY KEY, tenant TEXT NOT NULL DEFAULT 'ucepsa', site TEXT NOT NULL DEFAULT 'demo_edge_oee', name TEXT NOT NULL, area TEXT NOT NULL DEFAULT 'corte', machine_type TEXT NOT NULL DEFAULT 'cortadora', value_hour_eur NUMERIC(10,2) NOT NULL, standard_kg_h NUMERIC(10,2), sale_price_eur_kg NUMERIC(10,2), transformation_cost_eur_kg NUMERIC(10,2), raw_material_cost_eur_kg NUMERIC(10,2), notes TEXT ); CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_orders_demo ( order_id TEXT PRIMARY KEY, tenant TEXT NOT NULL DEFAULT 'ucepsa', site TEXT NOT NULL DEFAULT 'demo_edge_oee', machine_id TEXT NOT NULL REFERENCES mv_hot.edge_oee_machines(machine_id), product TEXT NOT NULL, odoo_start TIMESTAMPTZ NOT NULL, odoo_end TIMESTAMPTZ NOT NULL, qty_planned NUMERIC(14,2), qty_done NUMERIC(14,2), scrap_qty NUMERIC(14,2), uom TEXT DEFAULT 'unidades', commercial_note TEXT ); CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_events_demo ( event_id BIGSERIAL PRIMARY KEY, tenant TEXT NOT NULL DEFAULT 'ucepsa', site TEXT NOT NULL DEFAULT 'demo_edge_oee', machine_id TEXT NOT NULL REFERENCES mv_hot.edge_oee_machines(machine_id), order_id TEXT NULL REFERENCES mv_hot.edge_oee_orders_demo(order_id), event_type TEXT NOT NULL, cause TEXT NOT NULL, start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ NOT NULL, duration_min NUMERIC(10,2) NOT NULL, cost_eur NUMERIC(10,2) NOT NULL DEFAULT 0, source TEXT NOT NULL DEFAULT 'mesavault_demo', notes TEXT ); CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_loss_summary_demo ( cause TEXT PRIMARY KEY, hours_month NUMERIC(10,2) NOT NULL, value_hour_eur NUMERIC(10,2) NOT NULL, monthly_cost_eur NUMERIC(10,2) NOT NULL, recommended_action TEXT NOT NULL, effort TEXT, priority TEXT, machine_focus TEXT ); CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_data_quality_demo ( metric_id TEXT PRIMARY KEY, metric TEXT NOT NULL, value_numeric NUMERIC(10,2), value_text TEXT, unit TEXT, status TEXT NOT NULL, problem TEXT, likely_impact TEXT, recommended_action TEXT, sort_order INT NOT NULL ); CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_quality_cases_demo ( case_id TEXT PRIMARY KEY, order_id TEXT REFERENCES mv_hot.edge_oee_orders_demo(order_id), machine_id TEXT REFERENCES mv_hot.edge_oee_machines(machine_id), product TEXT, claim TEXT, sealing_adjustment_stops INT, sealing_adjustment_min NUMERIC(10,2), microstops_count INT, scrap_pct NUMERIC(10,2), energy_vs_avg_pct NUMERIC(10,2), interpretation TEXT, recommended_action TEXT ); CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_energy_demo ( ts TIMESTAMPTZ NOT NULL, tenant TEXT NOT NULL DEFAULT 'ucepsa', site TEXT NOT NULL DEFAULT 'demo_edge_oee', machine_id TEXT NOT NULL REFERENCES mv_hot.edge_oee_machines(machine_id), order_id TEXT NULL REFERENCES mv_hot.edge_oee_orders_demo(order_id), power_kw NUMERIC(10,3), kwh_total NUMERIC(14,3), source TEXT DEFAULT 'demo', payload_json JSONB DEFAULT '{}'::jsonb ); TRUNCATE TABLE mv_hot.edge_oee_energy_demo, mv_hot.edge_oee_quality_cases_demo, mv_hot.edge_oee_data_quality_demo, mv_hot.edge_oee_loss_summary_demo, mv_hot.edge_oee_events_demo, mv_hot.edge_oee_orders_demo, mv_hot.edge_oee_machines RESTART IDENTITY CASCADE; INSERT INTO mv_hot.edge_oee_machines (machine_id, name, value_hour_eur, standard_kg_h, sale_price_eur_kg, transformation_cost_eur_kg, raw_material_cost_eur_kg, notes) VALUES ('C0', 'Cortadora 0', 36.00, 34.90, 3.23, 0.80, 1.25, 'Máquina de alta carga. Valor hora demo prudente.'), ('C1', 'Cortadora 1', 36.00, 34.90, 3.23, 0.80, 1.25, 'Máquina de alta carga. Comparable con C0.'), ('C2', 'Cortadora 2', 36.00, 35.40, 2.80, 0.80, 1.25, 'Máquina crítica por carga y problemas de sellado.'); WITH d AS ( SELECT current_date::timestamp AS base ) INSERT INTO mv_hot.edge_oee_orders_demo (order_id, machine_id, product, odoo_start, odoo_end, qty_planned, qty_done, scrap_qty, uom, commercial_note) SELECT 'OF00481', 'C0', 'Bolsa 300x400', base + interval '08 hours', base + interval '11 hours 30 minutes', 50000, 48900, 1100, 'unidades', 'Orden con arranque real retrasado 22 minutos.' FROM d UNION ALL SELECT 'OF00482', 'C1', 'Bolsa 250x350', base + interval '08 hours 15 minutes', base + interval '12 hours 10 minutes', 42000, 41600, 400, 'unidades', 'Orden casi normal, usada como comparación.' FROM d UNION ALL SELECT 'OF00493', 'C2', 'Bolsa 300x400', base + interval '09 hours', base + interval '13 hours 20 minutes', 52000, 50500, 1500, 'unidades', 'Orden asociada a caso ficticio de sellado débil.' FROM d; WITH d AS ( SELECT current_date::timestamp AS base ) INSERT INTO mv_hot.edge_oee_events_demo (machine_id, order_id, event_type, cause, start_time, end_time, duration_min, cost_eur, notes) SELECT 'C0','OF00481','PREPARATION','Búsqueda de bobina',base + interval '08 hours',base + interval '08 hours 22 minutes',22,13.20,'Odoo inicia a las 08:00, pero la primera marcha real llega a las 08:22.' FROM d UNION ALL SELECT 'C0','OF00481','RUNNING','Producción',base + interval '08 hours 22 minutes',base + interval '09 hours 14 minutes',52,0,'Producción real.' FROM d UNION ALL SELECT 'C0','OF00481','MICROSTOP','Microparos acumulados',base + interval '09 hours 14 minutes',base + interval '09 hours 18 minutes',4,2.40,'Microparos invisibles para un registro manual.' FROM d UNION ALL SELECT 'C0','OF00481','STOP','Cambio bobina',base + interval '10 hours 05 minutes',base + interval '10 hours 18 minutes',13,7.80,'Paro operativo por cambio de bobina.' FROM d UNION ALL SELECT 'C1','OF00482','PREPARATION','Preparación normal',base + interval '08 hours 15 minutes',base + interval '08 hours 18 minutes',3,1.80,'Preparación corta, caso de comparación.' FROM d UNION ALL SELECT 'C1','OF00482','RUNNING','Producción',base + interval '08 hours 18 minutes',base + interval '10 hours 40 minutes',142,0,'Producción estable.' FROM d UNION ALL SELECT 'C1',NULL,'RUNNING_WITHOUT_ORDER','Máquina en marcha sin orden activa',base + interval '12 hours 30 minutes',base + interval '12 hours 56 minutes',26,15.60,'Brecha de dato: producción no imputada a Odoo.' FROM d UNION ALL SELECT 'C2','OF00493','PREPARATION','Ajuste inicial de máquina',base + interval '09 hours',base + interval '09 hours 16 minutes',16,9.60,'Preparación superior a 15 minutos.' FROM d UNION ALL SELECT 'C2','OF00493','RUNNING','Producción',base + interval '09 hours 16 minutes',base + interval '09 hours 40 minutes',24,0,'Producción real.' FROM d UNION ALL SELECT 'C2','OF00493','STOP','Ajuste de sellado',base + interval '09 hours 40 minutes',base + interval '09 hours 52 minutes',12,7.20,'Paro vinculado a sellado.' FROM d UNION ALL SELECT 'C2','OF00493','RUNNING','Producción',base + interval '09 hours 52 minutes',base + interval '10 hours 25 minutes',33,0,'Producción real.' FROM d UNION ALL SELECT 'C2','OF00493','MICROSTOP','Microparos durante orden',base + interval '10 hours 25 minutes',base + interval '10 hours 28 minutes 30 seconds',3.5,2.10,'Microparos durante producto con reclamación.' FROM d UNION ALL SELECT 'C2','OF00493','STOP','Ajuste de sellado',base + interval '11 hours 20 minutes',base + interval '11 hours 32 minutes',12,7.20,'Segundo ajuste de sellado.' FROM d UNION ALL SELECT 'C2','OF00493','STOP','Ajuste de sellado',base + interval '12 hours 04 minutes',base + interval '12 hours 12 minutes',8,4.80,'Tercer ajuste de sellado.' FROM d; INSERT INTO mv_hot.edge_oee_loss_summary_demo (cause, hours_month, value_hour_eur, monthly_cost_eur, recommended_action, effort, priority, machine_focus) VALUES ('Ajuste de sellado', 18.00, 36.00, 648.00, 'Revisar mordazas, teflón, temperatura y presión', 'Medio', 'Alta', 'C2'), ('Espera de material', 14.00, 36.00, 504.00, 'Crear prealistamiento de bobina antes del turno', 'Bajo', 'Alta', 'C0/C1/C2'), ('Microparos acumulados', 11.50, 36.00, 414.00, 'Revisar fotocélula, tensión de material y cuchilla', 'Bajo', 'Alta', 'C0/C2'), ('Cambio bobina/formato', 9.00, 36.00, 324.00, 'Implantar checklist de cambio de bobina/formato', 'Bajo', 'Media', 'C0/C1'), ('Arranque lento de orden', 6.00, 36.00, 216.00, 'Preparar primera orden y material antes del inicio del turno', 'Bajo', 'Media', 'C0/C2'), ('Paro sin causa', 5.00, 36.00, 180.00, 'Mejorar registro de causas y disciplina de imputación', 'Bajo', 'Alta', 'Todas'); INSERT INTO mv_hot.edge_oee_data_quality_demo (metric_id, metric, value_numeric, value_text, unit, status, problem, likely_impact, recommended_action, sort_order) VALUES ('orders_started_no_run_10m', 'Órdenes iniciadas sin marcha en 10 min', 6, NULL, 'órdenes', 'Warning', 'Odoo registra inicio, pero la máquina no produce.', 'El tiempo de preparación queda mezclado con tiempo productivo.', 'Separar preparación, espera de material y marcha real.', 1), ('machine_running_no_order', 'Máquina en marcha sin orden activa', 3, NULL, 'eventos', 'Warning', 'Hay producción física sin orden asignada.', 'Producción no imputada correctamente a Odoo.', 'Bloquear o alertar producción sin orden activa.', 2), ('orders_closed_before_real_end', 'Órdenes cerradas antes de fin real', 2, NULL, 'órdenes', 'Warning', 'El cierre administrativo no coincide con la realidad física.', 'Descuadre de tiempos, coste y trazabilidad.', 'Comparar fin Odoo contra última marcha real.', 3), ('stops_without_cause', 'Paros sin causa', 5, NULL, 'h/mes', 'Warning', 'Hay pérdidas sin clasificación útil.', 'No se puede decidir ninguna acción concreta.', 'Simplificar selección de causas en planta.', 4), ('preparation_gt_15m', 'Órdenes con preparación >15 min', 8, NULL, 'órdenes', 'Warning', 'Arranques lentos repetidos.', 'Pérdida de capacidad al inicio de orden o turno.', 'Prealistamiento de bobinas, útiles y parámetros.', 5), ('energy_above_avg', 'Órdenes con energía superior a media', 5, NULL, 'órdenes', 'Info', 'Consumo superior al patrón esperado.', 'Puede indicar ajustes, fricción, vacío, temperatura o régimen anómalo.', 'Cruzar energía con producto, máquina y causa de paro.', 6); INSERT INTO mv_hot.edge_oee_quality_cases_demo (case_id, order_id, machine_id, product, claim, sealing_adjustment_stops, sealing_adjustment_min, microstops_count, scrap_pct, energy_vs_avg_pct, interpretation, recommended_action) VALUES ( 'QC_SELLADO_OF00493', 'OF00493', 'C2', 'Bolsa 300x400', 'Sellado débil', 4, 32, 23, 2.80, 6.00, 'La reclamación no queda aislada: durante la orden aparecen ajustes de sellado, microparos, merma y energía superior a la media.', 'Revisar mordazas, teflón, temperatura, presión y procedimiento de arranque. Comprobar si el patrón se repite en C2 o con Bolsa 300x400.' ); WITH d AS ( SELECT current_date::timestamp AS base ) INSERT INTO mv_hot.edge_oee_energy_demo (ts, machine_id, order_id, power_kw, kwh_total, source, payload_json) SELECT base + interval '09 hours', 'C2', 'OF00493', 4.800, 102.100, 'demo', '{"note":"inicio orden sellado"}'::jsonb FROM d UNION ALL SELECT base + interval '10 hours', 'C2', 'OF00493', 5.150, 106.900, 'demo', '{"note":"energía sobre media durante ajustes"}'::jsonb FROM d UNION ALL SELECT base + interval '11 hours', 'C2', 'OF00493', 5.090, 112.050, 'demo', '{"note":"energía sobre media"}'::jsonb FROM d UNION ALL SELECT base + interval '12 hours', 'C2', 'OF00493', 5.200, 117.250, 'demo', '{"note":"ajuste sellado"}'::jsonb FROM d; CREATE OR REPLACE VIEW mv_hot.edge_oee_executive_summary_demo AS WITH losses AS ( SELECT SUM(hours_month) AS classified_loss_hours, SUM(monthly_cost_eur) AS estimated_loss_cost_eur FROM mv_hot.edge_oee_loss_summary_demo ), principal AS ( SELECT cause FROM mv_hot.edge_oee_loss_summary_demo ORDER BY monthly_cost_eur DESC LIMIT 1 ) SELECT now() AS evaluated_at, 816.0::numeric(10,1) AS hours_analyzed, losses.classified_loss_hours, losses.estimated_loss_cost_eur, principal.cause AS main_loss_cause, 'Cortadora 2'::text AS machine_with_more_losses, 5.0::numeric(10,1) AS unclassified_stop_hours, 7::int AS orders_with_odoo_machine_discrepancy FROM losses, principal; CREATE OR REPLACE VIEW mv_hot.edge_oee_kpi_cards_demo AS SELECT 1 AS sort_order, 'Horas analizadas' AS kpi, hours_analyzed AS value, 'h' AS unit, 'OK' AS status FROM mv_hot.edge_oee_executive_summary_demo UNION ALL SELECT 2, 'Pérdidas clasificadas', classified_loss_hours, 'h/mes', 'Warning' FROM mv_hot.edge_oee_executive_summary_demo UNION ALL SELECT 3, 'Coste estimado de pérdidas', estimated_loss_cost_eur, '€/mes', 'Warning' FROM mv_hot.edge_oee_executive_summary_demo UNION ALL SELECT 4, 'Paros sin causa', unclassified_stop_hours, 'h/mes', 'Warning' FROM mv_hot.edge_oee_executive_summary_demo UNION ALL SELECT 5, 'Órdenes con discrepancia Odoo/máquina', orders_with_odoo_machine_discrepancy, 'órdenes', 'Warning' FROM mv_hot.edge_oee_executive_summary_demo; CREATE OR REPLACE VIEW mv_hot.edge_oee_pareto_demo AS SELECT RANK() OVER (ORDER BY monthly_cost_eur DESC) AS ranking, cause, hours_month, value_hour_eur, monthly_cost_eur, recommended_action, effort, priority, machine_focus FROM mv_hot.edge_oee_loss_summary_demo ORDER BY monthly_cost_eur DESC; CREATE OR REPLACE VIEW mv_hot.edge_oee_order_timeline_demo AS SELECT o.odoo_start AS time, o.odoo_start + interval '1 minute' AS time_end, o.order_id, o.machine_id, m.name AS machine_name, o.product, 'ODOO' AS source, 'ODOO_START' AS state, 'Odoo inicia la orden' AS detail, 0::numeric AS duration_min, 0::numeric AS cost_eur, 1 AS sort_order FROM mv_hot.edge_oee_orders_demo o JOIN mv_hot.edge_oee_machines m ON m.machine_id = o.machine_id UNION ALL SELECT e.start_time AS time, e.end_time AS time_end, e.order_id, e.machine_id, m.name AS machine_name, COALESCE(o.product, 'Sin orden activa') AS product, 'MESAVAULT' AS source, e.event_type AS state, e.cause AS detail, e.duration_min, e.cost_eur, 2 AS sort_order FROM mv_hot.edge_oee_events_demo e JOIN mv_hot.edge_oee_machines m ON m.machine_id = e.machine_id LEFT JOIN mv_hot.edge_oee_orders_demo o ON o.order_id = e.order_id UNION ALL SELECT o.odoo_end AS time, o.odoo_end + interval '1 minute' AS time_end, o.order_id, o.machine_id, m.name AS machine_name, o.product, 'ODOO' AS source, 'ODOO_END' AS state, 'Odoo finaliza la orden' AS detail, 0::numeric AS duration_min, 0::numeric AS cost_eur, 3 AS sort_order FROM mv_hot.edge_oee_orders_demo o JOIN mv_hot.edge_oee_machines m ON m.machine_id = o.machine_id; CREATE OR REPLACE VIEW mv_hot.edge_oee_data_quality_dashboard_demo AS SELECT sort_order, metric AS "Indicador", COALESCE(value_numeric::text, value_text) AS "Valor", unit AS "Unidad", status AS "Estado", problem AS "Qué está mal", likely_impact AS "Impacto probable", recommended_action AS "Qué revisar / acción" FROM mv_hot.edge_oee_data_quality_demo ORDER BY sort_order; CREATE OR REPLACE VIEW mv_hot.edge_oee_actions_demo AS SELECT cause AS "Causa", monthly_cost_eur AS "Impacto estimado €/mes", effort AS "Esfuerzo", priority AS "Prioridad", recommended_action AS "Acción recomendada", CASE WHEN cause = 'Ajuste de sellado' THEN 4000::numeric ELSE NULL::numeric END AS "Inversión ejemplo €", CASE WHEN cause = 'Ajuste de sellado' THEN ROUND(4000::numeric / NULLIF(monthly_cost_eur, 0), 1) ELSE NULL::numeric END AS "Payback ejemplo meses" FROM mv_hot.edge_oee_loss_summary_demo ORDER BY monthly_cost_eur DESC; CREATE OR REPLACE VIEW mv_hot.edge_oee_quality_case_demo AS SELECT qc.case_id, qc.order_id, m.name AS machine_name, qc.product, qc.claim, qc.sealing_adjustment_stops, qc.sealing_adjustment_min, qc.microstops_count, qc.scrap_pct, qc.energy_vs_avg_pct, qc.interpretation, qc.recommended_action FROM mv_hot.edge_oee_quality_cases_demo qc JOIN mv_hot.edge_oee_machines m ON m.machine_id = qc.machine_id; CREATE INDEX IF NOT EXISTS idx_edge_oee_events_demo_time ON mv_hot.edge_oee_events_demo(start_time DESC); CREATE INDEX IF NOT EXISTS idx_edge_oee_events_demo_machine_time ON mv_hot.edge_oee_events_demo(machine_id, start_time DESC); CREATE INDEX IF NOT EXISTS idx_edge_oee_energy_demo_time ON mv_hot.edge_oee_energy_demo(ts DESC);