feat: implement customer registration flow and business hours system

Major changes:
- Add customer registration with email/phone lookup (app/booking/registro)
- Add customers API endpoint (app/api/customers/route)
- Implement business hours for locations (mon-fri 10-7, sat 10-6, sun closed)
- Fix availability function type casting issues
- Add business hours utilities (lib/utils/business-hours.ts)
- Update Location type to include business_hours JSONB
- Add mock payment component for testing
- Remove Supabase auth from booking flow
- Fix /cita redirect path in booking flow

Database migrations:
- Add category column to services table
- Add business_hours JSONB column to locations table
- Fix availability functions with proper type casting
- Update get_detailed_availability to use business_hours

Features:
- Customer lookup by email or phone
- Auto-redirect to registration if customer not found
- Pre-fill customer data if exists
- Business hours per day of week
- Location-specific opening/closing times
This commit is contained in:
Marco Gallegos
2026-01-17 00:29:49 -06:00
parent fb60178c86
commit 583a25a6f6
56 changed files with 2676 additions and 491 deletions

View File

@@ -0,0 +1,33 @@
-- Ejecutar este SQL en Supabase Dashboard: Database > SQL Editor
-- Agrega horarios de atención a la tabla locations
-- Agregar columna business_hours
ALTER TABLE locations
ADD COLUMN IF NOT EXISTS business_hours JSONB DEFAULT '{
"monday": {"open": "10:00", "close": "19:00", "is_closed": false},
"tuesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"wednesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"thursday": {"open": "10:00", "close": "19:00", "is_closed": false},
"friday": {"open": "10:00", "close": "19:00", "is_closed": false},
"saturday": {"open": "10:00", "close": "18:00", "is_closed": false},
"sunday": {"is_closed": true}
}'::jsonb;
-- Agregar comentario
COMMENT ON COLUMN locations.business_hours IS 'Horarios de atención por día. Formato JSONB con claves: monday-sunday, cada una con open/close en formato HH:MM e is_closed boolean';
-- Actualizar locations existentes con horarios por defecto
UPDATE locations
SET business_hours = '{
"monday": {"open": "10:00", "close": "19:00", "is_closed": false},
"tuesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"wednesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"thursday": {"open": "10:00", "close": "19:00", "is_closed": false},
"friday": {"open": "10:00", "close": "19:00", "is_closed": false},
"saturday": {"open": "10:00", "close": "18:00", "is_closed": false},
"sunday": {"is_closed": true}
}'::jsonb
WHERE business_hours IS NULL OR business_hours = '{}'::jsonb;
-- Verificar resultados
SELECT id, name, business_hours FROM locations;

View File

@@ -0,0 +1,41 @@
-- Debug and fix business_hours JSONB extraction
-- Execute in Supabase Dashboard: Database > SQL Editor
-- First, check what's actually stored in business_hours
SELECT
id,
name,
business_hours,
business_hours->>'monday' as monday_raw,
business_hours->'monday' as monday_object
FROM locations
LIMIT 1;
-- Test extraction logic
SELECT
'2026-01-20'::DATE as test_date,
EXTRACT(DOW FROM '2026-01-20'::DATE) as day_of_week_number,
ARRAY['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][EXTRACT(DOW FROM '2026-01-20'::DATE) + 1] as day_name;
-- Fix: Ensure business_hours is properly formatted
UPDATE locations
SET business_hours = '{
"monday": {"open": "10:00", "close": "19:00", "is_closed": false},
"tuesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"wednesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"thursday": {"open": "10:00", "close": "19:00", "is_closed": false},
"friday": {"open": "10:00", "close": "19:00", "is_closed": false},
"saturday": {"open": "10:00", "close": "18:00", "is_closed": false},
"sunday": {"is_closed": true}
}'::jsonb
WHERE business_hours IS NULL OR business_hours = '{}'::jsonb;
-- Verify the update
SELECT
id,
name,
business_hours->>'monday' as monday,
(business_hours->'monday'->>'open')::TIME as monday_open,
(business_hours->'monday'->>'close')::TIME as monday_close
FROM locations
LIMIT 1;

View File

