From 583a25a6f6ce0aaf34227d1aaae7263e81e6af60 Mon Sep 17 00:00:00 2001 From: Marco Gallegos Date: Sat, 17 Jan 2026 00:29:49 -0600 Subject: [PATCH] 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 --- STRIPE_SETUP.md | 75 ++ app/api/admin/kiosks/route.ts | 2 +- app/api/admin/locations/route.ts | 2 +- app/api/admin/users/route.ts | 2 +- app/api/aperture/dashboard/route.ts | 2 +- app/api/aperture/reports/payments/route.ts | 2 +- app/api/aperture/reports/payroll/route.ts | 2 +- app/api/aperture/reports/sales/route.ts | 2 +- app/api/aperture/resources/route.ts | 2 +- app/api/aperture/staff/route.ts | 2 +- app/api/aperture/staff/schedule/route.ts | 2 +- app/api/availability/blocks/route.ts | 2 +- .../availability/staff-unavailable/route.ts | 2 +- app/api/availability/staff/route.ts | 2 +- app/api/availability/time-slots/route.ts | 2 +- app/api/bookings/[id]/route.ts | 2 +- app/api/bookings/route.ts | 63 +- app/api/create-payment-intent/route.ts | 2 +- app/api/customers/route.ts | 120 ++++ app/api/kiosk/authenticate/route.ts | 2 +- .../kiosk/bookings/[shortId]/confirm/route.ts | 2 +- app/api/kiosk/bookings/route.ts | 2 +- app/api/kiosk/resources/available/route.ts | 2 +- app/api/kiosk/walkin/route.ts | 2 +- app/api/locations/route.ts | 2 +- app/api/services/route.ts | 5 +- app/booking/cita/page.tsx | 676 ++++++++++-------- app/booking/layout.tsx | 16 +- app/booking/registro/page.tsx | 285 ++++++++ app/booking/servicios/page.tsx | 399 +++++++---- app/globals.css | 34 + components/booking/date-picker.tsx | 105 +++ components/booking/mock-payment-form.tsx | 210 ++++++ components/ui/select.tsx | 15 +- lib/db/types.ts | 21 +- lib/supabase/admin.ts | 16 + lib/supabase/client.ts | 12 - lib/utils/business-hours.ts | 84 +++ lib/utils/short-id.ts | 2 +- scripts/add_business_hours.sql | 33 + scripts/debug_business_hours.sql | 41 ++ scripts/seed_booking_data.sql | 81 +++ scripts/seed_test_data.sql | 46 ++ scripts/test_availability_functions.sql | 24 + scripts/test_data_check.sql | 20 + scripts/update_availability_function.sql | 102 +++ scripts/update_business_hours.sql | 63 ++ scripts/update_business_hours_final.sql | 25 + ...60116100000_create_availability_tables.sql | 6 +- .../20260117000000_add_services_category.sql | 2 + ...0117010000_add_location_business_hours.sql | 13 + ...117020000_update_availability_function.sql | 101 +++ ...17030000_fix_availability_type_casting.sql | 72 ++ ...260117040000_complete_availability_fix.sql | 234 ++++++ ...20260117050000_fix_business_hours_json.sql | 107 +++ ...60117060000_update_business_hours_data.sql | 15 + 56 files changed, 2676 insertions(+), 491 deletions(-) create mode 100644 STRIPE_SETUP.md create mode 100644 app/api/customers/route.ts create mode 100644 app/booking/registro/page.tsx create mode 100644 components/booking/date-picker.tsx create mode 100644 components/booking/mock-payment-form.tsx create mode 100644 lib/supabase/admin.ts create mode 100644 lib/utils/business-hours.ts create mode 100644 scripts/add_business_hours.sql create mode 100644 scripts/debug_business_hours.sql create mode 100644 scripts/seed_booking_data.sql create mode 100644 scripts/seed_test_data.sql create mode 100644 scripts/test_availability_functions.sql create mode 100644 scripts/test_data_check.sql create mode 100644 scripts/update_availability_function.sql create mode 100644 scripts/update_business_hours.sql create mode 100644 scripts/update_business_hours_final.sql create mode 100644 supabase/migrations/20260117000000_add_services_category.sql create mode 100644 supabase/migrations/20260117010000_add_location_business_hours.sql create mode 100644 supabase/migrations/20260117020000_update_availability_function.sql create mode 100644 supabase/migrations/20260117030000_fix_availability_type_casting.sql create mode 100644 supabase/migrations/20260117040000_complete_availability_fix.sql create mode 100644 supabase/migrations/20260117050000_fix_business_hours_json.sql create mode 100644 supabase/migrations/20260117060000_update_business_hours_data.sql diff --git a/STRIPE_SETUP.md b/STRIPE_SETUP.md new file mode 100644 index 0000000..745a6b0 --- /dev/null +++ b/STRIPE_SETUP.md @@ -0,0 +1,75 @@ +# Stripe Payment Integration + +## Current Status +Stripe is currently **DISABLED** using mock payment mode for testing. + +## To Enable Real Stripe Payments + +### 1. Update Environment Variables + +In `.env.local`: + +```bash +NEXT_PUBLIC_STRIPE_ENABLED=true +STRIPE_SECRET_KEY=sk_test_your_real_stripe_secret_key +STRIPE_PUBLISHABLE_KEY=pk_test_your_real_stripe_publishable_key +STRIPE_WEBHOOK_SECRET=whsec_your_real_webhook_secret +``` + +### 2. Replace Mock Payment with Real Stripe + +In `app/booking/cita/page.tsx`: + +Replace the `MockPaymentForm` component usage with real Stripe integration: + +```tsx +import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js' + +// Replace the mock payment section with: + +``` + +### 3. Update Payment Handling + +Replace the `handleMockPayment` function with real Stripe confirmation: + +```tsx +const handlePayment = async () => { + if (!stripe || !elements) return + + const { error, paymentIntent } = await stripe.confirmCardPayment( + paymentIntent.clientSecret, + { + payment_method: { + card: elements.getElement(CardElement)!, + } + } + ) + + if (error) { + // Handle error + } else { + // Payment succeeded, create booking + } +} +``` + +### 4. Update Create Payment Intent API + +Ensure `/api/create-payment-intent` uses your real Stripe secret key. + +## Mock Payment Mode + +Currently using `components/booking/mock-payment-form.tsx` for testing without real payments. This validates card formatting and simulates payment flow. \ No newline at end of file diff --git a/app/api/admin/kiosks/route.ts b/app/api/admin/kiosks/route.ts index d282b5a..e3f2cb7 100644 --- a/app/api/admin/kiosks/route.ts +++ b/app/api/admin/kiosks/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateAdmin(request: NextRequest) { const authHeader = request.headers.get('authorization') diff --git a/app/api/admin/locations/route.ts b/app/api/admin/locations/route.ts index bbb5812..d18d059 100644 --- a/app/api/admin/locations/route.ts +++ b/app/api/admin/locations/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateAdmin(request: NextRequest) { const authHeader = request.headers.get('authorization') diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts index 0ea87fb..1df0ae6 100644 --- a/app/api/admin/users/route.ts +++ b/app/api/admin/users/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateAdmin(request: NextRequest) { const authHeader = request.headers.get('authorization') diff --git a/app/api/aperture/dashboard/route.ts b/app/api/aperture/dashboard/route.ts index 3eaa7e9..467912e 100644 --- a/app/api/aperture/dashboard/route.ts +++ b/app/api/aperture/dashboard/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Fetches bookings with filters for dashboard view diff --git a/app/api/aperture/reports/payments/route.ts b/app/api/aperture/reports/payments/route.ts index 1cfe0ef..cb17f0c 100644 --- a/app/api/aperture/reports/payments/route.ts +++ b/app/api/aperture/reports/payments/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Fetches recent payments report diff --git a/app/api/aperture/reports/payroll/route.ts b/app/api/aperture/reports/payroll/route.ts index e9b8181..12707f7 100644 --- a/app/api/aperture/reports/payroll/route.ts +++ b/app/api/aperture/reports/payroll/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Fetches payroll report for staff based on recent bookings diff --git a/app/api/aperture/reports/sales/route.ts b/app/api/aperture/reports/sales/route.ts index 10759d8..cdfaa11 100644 --- a/app/api/aperture/reports/sales/route.ts +++ b/app/api/aperture/reports/sales/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Fetches sales report including total sales, completed bookings, average service price, and sales by service diff --git a/app/api/aperture/resources/route.ts b/app/api/aperture/resources/route.ts index e625144..74b76aa 100644 --- a/app/api/aperture/resources/route.ts +++ b/app/api/aperture/resources/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Retrieves active resources, optionally filtered by location diff --git a/app/api/aperture/staff/route.ts b/app/api/aperture/staff/route.ts index 24a29bb..f471e0d 100644 --- a/app/api/aperture/staff/route.ts +++ b/app/api/aperture/staff/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Gets available staff for a location and date diff --git a/app/api/aperture/staff/schedule/route.ts b/app/api/aperture/staff/schedule/route.ts index 14f5ceb..291e867 100644 --- a/app/api/aperture/staff/schedule/route.ts +++ b/app/api/aperture/staff/schedule/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Retrieves staff availability schedule with optional filters diff --git a/app/api/availability/blocks/route.ts b/app/api/availability/blocks/route.ts index 5aaf5b6..4cd1b93 100644 --- a/app/api/availability/blocks/route.ts +++ b/app/api/availability/blocks/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateAdmin(request: NextRequest) { const authHeader = request.headers.get('authorization') diff --git a/app/api/availability/staff-unavailable/route.ts b/app/api/availability/staff-unavailable/route.ts index 86a18b4..aba0c8d 100644 --- a/app/api/availability/staff-unavailable/route.ts +++ b/app/api/availability/staff-unavailable/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateAdminOrStaff(request: NextRequest) { const authHeader = request.headers.get('authorization') diff --git a/app/api/availability/staff/route.ts b/app/api/availability/staff/route.ts index ff39bef..32f3da8 100644 --- a/app/api/availability/staff/route.ts +++ b/app/api/availability/staff/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Retrieves available staff for a time range diff --git a/app/api/availability/time-slots/route.ts b/app/api/availability/time-slots/route.ts index 0f23f96..0d18be5 100644 --- a/app/api/availability/time-slots/route.ts +++ b/app/api/availability/time-slots/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Retrieves detailed availability time slots for a date diff --git a/app/api/bookings/[id]/route.ts b/app/api/bookings/[id]/route.ts index 314fb9a..19319c9 100644 --- a/app/api/bookings/[id]/route.ts +++ b/app/api/bookings/[id]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Updates the status of a specific booking diff --git a/app/api/bookings/route.ts b/app/api/bookings/route.ts index 7437357..0666daa 100644 --- a/app/api/bookings/route.ts +++ b/app/api/bookings/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' import { generateShortId } from '@/lib/utils/short-id' /** @@ -9,6 +9,7 @@ export async function POST(request: NextRequest) { try { const body = await request.json() const { + customer_id, customer_email, customer_phone, customer_first_name, @@ -19,9 +20,16 @@ export async function POST(request: NextRequest) { notes } = body - if (!customer_email || !service_id || !location_id || !start_time_utc) { + if (!customer_id && (!customer_email || !customer_first_name || !customer_last_name)) { return NextResponse.json( - { error: 'Missing required fields: customer_email, service_id, location_id, start_time_utc' }, + { error: 'Missing required fields: customer_id OR (customer_email, customer_first_name, customer_last_name)' }, + { status: 400 } + ) + } + + if (!service_id || !location_id || !start_time_utc) { + return NextResponse.json( + { error: 'Missing required fields: service_id, location_id, start_time_utc' }, { status: 400 } ) } @@ -122,25 +130,39 @@ export async function POST(request: NextRequest) { const assignedResource = availableResources[0] - // Create or find customer based on email - const { data: customer, error: customerError } = await supabaseAdmin - .from('customers') - .upsert({ - email: customer_email, - phone: customer_phone || null, - first_name: customer_first_name || null, - last_name: customer_last_name || null - }, { - onConflict: 'email', - ignoreDuplicates: false - }) - .select() - .single() + let customer + let customerError + + if (customer_id) { + const result = await supabaseAdmin + .from('customers') + .select('*') + .eq('id', customer_id) + .single() + customer = result.data + customerError = result.error + } else { + const result = await supabaseAdmin + .from('customers') + .upsert({ + email: customer_email, + phone: customer_phone || null, + first_name: customer_first_name || null, + last_name: customer_last_name || null + }, { + onConflict: 'email', + ignoreDuplicates: false + }) + .select() + .single() + customer = result.data + customerError = result.error + } if (customerError || !customer) { - console.error('Error creating customer:', customerError) + console.error('Error handling customer:', customerError) return NextResponse.json( - { error: 'Failed to create customer' }, + { error: 'Failed to handle customer' }, { status: 500 } ) } @@ -248,8 +270,7 @@ export async function GET(request: NextRequest) { id, name, duration_minutes, - base_price, - category + base_price ), resource ( id, diff --git a/app/api/create-payment-intent/route.ts b/app/api/create-payment-intent/route.ts index da4785e..7ca798e 100644 --- a/app/api/create-payment-intent/route.ts +++ b/app/api/create-payment-intent/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import Stripe from 'stripe' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) diff --git a/app/api/customers/route.ts b/app/api/customers/route.ts new file mode 100644 index 0000000..01823fb --- /dev/null +++ b/app/api/customers/route.ts @@ -0,0 +1,120 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabaseAdmin } from '@/lib/supabase/admin' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const email = searchParams.get('email') + const phone = searchParams.get('phone') + + if (!email && !phone) { + return NextResponse.json( + { error: 'Se requiere email o teléfono' }, + { status: 400 } + ) + } + + let query = supabaseAdmin.from('customers').select('*').eq('is_active', true) + + if (email) { + query = query.ilike('email', email) + } else if (phone) { + query = query.ilike('phone', phone) + } + + const { data: customers, error } = await query.limit(1) + + if (error) { + console.error('Error buscando cliente:', error) + return NextResponse.json( + { error: 'Error al buscar cliente' }, + { status: 500 } + ) + } + + return NextResponse.json({ + exists: customers && customers.length > 0, + customer: customers && customers.length > 0 ? customers[0] : null + }) + } catch (error) { + console.error('Error en GET /api/customers:', error) + return NextResponse.json( + { error: 'Error interno del servidor' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { email, phone, first_name, last_name, birthday, occupation } = body + + if (!email || !phone || !first_name || !last_name) { + return NextResponse.json( + { error: 'Faltan campos requeridos: email, phone, first_name, last_name' }, + { status: 400 } + ) + } + + const { data: existingCustomer, error: checkError } = await supabaseAdmin + .from('customers') + .select('*') + .or(`email.ilike.${email},phone.ilike.${phone}`) + .eq('is_active', true) + .limit(1) + .single() + + if (checkError && checkError.code !== 'PGRST116') { + console.error('Error verificando cliente existente:', checkError) + return NextResponse.json( + { error: 'Error al verificar cliente existente' }, + { status: 500 } + ) + } + + if (existingCustomer) { + return NextResponse.json({ + success: false, + message: 'El cliente ya existe', + customer: existingCustomer + }) + } + + const { data: newCustomer, error: insertError } = await supabaseAdmin + .from('customers') + .insert({ + email, + phone, + first_name, + last_name, + tier: 'free', + total_spent: 0, + total_visits: 0, + is_active: true, + notes: birthday || occupation ? JSON.stringify({ birthday, occupation }) : null + }) + .select() + .single() + + if (insertError) { + console.error('Error creando cliente:', insertError) + return NextResponse.json( + { error: 'Error al crear cliente' }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + message: 'Cliente registrado exitosamente', + customer: newCustomer + }) + } catch (error) { + console.error('Error en POST /api/customers:', error) + return NextResponse.json( + { error: 'Error interno del servidor' }, + { status: 500 } + ) + } +} diff --git a/app/api/kiosk/authenticate/route.ts b/app/api/kiosk/authenticate/route.ts index 1690f54..c4bf4d9 100644 --- a/app/api/kiosk/authenticate/route.ts +++ b/app/api/kiosk/authenticate/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' import { Kiosk } from '@/lib/db/types' /** diff --git a/app/api/kiosk/bookings/[shortId]/confirm/route.ts b/app/api/kiosk/bookings/[shortId]/confirm/route.ts index 79a6efc..ab559f0 100644 --- a/app/api/kiosk/bookings/[shortId]/confirm/route.ts +++ b/app/api/kiosk/bookings/[shortId]/confirm/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateKiosk(request: NextRequest) { const apiKey = request.headers.get('x-kiosk-api-key') diff --git a/app/api/kiosk/bookings/route.ts b/app/api/kiosk/bookings/route.ts index 478ef0b..c18f20f 100644 --- a/app/api/kiosk/bookings/route.ts +++ b/app/api/kiosk/bookings/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateKiosk(request: NextRequest) { const apiKey = request.headers.get('x-kiosk-api-key') diff --git a/app/api/kiosk/resources/available/route.ts b/app/api/kiosk/resources/available/route.ts index 7a66571..4be5d3f 100644 --- a/app/api/kiosk/resources/available/route.ts +++ b/app/api/kiosk/resources/available/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' async function validateKiosk(request: NextRequest) { const apiKey = request.headers.get('x-kiosk-api-key') diff --git a/app/api/kiosk/walkin/route.ts b/app/api/kiosk/walkin/route.ts index b7d66c4..e4d53eb 100644 --- a/app/api/kiosk/walkin/route.ts +++ b/app/api/kiosk/walkin/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * Validates kiosk API key and returns kiosk info if valid diff --git a/app/api/locations/route.ts b/app/api/locations/route.ts index a8c82e7..c6b4b35 100644 --- a/app/api/locations/route.ts +++ b/app/api/locations/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Retrieves all active locations diff --git a/app/api/services/route.ts b/app/api/services/route.ts index 4d69bfd..c78c385 100644 --- a/app/api/services/route.ts +++ b/app/api/services/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { supabaseAdmin } from '@/lib/supabase/client' +import { supabaseAdmin } from '@/lib/supabase/admin' /** * @description Retrieves active services, optionally filtered by location @@ -11,9 +11,8 @@ export async function GET(request: NextRequest) { let query = supabaseAdmin .from('services') - .select('*') + .select('id, name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, category, is_active, created_at, updated_at') .eq('is_active', true) - .order('category', { ascending: true }) .order('name', { ascending: true }) if (locationId) { diff --git a/app/booking/cita/page.tsx b/app/booking/cita/page.tsx index 43b2d79..473fb61 100644 --- a/app/booking/cita/page.tsx +++ b/app/booking/cita/page.tsx @@ -1,21 +1,26 @@ 'use client' import { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { CheckCircle2, Calendar, Clock, MapPin, CreditCard } from 'lucide-react' -import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js' -import { format } from 'date-fns' +import { CheckCircle2, Calendar, Clock, MapPin, Mail, Phone, Search, User } from 'lucide-react' +import { format, parseISO } from 'date-fns' import { es } from 'date-fns/locale' -import { useAuth } from '@/lib/auth/context' +import MockPaymentForm from '@/components/booking/mock-payment-form' -/** @description Booking confirmation and payment page component for completing appointment reservations. */ export default function CitaPage() { - const { user, loading: authLoading } = useAuth() const router = useRouter() + const searchParams = useSearchParams() + + const [step, setStep] = useState<'search' | 'details' | 'payment' | 'success'>('search') + const [searchValue, setSearchValue] = useState('') + const [searchType, setSearchType] = useState<'email' | 'phone'>('email') + const [customer, setCustomer] = useState(null) + const [searchingCustomer, setSearchingCustomer] = useState(false) + const [formData, setFormData] = useState({ nombre: '', email: '', @@ -25,57 +30,57 @@ export default function CitaPage() { const [bookingDetails, setBookingDetails] = useState(null) const [pageLoading, setPageLoading] = useState(false) const [submitted, setSubmitted] = useState(false) - const [paymentIntent, setPaymentIntent] = useState(null) const [showPayment, setShowPayment] = useState(false) - const stripe = useStripe() - const elements = useElements() + const [depositAmount, setDepositAmount] = useState(0) + const [errors, setErrors] = useState>({}) useEffect(() => { - if (!authLoading && !user) { - router.push('/booking/login?redirect=/booking/cita' + window.location.search) - } - }, [user, authLoading, router]) - - if (authLoading) { - return ( -
-
-

