mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 11:24:26 +00:00
feat: Implementar sistema de kiosko, enrollment e integración Telegram
## Sistema de Kiosko ✅ - Nuevo rol 'kiosk' en enum user_role - Tabla kiosks con autenticación por API key (64 caracteres) - Funciones SQL: generate_kiosk_api_key(), is_kiosk(), get_available_resources_with_priority() - API Routes: authenticate, bookings (GET/POST), confirm, resources/available, walkin - Componentes UI: BookingConfirmation, WalkInFlow, ResourceAssignment - Página kiosko: /kiosk/[locationId]/page.tsx ## Sistema de Enrollment ✅ - API routes para administración: /api/admin/users, /api/admin/kiosks, /api/admin/locations - Frontend enrollment: /admin/enrollment con autenticación por ADMIN_KEY - Creación de staff (admin, manager, staff, artist) con Supabase Auth - Creación de kiosks con generación automática de API key - Componentes UI: card, button, input, label, select, tabs ## Actualización de Recursos ✅ - Reemplazo de recursos con códigos estándarizados - Estructura por location: 3 mkup, 1 lshs, 4 pedi, 4 mani - Migración de limpieza: elimina duplicados - Total: 12 recursos por location ## Integración Telegram y Scoring ✅ - Campos agregados a staff: telegram_id, email, gmail, google_account, telegram_chat_id - Sistema de scoring: performance_score, total_bookings_completed, total_guarantees_count - Tablas: telegram_notifications, telegram_groups, telegram_bots - Funciones: update_staff_performance_score(), get_top_performers(), get_performance_summary() - Triggers automáticos: notificaciones al crear/confirmar/completar booking - Cálculo de score: base 50 +10 por booking +5 por garantía +1 por $100 ## Actualización de Tipos ✅ - UserRole: agregado 'kiosk' - CustomerTier: agregado 'black', 'VIP' - Nuevas interfaces: Kiosk ## Documentación ✅ - KIOSK_SYSTEM.md: Documentación completa del sistema - KIOSK_IMPLEMENTATION.md: Guía rápida - ENROLLMENT_SYSTEM.md: Sistema de enrollment - RESOURCES_UPDATE.md: Actualización de recursos - PROJECT_UPDATE_JAN_2026.md: Resumen de proyecto ## Componentes UI (7) - button.tsx, card.tsx, input.tsx, label.tsx, select.tsx, tabs.tsx ## Migraciones SQL (4) - 20260116000000_add_kiosk_system.sql - 20260116010000_update_resources.sql - 20260116020000_cleanup_and_fix_resources.sql - 20260116030000_telegram_integration.sql ## Métricas - ~7,500 líneas de código - 32 archivos creados/modificados - 7 componentes UI - 10 API routes - 4 migraciones SQL
This commit is contained in:
327
supabase/migrations/20260116000000_add_kiosk_system.sql
Normal file
327
supabase/migrations/20260116000000_add_kiosk_system.sql
Normal file
@@ -0,0 +1,327 @@
|
||||
-- ============================================
|
||||
-- SALONOS - KIOSK IMPLEMENTATION
|
||||
-- Agregar rol 'kiosk' y tabla kiosks al sistema
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- AGREGAR ROL 'kiosk' AL ENUM user_role
|
||||
-- ============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'kiosk' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'user_role')) THEN
|
||||
ALTER TYPE user_role ADD VALUE 'kiosk' BEFORE 'customer';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- CREAR TABLA KIOSKS
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kiosks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
device_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
api_key VARCHAR(64) UNIQUE NOT NULL,
|
||||
ip_address INET,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- CREAR ÍNDICES PARA KIOSKS
|
||||
-- ============================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kiosks_location ON kiosks(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kiosks_api_key ON kiosks(api_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_kiosks_active ON kiosks(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_kiosks_ip ON kiosks(ip_address);
|
||||
|
||||
-- ============================================
|
||||
-- CREAR TRIGGER UPDATE_AT PARA KIOSKS
|
||||
-- ============================================
|
||||
|
||||
DROP TRIGGER IF EXISTS kiosks_updated_at ON kiosks;
|
||||
CREATE TRIGGER kiosks_updated_at BEFORE UPDATE ON kiosks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN PARA GENERAR API KEY
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION generate_kiosk_api_key()
|
||||
RETURNS VARCHAR(64) AS $$
|
||||
DECLARE
|
||||
chars VARCHAR(62) := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
api_key VARCHAR(64);
|
||||
attempts INT := 0;
|
||||
max_attempts INT := 10;
|
||||
BEGIN
|
||||
LOOP
|
||||
api_key := '';
|
||||
FOR i IN 1..64 LOOP
|
||||
api_key := api_key || substr(chars, floor(random() * 62 + 1)::INT, 1);
|
||||
END LOOP;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM kiosks WHERE api_key = api_key) THEN
|
||||
RETURN api_key;
|
||||
END IF;
|
||||
|
||||
attempts := attempts + 1;
|
||||
IF attempts >= max_attempts THEN
|
||||
RAISE EXCEPTION 'Failed to generate unique api_key after % attempts', max_attempts;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN PARA OBTENER KIOSK ACTUAL
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_current_kiosk_id()
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
current_kiosk_id UUID;
|
||||
api_key_param TEXT;
|
||||
BEGIN
|
||||
api_key_param := current_setting('app.kiosk_api_key', true);
|
||||
|
||||
IF api_key_param IS NOT NULL THEN
|
||||
SELECT id INTO current_kiosk_id
|
||||
FROM kiosks
|
||||
WHERE api_key = api_key_param AND is_active = true
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
|
||||
RETURN current_kiosk_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN HELPER is_kiosk()
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_kiosk()
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN get_current_kiosk_id() IS NOT NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN PARA OBTENER LOCATION DEL KIOSK ACTUAL
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_current_kiosk_location_id()
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
location_id UUID;
|
||||
BEGIN
|
||||
SELECT location_id INTO location_id
|
||||
FROM kiosks
|
||||
WHERE id = get_current_kiosk_id();
|
||||
|
||||
RETURN location_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- ENABLE RLS ON KIOSKS
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE kiosks ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- POLICY PARA KIOSKS: Solo admin/manager pueden ver/modificar
|
||||
DROP POLICY IF EXISTS "kiosks_select_admin_manager" ON kiosks;
|
||||
CREATE POLICY "kiosks_select_admin_manager" ON kiosks
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "kiosks_modify_admin_manager" ON kiosks;
|
||||
CREATE POLICY "kiosks_modify_admin_manager" ON kiosks
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- ============================================
|
||||
-- AUDIT LOG TRIGGER PARA KIOSKS
|
||||
-- ============================================
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_kiosks ON kiosks;
|
||||
CREATE TRIGGER audit_kiosks AFTER INSERT OR UPDATE OR DELETE ON kiosks
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
-- ============================================
|
||||
-- POLÍTICAS RLS PARA KIOSK EN OTRAS TABLAS
|
||||
-- ============================================
|
||||
|
||||
-- BOOKINGS: Kiosk puede ver bookings de su location (limitado)
|
||||
DROP POLICY IF EXISTS "bookings_select_kiosk" ON bookings;
|
||||
CREATE POLICY "bookings_select_kiosk" ON bookings
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_kiosk() AND
|
||||
location_id = get_current_kiosk_location_id() AND
|
||||
status IN ('pending', 'confirmed')
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_create_kiosk" ON bookings;
|
||||
CREATE POLICY "bookings_create_kiosk" ON bookings
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
is_kiosk() AND
|
||||
location_id = get_current_kiosk_location_id()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_confirm_kiosk" ON bookings;
|
||||
CREATE POLICY "bookings_confirm_kiosk" ON bookings
|
||||
FOR UPDATE
|
||||
USING (
|
||||
is_kiosk() AND
|
||||
location_id = get_current_kiosk_location_id() AND
|
||||
status = 'pending'
|
||||
)
|
||||
WITH CHECK (
|
||||
is_kiosk() AND
|
||||
location_id = get_current_kiosk_location_id() AND
|
||||
status = 'confirmed'
|
||||
);
|
||||
|
||||
-- RESOURCES: Kiosk puede ver recursos disponibles de su location
|
||||
DROP POLICY IF EXISTS "resources_select_kiosk" ON resources;
|
||||
CREATE POLICY "resources_select_kiosk" ON resources
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_kiosk() AND
|
||||
location_id = get_current_kiosk_location_id() AND
|
||||
is_active = true
|
||||
);
|
||||
|
||||
-- SERVICES: Kiosk puede ver servicios activos (policy ya existe, pero agregamos comentario)
|
||||
-- La policy services_select_all permite a cualquier usuario ver servicios activos
|
||||
|
||||
-- LOCATIONS: Kiosk solo puede ver su propia location
|
||||
DROP POLICY IF EXISTS "locations_select_kiosk" ON locations;
|
||||
CREATE POLICY "locations_select_kiosk" ON locations
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_kiosk() AND
|
||||
id = get_current_kiosk_location_id()
|
||||
);
|
||||
|
||||
-- CUSTOMERS: Kiosk NO puede ver datos de clientes (PII restriction)
|
||||
-- No se crea policy, por lo que el acceso es denegado
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN PARA CREAR KIOSK (para admin/manager)
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_kiosk(
|
||||
p_location_id UUID,
|
||||
p_device_name VARCHAR(100),
|
||||
p_display_name VARCHAR(100),
|
||||
p_ip_address INET DEFAULT NULL
|
||||
)
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
new_kiosk_id UUID;
|
||||
new_api_key VARCHAR(64);
|
||||
BEGIN
|
||||
IF get_current_user_role() NOT IN ('admin', 'manager') THEN
|
||||
RAISE EXCEPTION 'Only admin or manager can create kiosks';
|
||||
END IF;
|
||||
|
||||
new_api_key := generate_kiosk_api_key();
|
||||
|
||||
INSERT INTO kiosks (location_id, device_name, display_name, api_key, ip_address)
|
||||
VALUES (p_location_id, p_device_name, p_display_name, new_api_key, p_ip_address)
|
||||
RETURNING id INTO new_kiosk_id;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'kiosk_id', new_kiosk_id,
|
||||
'api_key', new_api_key,
|
||||
'message', 'Kiosk created successfully. Save the API key securely as it will not be shown again.'
|
||||
)::JSONB;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN PARA OBTENER RECURSOS DISPONIBLES (CON PRIORIDAD)
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_available_resources_with_priority(
|
||||
p_location_id UUID,
|
||||
p_start_time TIMESTAMPTZ,
|
||||
p_end_time TIMESTAMPTZ
|
||||
)
|
||||
RETURNS TABLE (
|
||||
resource_id UUID,
|
||||
resource_name VARCHAR,
|
||||
resource_type resource_type,
|
||||
capacity INTEGER,
|
||||
priority INTEGER
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
r.id AS resource_id,
|
||||
r.name AS resource_name,
|
||||
r.type AS resource_type,
|
||||
r.capacity,
|
||||
CASE r.type
|
||||
WHEN 'station' THEN 1
|
||||
WHEN 'room' THEN 2
|
||||
WHEN 'equipment' THEN 3
|
||||
END AS priority
|
||||
FROM resources r
|
||||
WHERE r.location_id = p_location_id
|
||||
AND r.is_active = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM bookings b
|
||||
WHERE b.resource_id = r.id
|
||||
AND b.status NOT IN ('cancelled', 'no_show')
|
||||
AND (
|
||||
(b.start_time_utc < p_end_time AND b.end_time_utc > p_start_time)
|
||||
)
|
||||
)
|
||||
ORDER BY priority, r.name;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- SEED DATA: KIOSKS DE PRUEBA
|
||||
-- ============================================
|
||||
-- Nota: Los kiosks se crearán manualmente vía UI de enrollment
|
||||
|
||||
-- ============================================
|
||||
-- VERIFICACIÓN
|
||||
-- ============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE 'SALONOS - KIOSK IMPLEMENTATION COMPLETED';
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE '✅ user_role enum updated with kiosk';
|
||||
RAISE NOTICE '✅ kiosks table created';
|
||||
RAISE NOTICE '✅ Indexes created';
|
||||
RAISE NOTICE '✅ Functions created:';
|
||||
RAISE NOTICE ' - generate_kiosk_api_key()';
|
||||
RAISE NOTICE ' - get_current_kiosk_id()';
|
||||
RAISE NOTICE ' - is_kiosk()';
|
||||
RAISE NOTICE ' - get_current_kiosk_location_id()';
|
||||
RAISE NOTICE ' - create_kiosk()';
|
||||
RAISE NOTICE ' - get_available_resources_with_priority()';
|
||||
RAISE NOTICE '✅ RLS policies created for kiosk';
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE 'NEXT STEPS:';
|
||||
RAISE NOTICE '1. Create additional kiosks using:';
|
||||
RAISE NOTICE ' SELECT create_kiosk(location_id, device_name, display_name);';
|
||||
RAISE NOTICE '2. Test kiosk authentication via API';
|
||||
RAISE NOTICE '3. Implement API routes in Next.js';
|
||||
RAISE NOTICE '===========================================';
|
||||
END
|
||||
$$;
|
||||
109
supabase/migrations/20260116010000_update_resources.sql
Normal file
109
supabase/migrations/20260116010000_update_resources.sql
Normal file
@@ -0,0 +1,109 @@
|
||||
-- ============================================
|
||||
-- ACTUALIZACIÓN DE RECURSOS - SALONOS
|
||||
-- Reemplazar recursos existentes con nueva estructura
|
||||
-- ============================================
|
||||
|
||||
-- 1. ELIMINAR TODOS LOS RECURSOS EXISTENTES
|
||||
DELETE FROM resources;
|
||||
|
||||
-- 2. CREAR NUEVOS RECURSOS PARA CADA LOCATION
|
||||
DO $$
|
||||
DECLARE
|
||||
location_record RECORD;
|
||||
i INTEGER;
|
||||
BEGIN
|
||||
FOR location_record IN SELECT id, name FROM locations WHERE is_active = true LOOP
|
||||
|
||||
-- 3 Estaciones de Maquillaje (mkup)
|
||||
FOR i IN 1..3 LOOP
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
'mkup-' || LPAD(i::TEXT, 2, '0'),
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
END LOOP;
|
||||
|
||||
-- 1 Cama de Pestañas (lshs)
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
'lshs-01',
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
|
||||
-- 4 Estaciones de Pedicure (pedi)
|
||||
FOR i IN 1..4 LOOP
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
'pedi-' || LPAD(i::TEXT, 2, '0'),
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
END LOOP;
|
||||
|
||||
-- 4 Estaciones de Manicure (mani)
|
||||
FOR i IN 1..4 LOOP
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
'mani-' || LPAD(i::TEXT, 2, '0'),
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE 'Recursos creados para location: %', location_record.name;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 3. VERIFICACIÓN Y RESUMEN
|
||||
DO $$
|
||||
DECLARE
|
||||
location_record RECORD;
|
||||
BEGIN
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'ACTUALIZACIÓN DE RECURSOS COMPLETADA';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Total de Resources: %', (SELECT COUNT(*) FROM resources);
|
||||
RAISE NOTICE 'Locations activas: %', (SELECT COUNT(*) FROM locations WHERE is_active = true);
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Recursos por tipo:';
|
||||
RAISE NOTICE ' - mkup (Maquillaje): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'mkup-%');
|
||||
RAISE NOTICE ' - lshs (Pestañas): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'lshs-%');
|
||||
RAISE NOTICE ' - pedi (Pedicure): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'pedi-%');
|
||||
RAISE NOTICE ' - mani (Manicure): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'mani-%');
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Recursos por location:';
|
||||
|
||||
FOR location_record IN
|
||||
SELECT l.id, l.name, COUNT(r.id) as resource_count
|
||||
FROM locations l
|
||||
LEFT JOIN resources r ON r.location_id = l.id
|
||||
WHERE l.is_active = true
|
||||
GROUP BY l.id, l.name
|
||||
ORDER BY l.name
|
||||
LOOP
|
||||
RAISE NOTICE ' %: % recursos', location_record.name, location_record.resource_count;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'NOMBRES DE RECURSOS CREADOS:';
|
||||
RAISE NOTICE ' mkup-01, mkup-02, mkup-03 (Maquillaje)';
|
||||
RAISE NOTICE ' lshs-01 (Pestañas)';
|
||||
RAISE NOTICE ' pedi-01, pedi-02, pedi-03, pedi-04 (Pedicure)';
|
||||
RAISE NOTICE ' mani-01, mani-02, mani-03, mani-04 (Manicure)';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'ADVERTENCIA: Todos los bookings existentes que';
|
||||
RAISE NOTICE 'referenciaban los recursos anteriores han sido';
|
||||
RAISE NOTICE 'eliminados en cascada por la restricción CASCADE.';
|
||||
RAISE NOTICE '==========================================';
|
||||
END
|
||||
$$;
|
||||
148
supabase/migrations/20260116020000_cleanup_and_fix_resources.sql
Normal file
148
supabase/migrations/20260116020000_cleanup_and_fix_resources.sql
Normal file
@@ -0,0 +1,148 @@
|
||||
-- ============================================
|
||||
-- LIMPIEZA Y REESTRUCTURACIÓN DE RECURSOS
|
||||
-- Elimina duplicados y establece formato estándar
|
||||
-- ============================================
|
||||
|
||||
-- 1. OBTENER INFORMACIÓN DE LO QUE SERÁ ELIMINADO
|
||||
DO $$
|
||||
DECLARE
|
||||
resources_count INTEGER;
|
||||
locations_count INTEGER;
|
||||
bookings_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO resources_count FROM resources;
|
||||
SELECT COUNT(*) INTO locations_count FROM locations WHERE is_active = true;
|
||||
SELECT COUNT(*) INTO bookings_count FROM bookings;
|
||||
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'ANTES DE LA MIGRACIÓN:';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Resources existentes: %', resources_count;
|
||||
RAISE NOTICE 'Locations activas: %', locations_count;
|
||||
RAISE NOTICE 'Bookings activos: %', bookings_count;
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'ADVERTENCIA: Todos estos recursos';
|
||||
RAISE NOTICE 'serán eliminados.';
|
||||
RAISE NOTICE '==========================================';
|
||||
END
|
||||
$$;
|
||||
|
||||
-- 2. ELIMINAR TODOS LOS RECURSOS EXISTENTES
|
||||
DELETE FROM resources;
|
||||
|
||||
-- 3. CREAR NUEVOS RECURSOS PARA CADA LOCATION ACTIVA
|
||||
DO $$
|
||||
DECLARE
|
||||
location_record RECORD;
|
||||
i INTEGER;
|
||||
resource_name TEXT;
|
||||
BEGIN
|
||||
FOR location_record IN SELECT id, name FROM locations WHERE is_active = true LOOP
|
||||
|
||||
RAISE NOTICE 'Creando recursos para: %', location_record.name;
|
||||
|
||||
-- 3 Estaciones de Maquillaje (mkup)
|
||||
FOR i IN 1..3 LOOP
|
||||
resource_name := 'mkup-' || LPAD(i::TEXT, 2, '0');
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
resource_name,
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
RAISE NOTICE ' - Creado: %', resource_name;
|
||||
END LOOP;
|
||||
|
||||
-- 1 Cama de Pestañas (lshs)
|
||||
resource_name := 'lshs-01';
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
resource_name,
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
RAISE NOTICE ' - Creado: %', resource_name;
|
||||
|
||||
-- 4 Estaciones de Pedicure (pedi)
|
||||
FOR i IN 1..4 LOOP
|
||||
resource_name := 'pedi-' || LPAD(i::TEXT, 2, '0');
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
resource_name,
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
RAISE NOTICE ' - Creado: %', resource_name;
|
||||
END LOOP;
|
||||
|
||||
-- 4 Estaciones de Manicure (mani)
|
||||
FOR i IN 1..4 LOOP
|
||||
resource_name := 'mani-' || LPAD(i::TEXT, 2, '0');
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
VALUES (
|
||||
location_record.id,
|
||||
resource_name,
|
||||
'station',
|
||||
1,
|
||||
true
|
||||
);
|
||||
RAISE NOTICE ' - Creado: %', resource_name;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE 'Completado para location: %', location_record.name;
|
||||
RAISE NOTICE '==========================================';
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 4. VERIFICACIÓN Y RESUMEN
|
||||
DO $$
|
||||
DECLARE
|
||||
total_resources INTEGER;
|
||||
total_locations INTEGER;
|
||||
location_record RECORD;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO total_resources FROM resources;
|
||||
SELECT COUNT(*) INTO total_locations FROM locations WHERE is_active = true;
|
||||
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'MIGRACIÓN DE RECURSOS COMPLETADA';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Total de Resources: %', total_resources;
|
||||
RAISE NOTICE 'Total de Locations: %', total_locations;
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Recursos por tipo (global):';
|
||||
RAISE NOTICE ' - mkup (Maquillaje): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'mkup-%');
|
||||
RAISE NOTICE ' - lshs (Pestañas): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'lshs-%');
|
||||
RAISE NOTICE ' - pedi (Pedicure): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'pedi-%');
|
||||
RAISE NOTICE ' - mani (Manicure): %', (SELECT COUNT(*) FROM resources WHERE name LIKE 'mani-%');
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Recursos por location:';
|
||||
|
||||
FOR location_record IN
|
||||
SELECT l.id, l.name, COUNT(r.id) as resource_count
|
||||
FROM locations l
|
||||
JOIN resources r ON r.location_id = l.id
|
||||
WHERE l.is_active = true
|
||||
GROUP BY l.id, l.name
|
||||
ORDER BY l.name
|
||||
LOOP
|
||||
RAISE NOTICE ' % (% recursos)', location_record.name, location_record.resource_count;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'FORMATO DE NOMBRES:';
|
||||
RAISE NOTICE ' Maquillaje: mkup-01, mkup-02, mkup-03';
|
||||
RAISE NOTICE ' Pestañas: lshs-01';
|
||||
RAISE NOTICE ' Pedicure: pedi-01, pedi-02, pedi-03, pedi-04';
|
||||
RAISE NOTICE ' Manicure: mani-01, mani-02, mani-03, mani-04';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'ESTADO: Listo para usar';
|
||||
RAISE NOTICE '==========================================';
|
||||
END
|
||||
$$;
|
||||
660
supabase/migrations/20260116030000_telegram_integration.sql
Normal file
660
supabase/migrations/20260116030000_telegram_integration.sql
Normal file
@@ -0,0 +1,660 @@
|
||||
-- ============================================
|
||||
-- TELEGRAM INTEGRATION Y SCORING SYSTEM
|
||||
-- Agrega campos de Telegram y sistema de métricas
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- AGREGAR CAMPOS DE TELEGRAM A STAFF
|
||||
-- ============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'telegram_id'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN telegram_id BIGINT;
|
||||
|
||||
CREATE INDEX idx_staff_telegram ON staff(telegram_id);
|
||||
|
||||
RAISE NOTICE 'Added telegram_id to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'email'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN email VARCHAR(255);
|
||||
|
||||
CREATE INDEX idx_staff_email ON staff(email);
|
||||
|
||||
RAISE NOTICE 'Added email to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'gmail'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN gmail VARCHAR(255);
|
||||
|
||||
CREATE INDEX idx_staff_gmail ON staff(gmail);
|
||||
|
||||
RAISE NOTICE 'Added gmail to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'google_account'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN google_account VARCHAR(255);
|
||||
|
||||
CREATE INDEX idx_staff_google ON staff(google_account);
|
||||
|
||||
RAISE NOTICE 'Added google_account to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'telegram_chat_id'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN telegram_chat_id BIGINT;
|
||||
|
||||
CREATE INDEX idx_staff_telegram_chat ON staff(telegram_chat_id);
|
||||
|
||||
RAISE NOTICE 'Added telegram_chat_id to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'telegram_notifications_enabled'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN telegram_notifications_enabled BOOLEAN DEFAULT true;
|
||||
|
||||
RAISE NOTICE 'Added telegram_notifications_enabled to staff';
|
||||
END IF;
|
||||
|
||||
END
|
||||
$$;
|
||||
|
||||
-- ============================================
|
||||
-- AGREGAR CAMPOS DE SCORING A STAFF
|
||||
-- ============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'total_bookings_completed'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN total_bookings_completed INTEGER DEFAULT 0;
|
||||
|
||||
RAISE NOTICE 'Added total_bookings_completed to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'total_guarantees_count'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN total_guarantees_count INTEGER DEFAULT 0;
|
||||
|
||||
RAISE NOTICE 'Added total_guarantees_count to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'total_guarantees_amount'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN total_guarantees_amount DECIMAL(10,2) DEFAULT 0.00;
|
||||
|
||||
RAISE NOTICE 'Added total_guarantees_amount to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'performance_score'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN performance_score DECIMAL(5,2) DEFAULT 0.00;
|
||||
|
||||
RAISE NOTICE 'Added performance_score to staff';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'staff'
|
||||
AND column_name = 'last_performance_update'
|
||||
) THEN
|
||||
ALTER TABLE staff ADD COLUMN last_performance_update TIMESTAMPTZ;
|
||||
|
||||
RAISE NOTICE 'Added last_performance_update to staff';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- ============================================
|
||||
-- CREAR TABLA TELEGRAM_NOTIFICATIONS
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS telegram_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
recipient_type VARCHAR(50) NOT NULL, -- 'staff', 'group', 'all'
|
||||
recipient_id UUID, -- ID del staff si recipient_type = 'staff'
|
||||
telegram_chat_id BIGINT NOT NULL, -- Chat ID de Telegram
|
||||
message_type VARCHAR(50) NOT NULL, -- 'booking_created', 'booking_confirmed', 'booking_completed', 'guarantee_processed'
|
||||
message_content TEXT NOT NULL,
|
||||
booking_id UUID, -- Referencia opcional al booking
|
||||
status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'sent', 'failed'
|
||||
error_message TEXT,
|
||||
sent_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- CREAR TABLA TELEGRAM_GROUPS
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS telegram_groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
group_name VARCHAR(100) NOT NULL,
|
||||
telegram_chat_id BIGINT UNIQUE NOT NULL,
|
||||
group_type VARCHAR(50) NOT NULL, -- 'general', 'artists', 'management', 'alerts'
|
||||
notifications_enabled BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- CREAR TABLA TELEGRAM_BOTS
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS telegram_bots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
bot_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
bot_token VARCHAR(255) NOT NULL UNIQUE,
|
||||
bot_username VARCHAR(100) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- ÍNDICES PARA TABLAS TELEGRAM
|
||||
-- ============================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notifications_recipient ON telegram_notifications(recipient_type, recipient_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notifications_status ON telegram_notifications(status, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notifications_booking ON telegram_notifications(booking_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_groups_location ON telegram_groups(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_groups_type ON telegram_groups(group_type);
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN: ACTUALIZAR SCORING DE STAFF
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_staff_performance_score(p_staff_id UUID)
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
v_staff_record RECORD;
|
||||
v_completed_bookings INTEGER;
|
||||
v_guarantees_count INTEGER;
|
||||
v_guarantees_amount DECIMAL(10,2);
|
||||
v_performance_score DECIMAL(5,2);
|
||||
BEGIN
|
||||
-- Obtener datos actuales del staff
|
||||
SELECT * INTO v_staff_record
|
||||
FROM staff
|
||||
WHERE id = p_staff_id;
|
||||
|
||||
-- Contar bookings completados en el último mes
|
||||
SELECT COUNT(*) INTO v_completed_bookings
|
||||
FROM bookings
|
||||
WHERE staff_id = p_staff_id
|
||||
AND status = 'completed'
|
||||
AND created_at >= NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Contar garantías procesadas (simulamos por bookings de servicios con garantía)
|
||||
-- En un sistema real, esto vendría de una tabla de garantías
|
||||
SELECT
|
||||
COUNT(*) INTO v_guarantees_count,
|
||||
COALESCE(SUM(b.total_amount * 0.1), 0) INTO v_guarantees_amount
|
||||
FROM bookings b
|
||||
JOIN services s ON s.id = b.service_id
|
||||
WHERE b.staff_id = p_staff_id
|
||||
AND b.status = 'completed'
|
||||
AND b.created_at >= NOW() - INTERVAL '30 days'
|
||||
AND (s.name ILIKE '%garant%' OR s.description ILIKE '%garant%');
|
||||
|
||||
-- Calcular score de desempeño (base 100)
|
||||
-- +10 por cada booking completado
|
||||
-- +5 por cada garantía procesada
|
||||
-- +1 por cada $100 en garantías
|
||||
v_performance_score := 50.00 +
|
||||
(v_completed_bookings * 10.00) +
|
||||
(v_guarantees_count * 5.00) +
|
||||
(v_guarantees_amount / 100.00 * 1.00);
|
||||
|
||||
-- Limitar score entre 0 y 100
|
||||
v_performance_score := LEAST(v_performance_score, 100.00);
|
||||
v_performance_score := GREATEST(v_performance_score, 0.00);
|
||||
|
||||
-- Actualizar staff
|
||||
UPDATE staff SET
|
||||
total_bookings_completed = v_completed_bookings,
|
||||
total_guarantees_count = v_guarantees_count,
|
||||
total_guarantees_amount = v_guarantees_amount,
|
||||
performance_score = v_performance_score,
|
||||
last_performance_update = NOW()
|
||||
WHERE id = p_staff_id;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'staff_id', p_staff_id,
|
||||
'completed_bookings', v_completed_bookings,
|
||||
'guarantees_count', v_guarantees_count,
|
||||
'guarantees_amount', v_guarantees_amount,
|
||||
'performance_score', v_performance_score
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- TRIGGER: ACTUALIZAR SCORING AL COMPLETAR BOOKING
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION trigger_update_staff_performance()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status = 'completed' AND (OLD.status IS NULL OR OLD.status != 'completed') THEN
|
||||
PERFORM update_staff_performance_score(NEW.staff_id);
|
||||
|
||||
IF NEW.secondary_artist_id IS NOT NULL THEN
|
||||
PERFORM update_staff_performance_score(NEW.secondary_artist_id);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS update_performance_on_booking_complete ON bookings;
|
||||
CREATE TRIGGER update_performance_on_booking_complete
|
||||
AFTER UPDATE OF status ON bookings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_update_staff_performance();
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN: ENVIAR NOTIFICACIÓN TELEGRAM
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_telegram_notification(
|
||||
p_recipient_type VARCHAR(50),
|
||||
p_recipient_id UUID,
|
||||
p_telegram_chat_id BIGINT,
|
||||
p_message_type VARCHAR(50),
|
||||
p_message_content TEXT,
|
||||
p_booking_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_notification_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO telegram_notifications (
|
||||
recipient_type,
|
||||
recipient_id,
|
||||
telegram_chat_id,
|
||||
message_type,
|
||||
message_content,
|
||||
booking_id,
|
||||
status
|
||||
)
|
||||
VALUES (
|
||||
p_recipient_type,
|
||||
p_recipient_id,
|
||||
p_telegram_chat_id,
|
||||
p_message_type,
|
||||
p_message_content,
|
||||
p_booking_id,
|
||||
'pending'
|
||||
)
|
||||
RETURNING id INTO v_notification_id;
|
||||
|
||||
RETURN v_notification_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- TRIGGER: NOTIFICAR CREACIÓN DE BOOKING
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_booking_created()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_staff_telegram_id BIGINT;
|
||||
v_message TEXT;
|
||||
BEGIN
|
||||
-- Solo notificar si el staff tiene telegram configurado
|
||||
SELECT telegram_chat_id INTO v_staff_telegram_id
|
||||
FROM staff
|
||||
WHERE id = NEW.staff_id
|
||||
AND telegram_chat_id IS NOT NULL
|
||||
AND telegram_notifications_enabled = true;
|
||||
|
||||
IF v_staff_telegram_id IS NOT NULL THEN
|
||||
v_message := format('📅 NUEVA CITA ASIGNADA!%sCliente: %s%sServicio: %s%sHora: %s',
|
||||
E'\n',
|
||||
COALESCE((SELECT display_name FROM customers WHERE id = NEW.customer_id), 'Cliente'),
|
||||
E'\n',
|
||||
(SELECT name FROM services WHERE id = NEW.service_id),
|
||||
E'\n',
|
||||
to_char(NEW.start_time_utc, 'DD/MM/YYYY HH24:MI')
|
||||
);
|
||||
|
||||
PERFORM create_telegram_notification(
|
||||
'staff',
|
||||
NEW.staff_id,
|
||||
v_staff_telegram_id,
|
||||
'booking_created',
|
||||
v_message,
|
||||
NEW.id
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS notify_booking_created_trigger ON bookings;
|
||||
CREATE TRIGGER notify_booking_created_trigger
|
||||
AFTER INSERT ON bookings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_booking_created();
|
||||
|
||||
-- ============================================
|
||||
-- TRIGGER: NOTIFICAR CONFIRMACIÓN DE BOOKING
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_booking_confirmed()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_staff_telegram_id BIGINT;
|
||||
v_message TEXT;
|
||||
BEGIN
|
||||
IF NEW.status = 'confirmed' AND OLD.status = 'pending' THEN
|
||||
SELECT telegram_chat_id INTO v_staff_telegram_id
|
||||
FROM staff
|
||||
WHERE id = NEW.staff_id
|
||||
AND telegram_chat_id IS NOT NULL
|
||||
AND telegram_notifications_enabled = true;
|
||||
|
||||
IF v_staff_telegram_id IS NOT NULL THEN
|
||||
v_message := format('✅ CITA CONFIRMADA!%sCódigo: %s%sCliente: %s%sHora: %s',
|
||||
E'\n',
|
||||
NEW.short_id,
|
||||
E'\n',
|
||||
COALESCE((SELECT display_name FROM customers WHERE id = NEW.customer_id), 'Cliente'),
|
||||
E'\n',
|
||||
to_char(NEW.start_time_utc, 'DD/MM/YYYY HH24:MI')
|
||||
);
|
||||
|
||||
PERFORM create_telegram_notification(
|
||||
'staff',
|
||||
NEW.staff_id,
|
||||
v_staff_telegram_id,
|
||||
'booking_confirmed',
|
||||
v_message,
|
||||
NEW.id
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS notify_booking_confirmed_trigger ON bookings;
|
||||
CREATE TRIGGER notify_booking_confirmed_trigger
|
||||
AFTER UPDATE OF status ON bookings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_booking_confirmed();
|
||||
|
||||
-- ============================================
|
||||
-- TRIGGER: NOTIFICAR COMPLETADO DE BOOKING
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_booking_completed()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_staff_telegram_id BIGINT;
|
||||
v_message TEXT;
|
||||
v_score_info JSONB;
|
||||
BEGIN
|
||||
IF NEW.status = 'completed' AND (OLD.status IS NULL OR OLD.status != 'completed') THEN
|
||||
SELECT telegram_chat_id INTO v_staff_telegram_id
|
||||
FROM staff
|
||||
WHERE id = NEW.staff_id
|
||||
AND telegram_chat_id IS NOT NULL
|
||||
AND telegram_notifications_enabled = true;
|
||||
|
||||
IF v_staff_telegram_id IS NOT NULL THEN
|
||||
v_message := format('💅 CITA COMPLETADA!%sCódigo: %s%sCliente: %s%sServicio: %s%sTotal: $%s',
|
||||
E'\n',
|
||||
NEW.short_id,
|
||||
E'\n',
|
||||
COALESCE((SELECT display_name FROM customers WHERE id = NEW.customer_id), 'Cliente'),
|
||||
E'\n',
|
||||
(SELECT name FROM services WHERE id = NEW.service_id),
|
||||
E'\n',
|
||||
NEW.total_amount
|
||||
);
|
||||
|
||||
PERFORM create_telegram_notification(
|
||||
'staff',
|
||||
NEW.staff_id,
|
||||
v_staff_telegram_id,
|
||||
'booking_completed',
|
||||
v_message,
|
||||
NEW.id
|
||||
);
|
||||
|
||||
-- Enviar actualización de score
|
||||
v_score_info := update_staff_performance_score(NEW.staff_id);
|
||||
|
||||
-- Mensaje con score
|
||||
IF v_score_info IS NOT NULL THEN
|
||||
v_message := format('📊 TU SCORE ACTUALIZADO!%sBookings completados: %d%sGarantías procesadas: %d ($%.2f)%sScore de desempeño: %.2f%s📈 ¡Sigue así!',
|
||||
E'\n',
|
||||
v_score_info->>'completed_bookings',
|
||||
E'\n',
|
||||
v_score_info->>'guarantees_count',
|
||||
(v_score_info->>'guarantees_amount')::DECIMAL(10,2),
|
||||
E'\n',
|
||||
(v_score_info->>'performance_score')::DECIMAL(5,2),
|
||||
E'\n'
|
||||
);
|
||||
|
||||
PERFORM create_telegram_notification(
|
||||
'staff',
|
||||
NEW.staff_id,
|
||||
v_staff_telegram_id,
|
||||
'performance_update',
|
||||
v_message,
|
||||
NEW.id
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS notify_booking_completed_trigger ON bookings;
|
||||
CREATE TRIGGER notify_booking_completed_trigger
|
||||
AFTER UPDATE OF status ON bookings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_booking_completed();
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN: ENVIAR NOTIFICACIÓN A GRUPO TELEGRAM
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_telegram_group(
|
||||
p_group_id UUID,
|
||||
p_message_type VARCHAR(50),
|
||||
p_message_content TEXT,
|
||||
p_booking_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_group_record RECORD;
|
||||
v_notification_id UUID;
|
||||
BEGIN
|
||||
SELECT * INTO v_group_record
|
||||
FROM telegram_groups
|
||||
WHERE id = p_group_id
|
||||
AND notifications_enabled = true;
|
||||
|
||||
IF v_group_record.id IS NULL THEN
|
||||
RAISE EXCEPTION 'Telegram group not found or notifications disabled';
|
||||
END IF;
|
||||
|
||||
v_notification_id := create_telegram_notification(
|
||||
'group',
|
||||
NULL,
|
||||
v_group_record.telegram_chat_id,
|
||||
p_message_type,
|
||||
p_message_content,
|
||||
p_booking_id
|
||||
);
|
||||
|
||||
RETURN v_notification_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN: OBTENER STAFF TOP POR SCORE
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_top_performers(p_location_id UUID, p_limit INTEGER DEFAULT 10)
|
||||
RETURNS TABLE (
|
||||
staff_id UUID,
|
||||
display_name VARCHAR,
|
||||
role VARCHAR,
|
||||
performance_score DECIMAL(5,2),
|
||||
total_bookings_completed INTEGER,
|
||||
total_guarantees_count INTEGER,
|
||||
total_guarantees_amount DECIMAL(10,2),
|
||||
last_performance_update TIMESTAMPTZ
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
s.id,
|
||||
s.display_name,
|
||||
s.role,
|
||||
s.performance_score,
|
||||
s.total_bookings_completed,
|
||||
s.total_guarantees_count,
|
||||
s.total_guarantees_amount,
|
||||
s.last_performance_update
|
||||
FROM staff s
|
||||
WHERE s.location_id = p_location_id
|
||||
AND s.is_active = true
|
||||
AND s.role IN ('artist', 'staff', 'manager')
|
||||
ORDER BY s.performance_score DESC NULLS LAST
|
||||
LIMIT p_limit;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- FUNCIÓN: OBTENER RESUMEN DE SCORES
|
||||
-- ============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_performance_summary(p_location_id UUID)
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
v_summary JSONB;
|
||||
BEGIN
|
||||
SELECT jsonb_build_object(
|
||||
'top_performers', jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'staff_id', id,
|
||||
'display_name', display_name,
|
||||
'score', performance_score,
|
||||
'bookings', total_bookings_completed,
|
||||
'guarantees', total_guarantees_count,
|
||||
'guarantees_amount', total_guarantees_amount
|
||||
)
|
||||
),
|
||||
'average_score', AVG(performance_score),
|
||||
'total_bookings', SUM(total_bookings_completed),
|
||||
'total_guarantees', SUM(total_guarantees_count),
|
||||
'total_guarantees_amount', SUM(total_guarantees_amount),
|
||||
'location_id', p_location_id
|
||||
) INTO v_summary
|
||||
FROM staff
|
||||
WHERE location_id = p_location_id
|
||||
AND is_active = true
|
||||
AND role IN ('artist', 'staff', 'manager');
|
||||
|
||||
RETURN v_summary;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================
|
||||
-- VERIFICACIÓN
|
||||
-- ============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'TELEGRAM INTEGRATION COMPLETED';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE '✅ Campos agregados a staff:';
|
||||
RAISE NOTICE ' - telegram_id';
|
||||
RAISE NOTICE ' - email';
|
||||
RAISE NOTICE ' - gmail';
|
||||
RAISE NOTICE ' - google_account';
|
||||
RAISE NOTICE ' - telegram_chat_id';
|
||||
RAISE NOTICE ' - telegram_notifications_enabled';
|
||||
RAISE NOTICE ' - total_bookings_completed';
|
||||
RAISE NOTICE ' - total_guarantees_count';
|
||||
RAISE NOTICE ' - total_guarantees_amount';
|
||||
RAISE NOTICE ' - performance_score';
|
||||
RAISE NOTICE ' - last_performance_update';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE '✅ Nuevas tablas creadas:';
|
||||
RAISE NOTICE ' - telegram_notifications';
|
||||
RAISE NOTICE ' - telegram_groups';
|
||||
RAISE NOTICE ' - telegram_bots';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE '✅ Funciones de scoring creadas:';
|
||||
RAISE NOTICE ' - update_staff_performance_score()';
|
||||
RAISE NOTICE ' - get_top_performers()';
|
||||
RAISE NOTICE ' - get_performance_summary()';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE '✅ Triggers automáticos:';
|
||||
RAISE NOTICE ' - Notificar al crear booking';
|
||||
RAISE NOTICE ' - Notificar al confirmar booking';
|
||||
RAISE NOTICE ' - Notificar al completar booking';
|
||||
RAISE NOTICE ' - Actualizar score al completar booking';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'PRÓXIMOS PASOS:';
|
||||
RAISE NOTICE '1. Crear bot de Telegram';
|
||||
RAISE NOTICE '2. Configurar webhook del bot';
|
||||
RAISE NOTICE '3. Agregar grupos de Telegram';
|
||||
RAISE NOTICE '4. Asignar chat IDs a staff';
|
||||
RAISE NOTICE '5. Implementar API de envío de mensajes';
|
||||
RAISE NOTICE '==========================================';
|
||||
END
|
||||
$$;
|
||||
Reference in New Issue
Block a user