@@ -0,0 +1,81 @@
-- Seed data for testing booking flow
-- Execute in Supabase Dashboard: Database > SQL Editor
-- Get active locations
DO $$
DECLARE
v_location_id UUID;
v_staff_id UUID;
v_resource_id UUID;
v_service_id UUID;
BEGIN
-- Get first active location
SELECT id INTO v_location_id FROM locations WHERE is_active = true LIMIT 1;
IF v_location_id IS NULL THEN
RAISE NOTICE 'No active locations found';
RETURN;
END IF;
RAISE NOTICE 'Using location: %', v_location_id;
-- Insert sample staff if none exists
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active, is_available_for_booking, work_hours_start, work_hours_end, work_days)
SELECT
gen_random_uuid(),
v_location_id,
'artist',
'Artista Demo',
'+52 844 123 4567',
true,
true,
'10:00'::TIME,
'19:00'::TIME,
'MON,TUE,WED,THU,FRI,SAT'
WHERE NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artista Demo' AND location_id = v_location_id)
RETURNING id INTO v_staff_id;
IF v_staff_id IS NOT NULL THEN
RAISE NOTICE 'Created staff: %', v_staff_id;
ELSE
SELECT id INTO v_staff_id FROM staff WHERE display_name = 'Artista Demo' AND location_id = v_location_id;
RAISE NOTICE 'Using existing staff: %', v_staff_id;
END IF;
-- Insert sample resources if none exists
INSERT INTO resources (location_id, name, type, capacity, is_active)
SELECT
v_location_id,
'Estación Demo',
'station',
1,
true
WHERE NOT EXISTS (SELECT 1 FROM resources WHERE name = 'Estación Demo' AND location_id = v_location_id)
RETURNING id INTO v_resource_id;
IF v_resource_id IS NOT NULL THEN
RAISE NOTICE 'Created resource: %', v_resource_id;
ELSE
SELECT id INTO v_resource_id FROM resources WHERE name = 'Estación Demo' AND location_id = v_location_id;
RAISE NOTICE 'Using existing resource: %', v_resource_id;
END IF;
-- Check if we have services
SELECT id INTO v_service_id FROM services WHERE is_active = true LIMIT 1;
IF v_service_id IS NOT NULL THEN
RAISE NOTICE 'Using service: %', v_service_id;
END IF;
RAISE NOTICE 'Seed data completed';
END $$;
-- Verify results
SELECT
'Locations' as type, COUNT(*)::text as count FROM locations WHERE is_active = true
UNION ALL
SELECT 'Staff', COUNT(*)::text FROM staff WHERE is_active = true
UNION ALL
SELECT 'Resources', COUNT(*)::text FROM resources WHERE is_active = true
UNION ALL
SELECT 'Services', COUNT(*)::text FROM services WHERE is_active = true;

View File

@@ -0,0 +1,46 @@
-- Seed data for testing availability
-- Execute in Supabase Dashboard: Database > SQL Editor
-- Insert sample staff if none exists
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active, is_available_for_booking, work_hours_start, work_hours_end, work_days)
SELECT
gen_random_uuid() as user_id,
id as location_id,
'artist' as role,
'Artista Demo' as display_name,
'+52 844 123 4567' as phone,
true as is_active,
true as is_available_for_booking,
'10:00'::TIME as work_hours_start,
'19:00'::TIME as work_hours_end,
'MON,TUE,WED,THU,FRI,SAT' as work_days
FROM locations
WHERE is_active = true
AND NOT EXISTS (SELECT 1 FROM staff WHERE location_id = locations.id AND display_name = 'Artista Demo')
LIMIT 1;
-- Insert sample resources if none exists
INSERT INTO resources (location_id, name, type, capacity, is_active)
SELECT
id as location_id,
'Estación Demo' as name,
'station' as type,
1 as capacity,
true as is_active
FROM locations
WHERE is_active = true
AND NOT EXISTS (SELECT 1 FROM resources WHERE location_id = locations.id AND name = 'Estación Demo')
LIMIT 1;
-- Verify results
SELECT
'Staff added' as info,
COUNT(*)::text as count
FROM staff
WHERE display_name = 'Artista Demo'
UNION ALL
SELECT
'Resources added',
COUNT(*)::text
FROM resources
WHERE name = 'Estación Demo';

View File