Cargando...

-
-
- ) - } - - if (!user) { - return null - } - - useEffect(() => { - const params = new URLSearchParams(window.location.search) - const service_id = params.get('service_id') - const location_id = params.get('location_id') - const date = params.get('date') - const time = params.get('time') + const service_id = searchParams.get('service_id') + const location_id = searchParams.get('location_id') + const date = searchParams.get('date') + const time = searchParams.get('time') + const customer_id = searchParams.get('customer_id') if (service_id && location_id && date && time) { fetchBookingDetails(service_id, location_id, date, time) } - }, []) - useEffect(() => { - if (user) { - setFormData(prev => ({ - ...prev, - email: user.email || '' - })) + if (customer_id) { + fetchCustomerById(customer_id) } - }, [user]) + }, [searchParams]) + + const fetchCustomerById = async (customerId: string) => { + try { + const response = await fetch(`/api/customers?email=${customerId}`) + const data = await response.json() + + if (data.exists && data.customer) { + setCustomer(data.customer) + setFormData(prev => ({ + ...prev, + nombre: `${data.customer.first_name} ${data.customer.last_name}`, + email: data.customer.email, + telefono: data.customer.phone || '' + })) + setStep('details') + } + } catch (error) { + console.error('Error fetching customer:', error) + } + } const fetchBookingDetails = async (serviceId: string, locationId: string, date: string, time: string) => { try { const response = await fetch(`/api/availability/time-slots?location_id=${locationId}&service_id=${serviceId}&date=${date}`) const data = await response.json() - + + if (data.availability?.services) { + const service = data.availability.services[0] + const deposit = Math.min(service.base_price * 0.5, 200) + setDepositAmount(deposit) + } + setBookingDetails({ service_id: serviceId, location_id: locationId, @@ -85,45 +90,76 @@ export default function CitaPage() { }) } catch (error) { console.error('Error fetching booking details:', error) + setErrors({ fetch: 'Error al cargar los detalles de la reserva' }) + } + } + + const handleSearchCustomer = async (e: React.FormEvent) => { + e.preventDefault() + setSearchingCustomer(true) + setErrors({}) + + if (!searchValue.trim()) { + setErrors({ search: 'Ingresa un email o teléfono' }) + setSearchingCustomer(false) + return + } + + try { + const response = await fetch(`/api/customers?${searchType}=${encodeURIComponent(searchValue)}`) + const data = await response.json() + + if (data.exists && data.customer) { + setCustomer(data.customer) + setFormData(prev => ({ + ...prev, + nombre: `${data.customer.first_name} ${data.customer.last_name}`, + email: data.customer.email, + telefono: data.customer.phone || '' + })) + setStep('details') + } else { + const params = new URLSearchParams({ + ...Object.fromEntries(searchParams.entries()), + [searchType]: searchValue + }) + router.push(`/booking/registro?${params.toString()}`) + } + } catch (error) { + console.error('Error searching customer:', error) + setErrors({ search: 'Error al buscar cliente' }) + } finally { + setSearchingCustomer(false) } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + setErrors({}) setPageLoading(true) - try { - const response = await fetch('/api/create-payment-intent', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - customer_email: formData.email, - customer_phone: formData.telefono, - customer_first_name: formData.nombre.split(' ')[0] || formData.nombre, - customer_last_name: formData.nombre.split(' ').slice(1).join(' '), - service_id: bookingDetails.service_id, - location_id: bookingDetails.location_id, - start_time_utc: bookingDetails.startTime, - notes: formData.notas - }) - }) + const validationErrors: Record = {} - const data = await response.json() - - if (response.ok) { - setPaymentIntent(data) - setShowPayment(true) - } else { - alert('Error al preparar el pago: ' + (data.error || 'Error desconocido')) - } - } catch (error) { - console.error('Error creating payment intent:', error) - alert('Error al preparar el pago') - } finally { - setPageLoading(false) + if (!formData.nombre.trim()) { + validationErrors.nombre = 'Nombre requerido' } + + if (!formData.email.trim() || !formData.email.includes('@')) { + validationErrors.email = 'Email inválido' + } + + if (!formData.telefono.trim()) { + validationErrors.telefono = 'Teléfono requerido' + } + + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors) + setPageLoading(false) + return + } + + setShowPayment(true) + setPageLoading(false) } const handleChange = (e: React.ChangeEvent) => { @@ -131,80 +167,70 @@ export default function CitaPage() { ...formData, [e.target.name]: e.target.value }) + setErrors({ ...errors, [e.target.name]: '' }) } - const handlePayment = async () => { - if (!stripe || !elements) return - + const handleMockPayment = async (paymentMethod: any) => { setPageLoading(true) - const { error } = await stripe.confirmCardPayment(paymentIntent.clientSecret, { - payment_method: { - card: elements.getElement(CardElement)!, - } - }) - - if (error) { - alert('Error en el pago: ' + error.message) - setPageLoading(false) - } else { - // Payment succeeded, create booking - try { - const response = await fetch('/api/bookings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - customer_email: formData.email, - customer_phone: formData.telefono, - customer_first_name: formData.nombre.split(' ')[0] || formData.nombre, - customer_last_name: formData.nombre.split(' ').slice(1).join(' '), - service_id: bookingDetails.service_id, - location_id: bookingDetails.location_id, - start_time_utc: bookingDetails.startTime, - notes: formData.notas, - payment_intent_id: paymentIntent.id - }) + try { + const response = await fetch('/api/bookings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + customer_id: customer?.id, + customer_email: formData.email, + customer_phone: formData.telefono, + customer_first_name: formData.nombre.split(' ')[0] || formData.nombre, + customer_last_name: formData.nombre.split(' ').slice(1).join(' '), + service_id: bookingDetails.service_id, + location_id: bookingDetails.location_id, + start_time_utc: bookingDetails.startTime, + notes: formData.notas, + payment_method_id: 'mock_' + paymentMethod.cardNumber.slice(-4), + deposit_amount: depositAmount }) + }) - const data = await response.json() + const data = await response.json() - if (response.ok && data.success) { - setSubmitted(true) - } else { - alert('Error al crear la reserva: ' + (data.error || 'Error desconocido')) - } - } catch (error) { - console.error('Error creating booking:', error) - alert('Error al crear la reserva') - } finally { + if (response.ok && data.success) { + setSubmitted(true) + } else { + setErrors({ submit: data.error || 'Error al crear la reserva' }) setPageLoading(false) } + } catch (error) { + console.error('Error creating booking:', error) + setErrors({ submit: 'Error al crear la reserva' }) + setPageLoading(false) } } if (submitted) { return ( -
-
- +
+
+ -

+

¡Reserva Confirmada!

Hemos enviado un correo de confirmación con los detalles de tu cita.

-
+

Fecha: {format(new Date(bookingDetails.date), 'PPP', { locale: es })}

Hora: {bookingDetails.time}

-

Puedes agregar esta cita a tu calendario.

+

Depósito pagado: ${depositAmount.toFixed(2)} USD

@@ -217,188 +243,278 @@ export default function CitaPage() { if (!bookingDetails) { return ( -
+
-

Cargando detalles de la reserva...

+

Cargando detalles de la reserva...

) } return ( -
-
-
+
+
+
-
- - - - Resumen de la Cita - - - -
-
- + {step === 'search' && ( +
+ + + + Buscar Cliente + + + Ingresa tu email o teléfono para continuar con la reserva + + + +
-

Fecha

-

- {format(new Date(bookingDetails.date), 'PPP', { locale: es })} -

-
-
- -
- -
-

Hora

-

- {bookingDetails.time} -

-
-
- -
- -
-

Ubicación

-

- Anchor:23 - Saltillo -

-
-
-
- - - - - - - Tus Datos - - - Ingresa tus datos personales para completar la reserva - - - - -
- - -
- -
- - -
- -
- - -
- -
- -