diff --git a/TASKS.md b/TASKS.md index 206e107..7ef249e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -425,6 +425,17 @@ La migración de recursos eliminó todos los bookings existentes debido a CASCAD - Implementarse con migración de datos - Notificar a clientes de la necesidad de reprogramar +### Good to Have - Funcionalidades Adicionales + +8. **Sistema de Passes Digitales para Clientes** + - Los clientes pueden generar passes/códigos de acceso desde su cuenta + - Pases válidos por tiempo limitado + - Integración con wallet móvil + - Gestión de passes activos/inactivos + - Auditoría de uso de passes + +--- + ### Próximas Decisiones 1. ¿Implementar Auth con Supabase Magic Links o SMS? 2. ¿Usar Google Calendar API o Edge Functions para sync? diff --git a/app/aperture/page.tsx b/app/aperture/page.tsx index ff90b84..a13bba0 100644 --- a/app/aperture/page.tsx +++ b/app/aperture/page.tsx @@ -176,16 +176,18 @@ export default function ApertureDashboard() {
{booking.status}
- ) + )) )} )} diff --git a/app/api/aperture/staff/schedule/route.ts b/app/api/aperture/staff/schedule/route.ts index 4dbd48b..12e8258 100644 --- a/app/api/aperture/staff/schedule/route.ts +++ b/app/api/aperture/staff/schedule/route.ts @@ -15,13 +15,15 @@ export async function GET(request: NextRequest) { .order('date', { ascending: true }) if (locationId) { - const locationStaff = await supabaseAdmin + const { data: locationStaff } = await supabaseAdmin .from('staff') .select('id, display_name') .eq('location_id', locationId) .eq('is_active', true) - query = query.in('staff_id', locationStaff.map(s => s.id)) + if (locationStaff && locationStaff.length > 0) { + query = query.in('staff_id', locationStaff.map(s => s.id)) + } } if (staffId) { diff --git a/app/booking/layout.tsx b/app/booking/layout.tsx index cbf9749..3991752 100644 --- a/app/booking/layout.tsx +++ b/app/booking/layout.tsx @@ -21,18 +21,28 @@ export default function BookingLayout({
- + + + + - + + + +
diff --git a/app/booking/login/page.tsx b/app/booking/login/page.tsx new file mode 100644 index 0000000..83cea63 --- /dev/null +++ b/app/booking/login/page.tsx @@ -0,0 +1,320 @@ +'use client' + +import { useState } from 'react' +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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Eye, EyeOff, Mail, Lock, User } from 'lucide-react' + +export default function LoginPage() { + const [activeTab, setActiveTab] = useState<'login' | 'signup'>('login') + const [showPassword, setShowPassword] = useState(false) + const [loading, setLoading] = useState(false) + const [formData, setFormData] = useState({ + email: '', + password: '', + confirmPassword: '', + firstName: '', + lastName: '', + phone: '' + }) + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + + try { + // En una implementación real, esto haría una llamada a la API de autenticación + // Por ahora, simulamos un login exitoso + setTimeout(() => { + localStorage.setItem('customer_token', 'mock-token-123') + alert('Login exitoso! Redirigiendo...') + window.location.href = '/perfil' + }, 1000) + } catch (error) { + console.error('Login error:', error) + alert('Error al iniciar sesión') + } finally { + setLoading(false) + } + } + + const handleSignup = async (e: React.FormEvent) => { + e.preventDefault() + + if (formData.password !== formData.confirmPassword) { + alert('Las contraseñas no coinciden') + return + } + + setLoading(true) + + try { + // En una implementación real, esto crearía la cuenta del cliente + // Por ahora, simulamos un registro exitoso + setTimeout(() => { + alert('Cuenta creada exitosamente! Ahora puedes iniciar sesión.') + setActiveTab('login') + setFormData({ + ...formData, + password: '', + confirmPassword: '' + }) + }, 1000) + } catch (error) { + console.error('Signup error:', error) + alert('Error al crear la cuenta') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

+ Anchor:23 +

+

+ Accede a tu cuenta +

+
+ + + + + Bienvenido + + + Gestiona tus citas y accede a beneficios exclusivos + + + + setActiveTab(value as 'login' | 'signup')}> + + Iniciar Sesión + Crear Cuenta + + + +
+
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + + +
+
+ +
+ + +
+ + +
+ +
+

+ Al crear una cuenta, aceptas nuestros{' '} + + términos de privacidad + {' '} + y{' '} + + condiciones de servicio + . +

+
+
+
+
+
+ +
+

+ ¿No necesitas cuenta? Reserva como invitado +

+ +
+
+
+ ) +} diff --git a/app/booking/mis-citas/page.tsx b/app/booking/mis-citas/page.tsx new file mode 100644 index 0000000..f290b07 --- /dev/null +++ b/app/booking/mis-citas/page.tsx @@ -0,0 +1,252 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Calendar, Clock, MapPin, User, DollarSign } from 'lucide-react' +import { format } from 'date-fns' +import { es } from 'date-fns/locale' + +export default function MisCitasPage() { + const [bookings, setBookings] = useState([]) + const [loading, setLoading] = useState(false) + const [filter, setFilter] = useState<'all' | 'upcoming' | 'past'>('all') + + useEffect(() => { + loadBookings() + }, []) + + const loadBookings = async () => { + setLoading(true) + try { + // En una implementación real, esto vendría de la API + // Por ahora, simulamos algunas citas del cliente + const mockBookings = [ + { + id: 'booking-1', + short_id: 'ABC123', + status: 'confirmed', + start_time_utc: '2024-01-20T10:00:00Z', + end_time_utc: '2024-01-20T11:30:00Z', + service: { name: 'Corte y Estilismo', duration_minutes: 90, base_price: 2500 }, + staff: { display_name: 'Ana López' }, + location: { name: 'Anchor:23 Saltillo' }, + notes: 'Corte moderno con degradado' + }, + { + id: 'booking-2', + short_id: 'DEF456', + status: 'pending', + start_time_utc: '2024-01-25T14:30:00Z', + end_time_utc: '2024-01-25T15:45:00Z', + service: { name: 'Manicure de Precisión', duration_minutes: 75, base_price: 1200 }, + staff: { display_name: 'Carlos Martínez' }, + location: { name: 'Anchor:23 Saltillo' }, + notes: null + }, + { + id: 'booking-3', + short_id: 'GHI789', + status: 'completed', + start_time_utc: '2024-01-15T09:00:00Z', + end_time_utc: '2024-01-15T10:30:00Z', + service: { name: 'Peinado y Maquillaje', duration_minutes: 90, base_price: 3500 }, + staff: { display_name: 'Sofia Ramírez' }, + location: { name: 'Anchor:23 Saltillo' }, + notes: 'Evento especial - boda' + } + ] + setBookings(mockBookings) + } catch (error) { + console.error('Error loading bookings:', error) + } finally { + setLoading(false) + } + } + + const filteredBookings = bookings.filter(booking => { + const now = new Date() + const bookingDate = new Date(booking.start_time_utc) + + switch (filter) { + case 'upcoming': + return bookingDate >= now + case 'past': + return bookingDate < now + default: + return true + } + }) + + const getStatusBadge = (status: string, startTime: string) => { + const now = new Date() + const bookingDate = new Date(startTime) + const isPast = bookingDate < now + + const statuses = { + pending: { label: 'Pendiente', color: 'bg-yellow-100 text-yellow-800' }, + confirmed: { label: 'Confirmada', color: 'bg-green-100 text-green-800' }, + completed: { label: 'Completada', color: 'bg-blue-100 text-blue-800' }, + cancelled: { label: 'Cancelada', color: 'bg-red-100 text-red-800' }, + no_show: { label: 'No Show', color: 'bg-gray-100 text-gray-800' } + } + + const statusInfo = statuses[status as keyof typeof statuses] || statuses.pending + + // Si es pasado y no completado, mostrar como completado + if (isPast && status !== 'completed' && status !== 'cancelled') { + return { label: 'Completada', color: 'bg-blue-100 text-blue-800' } + } + + return statusInfo + } + + const handleCancelBooking = async (bookingId: string) => { + if (!confirm('¿Estás seguro de que quieres cancelar esta cita?')) { + return + } + + try { + // En una implementación real, esto haría una llamada a la API + // Por ahora, simulamos la cancelación + setBookings(bookings.map(b => + b.id === bookingId ? { ...b, status: 'cancelled' } : b + )) + alert('Cita cancelada exitosamente') + } catch (error) { + console.error('Error cancelling booking:', error) + alert('Error al cancelar la cita') + } + } + + return ( +
+
+
+

+ Mis Citas +

+

+ Gestiona tus reservas y citas programadas +

+
+ +
+ + + +
+ + {loading ? ( +
+

Cargando tus citas...

+
+ ) : filteredBookings.length === 0 ? ( + + + +

+ {filter === 'all' ? 'No tienes citas' : filter === 'upcoming' ? 'No tienes citas próximas' : 'No tienes citas pasadas'} +

+

+ {filter === 'all' ? 'Programa tu primera cita con nosotros' : 'Programa una nueva cita'} +

+ +
+
+ ) : ( +
+ {filteredBookings.map((booking) => { + const statusInfo = getStatusBadge(booking.status, booking.start_time_utc) + const bookingDate = new Date(booking.start_time_utc) + const now = new Date() + const isUpcoming = bookingDate >= now + const canCancel = booking.status === 'pending' || booking.status === 'confirmed' + + return ( + + +
+
+

+ {booking.service?.name} +

+
+
+ + {format(bookingDate, 'EEEE, d MMMM yyyy', { locale: es })} +
+
+ + {format(new Date(booking.start_time_utc), 'HH:mm', { locale: es })} - {format(new Date(booking.end_time_utc), 'HH:mm', { locale: es })} +
+
+ + {booking.staff?.display_name} +
+
+ + {booking.location?.name} +
+
+ {booking.notes && ( +
+

"{booking.notes}"

+
+ )} +
+ +
+
+ + {statusInfo.label} + +
+
+ + + ${booking.service?.base_price?.toLocaleString()} + +
+
+ Código: {booking.short_id} +
+ {isUpcoming && canCancel && ( + + )} +
+
+
+
+ ) + })} +
+ )} +
+
+ ) +} diff --git a/app/booking/perfil/page.tsx b/app/booking/perfil/page.tsx new file mode 100644 index 0000000..9e46b1d --- /dev/null +++ b/app/booking/perfil/page.tsx @@ -0,0 +1,341 @@ +'use client' + +import { useState, useEffect } from 'react' +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 { Calendar, Clock, MapPin, User, Mail, Phone } from 'lucide-react' +import { format } from 'date-fns' +import { es } from 'date-fns/locale' + +export default function PerfilPage() { + const [customer, setCustomer] = useState(null) + const [bookings, setBookings] = useState([]) + const [isEditing, setIsEditing] = useState(false) + const [loading, setLoading] = useState(false) + const [formData, setFormData] = useState({ + first_name: '', + last_name: '', + email: '', + phone: '' + }) + + useEffect(() => { + loadCustomerProfile() + loadCustomerBookings() + }, []) + + const loadCustomerProfile = async () => { + try { + // En una implementación real, esto vendría de autenticación + // Por ahora, simulamos datos del cliente + const mockCustomer = { + id: 'customer-123', + first_name: 'María', + last_name: 'García', + email: 'maria.garcia@email.com', + phone: '+52 844 123 4567', + tier: 'gold', + created_at: '2024-01-15' + } + setCustomer(mockCustomer) + setFormData({ + first_name: mockCustomer.first_name, + last_name: mockCustomer.last_name, + email: mockCustomer.email, + phone: mockCustomer.phone || '' + }) + } catch (error) { + console.error('Error loading customer profile:', error) + } + } + + const loadCustomerBookings = async () => { + try { + // En una implementación real, esto vendría de la API + // Por ahora, simulamos algunas citas + const mockBookings = [ + { + id: 'booking-1', + short_id: 'ABC123', + status: 'confirmed', + start_time_utc: '2024-01-20T10:00:00Z', + service: { name: 'Corte y Estilismo', duration_minutes: 60, base_price: 2500 }, + staff: { display_name: 'Ana López' }, + location: { name: 'Anchor:23 Saltillo' } + }, + { + id: 'booking-2', + short_id: 'DEF456', + status: 'pending', + start_time_utc: '2024-01-25T14:30:00Z', + service: { name: 'Manicure de Precisión', duration_minutes: 45, base_price: 1200 }, + staff: { display_name: 'Carlos Martínez' }, + location: { name: 'Anchor:23 Saltillo' } + } + ] + setBookings(mockBookings) + } catch (error) { + console.error('Error loading customer bookings:', error) + } + } + + const handleSaveProfile = async () => { + setLoading(true) + try { + // En una implementación real, esto actualizaría el perfil del cliente + setCustomer({ + ...customer, + ...formData + }) + setIsEditing(false) + alert('Perfil actualizado exitosamente') + } catch (error) { + console.error('Error updating profile:', error) + alert('Error al actualizar el perfil') + } finally { + setLoading(false) + } + } + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + const getTierBadge = (tier: string) => { + const tiers = { + free: { label: 'Free', color: 'bg-gray-100 text-gray-800' }, + gold: { label: 'Gold', color: 'bg-yellow-100 text-yellow-800' }, + black: { label: 'Black', color: 'bg-gray-900 text-white' }, + vip: { label: 'VIP', color: 'bg-purple-100 text-purple-800' } + } + return tiers[tier as keyof typeof tiers] || tiers.free + } + + const getStatusBadge = (status: string) => { + const statuses = { + pending: { label: 'Pendiente', color: 'bg-yellow-100 text-yellow-800' }, + confirmed: { label: 'Confirmada', color: 'bg-green-100 text-green-800' }, + completed: { label: 'Completada', color: 'bg-blue-100 text-blue-800' }, + cancelled: { label: 'Cancelada', color: 'bg-red-100 text-red-800' }, + no_show: { label: 'No Show', color: 'bg-gray-100 text-gray-800' } + } + return statuses[status as keyof typeof statuses] || statuses.pending + } + + if (!customer) { + return ( +
+
+

Cargando perfil...

+
+
+ ) + } + + const tierInfo = getTierBadge(customer.tier) + + return ( +
+
+
+

+ Mi Perfil +

+

+ Gestiona tu información y citas +

+
+ +
+ + + + Información Personal + {!isEditing && ( + + )} + + + + {isEditing ? ( +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ ) : ( +
+
+ + + {customer.first_name} {customer.last_name} + +
+
+ + {customer.email} +
+ {customer.phone && ( +
+ + {customer.phone} +
+ )} +
+ + {tierInfo.label} Tier + +
+
+ )} +
+
+ + + + + Resumen de Actividad + + + +
+
+
+ {bookings.length} +
+
+ Citas totales +
+
+ +
+
+ {bookings.filter(b => b.status === 'completed').length} +
+
+ Completadas +
+
+ +
+
+ Miembro desde {format(new Date(customer.created_at), 'MMM yyyy', { locale: es })} +
+
+
+
+
+
+ + + + + Mis Últimas Citas + + + Historial de tus reservas + + + + {bookings.length === 0 ? ( +
+

+ No tienes citas programadas +

+ +
+ ) : ( +
+ {bookings.map((booking) => ( +
+
+
+

+ {booking.service?.name} +

+

+ {booking.staff?.display_name} • {booking.location?.name} +

+

+ {format(new Date(booking.start_time_utc), 'PPP HH:mm', { locale: es })} +

+
+ + {getStatusBadge(booking.status).label} + +
+
+

+ ${booking.service?.base_price?.toLocaleString()} +

+

+ Código: {booking.short_id} +

+
+
+ ))} +
+ )} +
+
+
+
+ ) +}