@@ -0,0 +1,24 @@
-- Test the availability functions
-- Execute in Supabase Dashboard: Database > SQL Editor
-- Test check_staff_availability with fixed type casting
SELECT
check_staff_availability(
(SELECT id FROM staff LIMIT 1),
NOW() + INTERVAL '1 hour',
NOW() + INTERVAL '2 hours'
) as is_available;
-- Test get_detailed_availability with business hours
SELECT * FROM get_detailed_availability(
(SELECT id FROM locations LIMIT 1),
(SELECT id FROM services LIMIT 1),
CURRENT_DATE + INTERVAL '1 day',
60
);
-- Check business hours structure
SELECT name, business_hours FROM locations LIMIT 1;
-- Check services with category
SELECT id, name, category, is_active FROM services LIMIT 5;

View File

@@ -0,0 +1,20 @@
-- Test script to check database data
-- Execute in Supabase Dashboard: Database > SQL Editor
-- Check counts
SELECT
'Locations' as table_name, COUNT(*)::text as count FROM locations
UNION ALL
SELECT 'Services', COUNT(*)::text FROM services
UNION ALL
SELECT 'Staff', COUNT(*)::text FROM staff
UNION ALL
SELECT 'Resources', COUNT(*)::text FROM resources
UNION ALL
SELECT 'Bookings', COUNT(*)::text FROM bookings;
-- Show sample data
SELECT id, name, timezone, is_active FROM locations LIMIT 5;
SELECT id, name, duration_minutes, base_price, is_active FROM services LIMIT 5;
SELECT id, display_name, role, is_active, is_available_for_booking FROM staff LIMIT 5;
SELECT id, name, type, capacity, is_active FROM resources LIMIT 5;

View File

