CREATE SCHEMA IF NOT EXISTS mv_hot; CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_stop_causes ( cause_code TEXT PRIMARY KEY, cause_name TEXT NOT NULL, description TEXT, is_planned BOOLEAN NOT NULL DEFAULT false, is_active BOOLEAN NOT NULL DEFAULT true, sort_order INT NOT NULL DEFAULT 100 ); INSERT INTO mv_hot.edge_oee_stop_causes (cause_code, cause_name, description, is_planned, sort_order) VALUES ('cambio_bobina', 'Cambio de bobina', 'Paro por cambio de bobina o material.', false, 10), ('fallo_empalme', 'Fallo en empalme', 'Paro por fallo o ajuste en empalme de material.', false, 20), ('ajuste_sellado', 'Ajuste de sellado', 'Paro por ajuste de mordazas, temperatura, presión o teflón.', false, 30), ('falta_material', 'Falta de material', 'Paro por ausencia de bobina, material no preparado o espera logística.', false, 40), ('limpieza_fotocelula', 'Limpieza fotocélula', 'Paro por limpieza o ajuste de sensor/fotocélula.', false, 50), ('cambio_formato', 'Cambio de formato', 'Paro por cambio de medidas, producto o configuración.', false, 60), ('averia_mecanica', 'Avería mecánica', 'Paro por fallo mecánico de la máquina.', false, 70), ('espera_instrucciones', 'Espera de instrucciones', 'Paro por falta de definición, autorización o instrucción.', false, 80), ('pausa_cafe', 'Pausa café', 'Pausa del operario o descanso.', true, 90), ('otro', 'Otro', 'Causa no clasificada en el catálogo principal.', false, 999) ON CONFLICT (cause_code) DO UPDATE SET cause_name = EXCLUDED.cause_name, description = EXCLUDED.description, is_planned = EXCLUDED.is_planned, sort_order = EXCLUDED.sort_order; CREATE TABLE IF NOT EXISTS mv_hot.edge_oee_machine_stops ( id BIGSERIAL PRIMARY KEY, tenant TEXT NOT NULL, site TEXT NOT NULL, vertical TEXT NOT NULL, asset TEXT NOT NULL, machine_id TEXT, order_id TEXT, started_at TIMESTAMPTZ NOT NULL, ended_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'OPEN', classification_status TEXT NOT NULL DEFAULT 'PENDING', cause_code TEXT REFERENCES mv_hot.edge_oee_stop_causes(cause_code), operator_note TEXT, value_hour_eur NUMERIC(10,2) NOT NULL DEFAULT 36.00, duration_s NUMERIC(14,2), cost_eur NUMERIC(14,2), start_event_name TEXT, end_event_name TEXT, start_payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, end_payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_edge_oee_machine_stops_asset_status ON mv_hot.edge_oee_machine_stops(asset, status); CREATE INDEX IF NOT EXISTS idx_edge_oee_machine_stops_started_at ON mv_hot.edge_oee_machine_stops(started_at DESC); CREATE INDEX IF NOT EXISTS idx_edge_oee_machine_stops_machine_started ON mv_hot.edge_oee_machine_stops(machine_id, started_at DESC); CREATE OR REPLACE VIEW mv_hot.edge_oee_open_stops AS SELECT s.id, s.tenant, s.site, s.vertical, s.asset, s.machine_id, s.order_id, s.started_at, EXTRACT(EPOCH FROM (now() - s.started_at))::numeric(14,2) AS current_duration_s, ROUND((EXTRACT(EPOCH FROM (now() - s.started_at)) / 60.0)::numeric, 2) AS current_duration_min, ROUND(((EXTRACT(EPOCH FROM (now() - s.started_at)) / 3600.0) * s.value_hour_eur)::numeric, 2) AS current_cost_eur, s.status, s.classification_status, s.cause_code, c.cause_name, s.operator_note, s.value_hour_eur FROM mv_hot.edge_oee_machine_stops s LEFT JOIN mv_hot.edge_oee_stop_causes c ON c.cause_code = s.cause_code WHERE s.status = 'OPEN' ORDER BY s.started_at DESC; CREATE OR REPLACE VIEW mv_hot.edge_oee_stops_recent AS SELECT s.id, s.tenant, s.site, s.vertical, s.asset, s.machine_id, s.order_id, s.started_at, s.ended_at, COALESCE( s.duration_s, EXTRACT(EPOCH FROM (now() - s.started_at))::numeric(14,2) ) AS duration_s, ROUND(( COALESCE(s.duration_s, EXTRACT(EPOCH FROM (now() - s.started_at))::numeric) / 60.0 )::numeric, 2) AS duration_min, COALESCE( s.cost_eur, ROUND(((EXTRACT(EPOCH FROM (now() - s.started_at)) / 3600.0) * s.value_hour_eur)::numeric, 2) ) AS cost_eur, s.status, s.classification_status, s.cause_code, c.cause_name, c.is_planned, s.operator_note, s.value_hour_eur, s.start_event_name, s.end_event_name, s.created_at, s.updated_at FROM mv_hot.edge_oee_machine_stops s LEFT JOIN mv_hot.edge_oee_stop_causes c ON c.cause_code = s.cause_code ORDER BY s.started_at DESC; CREATE OR REPLACE VIEW mv_hot.edge_oee_stop_summary_demo AS SELECT COALESCE(c.cause_name, 'Pendiente de clasificar') AS cause_name, COALESCE(s.cause_code, 'pending') AS cause_code, COUNT(*) AS stop_count, ROUND(SUM(COALESCE(s.duration_s, EXTRACT(EPOCH FROM (now() - s.started_at))::numeric)) / 60.0, 2) AS total_min, ROUND(SUM(COALESCE(s.cost_eur, ((EXTRACT(EPOCH FROM (now() - s.started_at)) / 3600.0) * s.value_hour_eur)))::numeric, 2) AS total_cost_eur FROM mv_hot.edge_oee_machine_stops s LEFT JOIN mv_hot.edge_oee_stop_causes c ON c.cause_code = s.cause_code GROUP BY COALESCE(c.cause_name, 'Pendiente de clasificar'), COALESCE(s.cause_code, 'pending') ORDER BY total_cost_eur DESC; CREATE OR REPLACE VIEW mv_hot.edge_oee_stop_cause_options AS SELECT cause_code, cause_name, description, is_planned, sort_order FROM mv_hot.edge_oee_stop_causes WHERE is_active = true ORDER BY sort_order, cause_name;