@@ -0,0 +1,102 @@
-- Update get_detailed_availability to use business_hours from locations table
-- Execute in Supabase Dashboard: Database > SQL Editor
DROP FUNCTION IF EXISTS get_detailed_availability(p_location_id UUID, p_service_id UUID, p_date DATE, p_time_slot_duration_minutes INTEGER) CASCADE;
CREATE OR REPLACE FUNCTION get_detailed_availability(
p_location_id UUID,
p_service_id UUID,
p_date DATE,
p_time_slot_duration_minutes INTEGER DEFAULT 60
)
RETURNS JSONB AS $$
DECLARE
v_service_duration INTEGER;
v_location_timezone TEXT;
v_business_hours JSONB;
v_day_of_week TEXT;
v_day_hours JSONB;
v_start_time TIME := '09:00'::TIME;
v_end_time TIME := '21:00'::TIME;
v_time_slots JSONB := '[]'::JSONB;
v_slot_start TIMESTAMPTZ;
v_slot_end TIMESTAMPTZ;
v_available_staff_count INTEGER;
v_day_names TEXT[] := ARRAY['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
BEGIN
-- Obtener duración del servicio
SELECT duration_minutes INTO v_service_duration
FROM services
WHERE id = p_service_id;
IF v_service_duration IS NULL THEN
RETURN '[]'::JSONB;
END IF;
-- Obtener zona horaria y horarios de la ubicación
SELECT
timezone,
business_hours
INTO
v_location_timezone,
v_business_hours
FROM locations
WHERE id = p_location_id;
IF v_location_timezone IS NULL THEN
RETURN '[]'::JSONB;
END IF;
-- Obtener día de la semana (0 = Domingo, 1 = Lunes, etc.)
v_day_of_week := v_day_names[EXTRACT(DOW FROM p_date) + 1];
-- Obtener horarios para este día
v_day_hours := v_business_hours -> v_day_of_week;
-- Verificar si el lugar está cerrado este día
IF v_day_hours->>'is_closed' = 'true' THEN
RETURN '[]'::JSONB;
END IF;
-- Extraer horas de apertura y cierre
v_start_time := (v_day_hours->>'open')::TIME;
v_end_time := (v_day_hours->>'close')::TIME;
-- Generar slots de tiempo para el día
v_slot_start := (p_date || ' ' || v_start_time::TEXT)::TIMESTAMPTZ
AT TIME ZONE v_location_timezone;
v_slot_end := (p_date || ' ' || v_end_time::TEXT)::TIMESTAMPTZ
AT TIME ZONE v_location_timezone;
-- Iterar por cada slot
WHILE v_slot_start < v_slot_end LOOP
-- Verificar staff disponible para este slot
SELECT COUNT(*) INTO v_available_staff_count
FROM (
SELECT 1
FROM staff s
WHERE s.location_id = p_location_id
AND s.is_active = true
AND s.is_available_for_booking = true
AND s.role IN ('artist', 'staff', 'manager')
AND check_staff_availability(s.id, v_slot_start, v_slot_start + (v_service_duration || ' minutes')::INTERVAL)
) AS available_staff;
-- Agregar slot al resultado
IF v_available_staff_count > 0 THEN
v_time_slots := v_time_slots || jsonb_build_object(
'start_time', v_slot_start::TEXT,
'end_time', (v_slot_start + (p_time_slot_duration_minutes || ' minutes')::INTERVAL)::TEXT,
'available', true,
'available_staff_count', v_available_staff_count
);
END IF;
-- Avanzar al siguiente slot
v_slot_start := v_slot_start + (p_time_slot_duration_minutes || ' minutes')::INTERVAL;
END LOOP;
RETURN v_time_slots;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

View File

@@ -0,0 +1,63 @@
-- Update business_hours with correct structure and values
-- Execute in Supabase Dashboard: Database > SQL Editor
-- First, check current state
SELECT
id,
name,
business_hours->>'monday' as monday_check,
(business_hours->'monday'->>'open') as monday_open_check,
(business_hours->'monday'->>'close') as monday_close_check
FROM locations
LIMIT 1;
-- Update with correct structure - Monday to Friday 10-7, Saturday 10-6, Sunday closed
UPDATE locations
SET business_hours = '{
"monday": {"open": "10:00", "close": "19:00", "is_closed": false},
"tuesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"wednesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"thursday": {"open": "10:00", "close": "19:00", "is_closed": false},
"friday": {"open": "10:00", "close": "19:00", "is_closed": false},
"saturday": {"open": "10:00", "close": "18:00", "is_closed": false},
"sunday": {"is_closed": true}
}'::jsonb;
-- Verify the update
SELECT
id,
name,
timezone,
business_hours
FROM locations
LIMIT 1;
-- Test extraction for different days
SELECT
'Monday' as day,
(business_hours->'monday'->>'open')::TIME as open_time,
(business_hours->'monday'->>'close')::TIME as close_time,
business_hours->'monday'->>'is_closed' as is_closed
FROM locations
UNION ALL
SELECT
'Saturday' as day,
(business_hours->'saturday'->>'open')::TIME as open_time,
(business_hours->'saturday'->>'close')::TIME as close_time,
business_hours->'saturday'->>'is_closed' as is_closed
FROM locations
UNION ALL
SELECT
'Sunday' as day,
(business_hours->'sunday'->>'open')::TIME as open_time,
(business_hours->'sunday'->>'close')::TIME as close_time,
business_hours->'sunday'->>'is_closed' as is_closed
FROM locations;
-- Test the get_detailed_availability function
SELECT * FROM get_detailed_availability(
(SELECT id FROM locations LIMIT 1),
(SELECT id FROM services WHERE is_active = true LIMIT 1),
CURRENT_DATE,
60
);

View File

@@ -0,0 +1,25 @@
-- Update business_hours with correct structure and values
-- Execute in Supabase Dashboard: Database > SQL Editor
-- Update with correct structure - Monday to Friday 10-7, Saturday 10-6, Sunday closed
UPDATE locations
SET business_hours = '{
"monday": {"open": "10:00", "close": "19:00", "is_closed": false},
"tuesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"wednesday": {"open": "10:00", "close": "19:00", "is_closed": false},
"thursday": {"open": "10:00", "close": "19:00", "is_closed": false},
"friday": {"open": "10:00", "close": "19:00", "is_closed": false},
"saturday": {"open": "10:00", "close": "18:00", "is_closed": false},
"sunday": {"is_closed": true}
}'::jsonb
WHERE business_hours IS NULL OR business_hours = '{}'::jsonb;
-- Verify update
SELECT
id,
name,
business_hours->>'monday' as monday_check,
(business_hours->'monday'->>'open')::TIME as monday_open,
(business_hours->'monday'->>'close')::TIME as monday_close
FROM locations
LIMIT 1;