diff --git a/TASKS.md b/TASKS.md index 6f80d8e..206e107 100644 --- a/TASKS.md +++ b/TASKS.md @@ -329,20 +329,34 @@ Validación Staff (rol Staff): - 🚧 The Boutique - Frontend de reservas (booking.anchor23.mx) - ✅ Página de selección de servicios (/booking/servicios) - ✅ Página de confirmación de reserva (/booking/cita) + - ✅ Página de confirmación por código (/booking/confirmacion) + - ✅ Layout específico con navbar personalizado - ✅ API para obtener servicios (/api/services) - ✅ API para obtener ubicaciones (/api/locations) - ⏳ Configuración de dominios wildcard en producción + - ⏳ Autenticación de clientes + - ⏳ Integración con Stripe + +- 🚧 Aperture - Backend para staff/manager/admin (aperture.anchor23.mx) + - ✅ API para obtener staff disponible (/api/aperture/staff) + - ✅ API para gestión de horarios (/api/aperture/staff/schedule) + - ✅ API para recursos (/api/aperture/resources) + - ✅ API para dashboard (/api/aperture/dashboard) + - ✅ Página principal de admin (/aperture) + - ⏳ Autenticación de admin/staff/manager + - ⏳ Gestión completa de staff + - ⏳ Gestión de recursos y asignación ### ⏳ Pendiente -- ⏳ Implementar aperture.anchor23.mx - Backend para staff/manager/admin -- ⏳ Implementar API pública (api.anchor23.mx) -- ⏳ Implementar sistema de asignación de disponibilidad (staff management) -- ⏳ Implementar autenticación para staff/manager/admin +- ⏳ Implementar API pública (api.anchor23.mx) - Horarios, servicios, ubicaciones públicas +- ⏳ Implementar autenticación para staff/manager/admin (Supabase Auth) +- ⏳ Implementar sistema completo de asignación de disponibilidad - ⏳ Integración con Google Calendar -- ⏳ Integración con Stripe (pagos) +- ⏳ Integración con Stripe (pagos y depósitos dinámicos) - ⏳ The Vault (storage de fotos privadas) - ⏳ Notificaciones y automatización (WhatsApp API) - ⏳ Autenticación de clientes en The Boutique +- ⏳ Testing completo de todos los flujos --- @@ -356,36 +370,36 @@ Validación Staff (rol Staff): - Integrar con sistema de pagos (Stripe) - Testing completo del flujo -2. **Configurar Kioskos en Producción** +2. **Completar Aperture (aperture.anchor23.mx)** + - Implementar autenticación de admin/staff/manager + - Gestión completa de staff (CRUD, horarios) + - Gestión de recursos y asignación + - Dashboard operativo completo + - Testing de APIs + +3. **Configurar Kioskos en Producción** - Crear kioskos para cada location - Configurar API keys en variables de entorno - Probar acceso desde pantalla táctil - Usar el sistema de enrollment en `/admin/enrollment` -3. **Sistema de Enrollment** - - ✅ API route `/api/admin/locations` - Obtener locations - - ✅ API route `/api/admin/users` - Crear staff members - - ✅ API route `/api/admin/kiosks` - Crear kiosks - - ✅ Frontend `/admin/enrollment` - Interfaz de gestión - - ⏳ Configurar `ADMIN_ENROLLMENT_KEY` en variables de entorno - ### Prioridad Media - Próximas 2 Semanas -4. **Implementar API Routes para Bookings (Cliente)** - - `GET /api/bookings` - Listar bookings del cliente - - `POST /api/bookings` - Crear nuevo booking - - `PUT /api/bookings/{id}` - Modificar booking (solo staff/admin) - - `DELETE /api/bookings/{id}` - Cancelar booking +4. **Implementar API Pública (api.anchor23.mx)** + - Horarios de operación públicos + - Lista de servicios disponibles + - Ubicaciones y contacto + - Información sin datos sensibles -5. **Implementar Lógica de Disponibilidad** - - Función para buscar disponibilidad de staff - - Función para buscar disponibilidad de recursos - - Integración con `get_available_resources_with_priority()` +5. **Sistema de Autenticación Completo** + - Supabase Auth para staff/admin + - Perfiles de cliente en The Boutique + - Gestión de sesiones -6. **Implementar Notificaciones Básicas** - - Email de confirmación de booking - - Email de recordatorio (24h antes) - - Email de cancelación +6. **Integración con Stripe** + - Webhooks para pagos + - Depósitos dinámicos ($200 vs 50%) + - Lógica de no-show y penalizaciones ### Prioridad Baja - Próximo Mes @@ -393,7 +407,7 @@ Validación Staff (rol Staff): - API docs para aperture.anchor23.mx - API docs para api.anchor23.mx - Configuración de dominios wildcard - - Guías de despliegue + - Guías de despliegue y testing --- diff --git a/app/aperture/page.tsx b/app/aperture/page.tsx new file mode 100644 index 0000000..ff90b84 --- /dev/null +++ b/app/aperture/page.tsx @@ -0,0 +1,240 @@ +'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, Users, Clock, DollarSign, TrendingUp, LogOut } from 'lucide-react' +import { format } from 'date-fns' +import { es } from 'date-fns/locale' + +export default function ApertureDashboard() { + const [activeTab, setActiveTab] = useState<'dashboard' | 'staff' | 'resources' | 'reports'>('dashboard') + const [bookings, setBookings] = useState([]) + const [loading, setLoading] = useState(false) + const [stats, setStats] = useState({ + totalBookings: 0, + totalRevenue: 0, + completedToday: 0, + upcomingToday: 0 + }) + + useEffect(() => { + fetchBookings() + fetchStats() + }, []) + + const fetchBookings = async () => { + setLoading(true) + try { + const today = format(new Date(), 'yyyy-MM-dd') + const response = await fetch(`/api/aperture/dashboard?start_date=${today}&end_date=${today}`) + const data = await response.json() + if (data.success) { + setBookings(data.bookings) + } + } catch (error) { + console.error('Error fetching bookings:', error) + } finally { + setLoading(false) + } + } + + const fetchStats = async () => { + try { + const response = await fetch('/api/aperture/stats') + const data = await response.json() + if (data.success) { + setStats(data.stats) + } + } catch (error) { + console.error('Error fetching stats:', error) + } + } + + const handleLogout = () => { + localStorage.removeItem('admin_enrollment_key') + window.location.href = '/' + } + + return ( +
+
+
+

Aperture - Admin

+
+ +
+ +
+
+ + + Citas Hoy + + +

{stats.completedToday}

+

Completadas

+
+
+ + + + Ingresos Hoy + + +

${stats.totalRevenue.toLocaleString()}

+

Ingresos

+
+
+ + + + Pendientes + + +

{stats.upcomingToday}

+

Por iniciar

+
+
+ + + + Total Mes + + +

{stats.totalBookings}

+

Este mes

+
+
+
+ +
+
+ + + + +
+
+ + {activeTab === 'dashboard' && ( + + + Dashboard + Resumen de operaciones del día + + + {loading ? ( +
+ Cargando... +
+ ) : ( +
+ {bookings.length === 0 ? ( +

No hay citas para hoy

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

{booking.customer?.first_name} {booking.customer?.last_name}

+

{booking.service?.name}

+

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

+
+
+ + {booking.status} + +
+
+
+ ) + )} +
+ )} +
+
+ )} + + {activeTab === 'staff' && ( + + + Gestión de Staff + Administra horarios y disponibilidad del equipo + + +

+ Funcionalidad de gestión de staff próximamente +

+
+
+ )} + + {activeTab === 'resources' && ( + + + Gestión de Recursos + Administra estaciones y asignación + + +

+ Funcionalidad de gestión de recursos próximamente +

+
+
+ )} + + {activeTab === 'reports' && ( + + + Reportes + Estadísticas y análisis de operaciones + + +

+ Funcionalidad de reportes próximamente +

+
+
+ )} +
+
+ ) +} diff --git a/app/api/aperture/dashboard/route.ts b/app/api/aperture/dashboard/route.ts new file mode 100644 index 0000000..6fa73b2 --- /dev/null +++ b/app/api/aperture/dashboard/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabaseAdmin } from '@/lib/supabase/client' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const locationId = searchParams.get('location_id') + const startDate = searchParams.get('start_date') + const endDate = searchParams.get('end_date') + const staffId = searchParams.get('staff_id') + const status = searchParams.get('status') + + let query = supabaseAdmin + .from('bookings') + .select(` + id, + short_id, + status, + start_time_utc, + end_time_utc, + is_paid, + created_at, + customer ( + id, + first_name, + last_name, + email + ), + service ( + id, + name, + duration_minutes, + base_price + ), + staff ( + id, + display_name + ), + resource ( + id, + name, + type + ) + `) + .order('start_time_utc', { ascending: true }) + + if (locationId) { + query = query.eq('location_id', locationId) + } + + if (startDate) { + query = query.gte('start_time_utc', startDate) + } + + if (endDate) { + query = query.lte('end_time_utc', endDate) + } + + if (staffId) { + query = query.eq('staff_id', staffId) + } + + if (status) { + query = query.in('status', status.split(',')) + } + + const { data: bookings, error } = await query + + if (error) { + console.error('Aperture dashboard GET error:', error) + return NextResponse.json( + { error: error.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + bookings: bookings || [] + }) + } catch (error) { + console.error('Aperture dashboard GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/app/api/aperture/resources/route.ts b/app/api/aperture/resources/route.ts new file mode 100644 index 0000000..7aa6954 --- /dev/null +++ b/app/api/aperture/resources/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabaseAdmin } from '@/lib/supabase/client' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const locationId = searchParams.get('location_id') + + let query = supabaseAdmin + .from('resources') + .select('*') + .eq('is_active', true) + .order('type', { ascending: true }) + .order('name', { ascending: true }) + + if (locationId) { + query = query.eq('location_id', locationId) + } + + const { data: resources, error } = await query + + if (error) { + return NextResponse.json( + { error: error.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + resources: resources || [] + }) + } catch (error) { + console.error('Resources GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/app/api/aperture/staff/route.ts b/app/api/aperture/staff/route.ts new file mode 100644 index 0000000..72d8db7 --- /dev/null +++ b/app/api/aperture/staff/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabaseAdmin } from '@/lib/supabase/client' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const locationId = searchParams.get('location_id') + const date = searchParams.get('date') + + if (!locationId || !date) { + return NextResponse.json( + { error: 'Missing required parameters: location_id, date' }, + { status: 400 } + ) + } + + const { data: staff, error: staffError } = await supabaseAdmin.rpc('get_available_staff', { + p_location_id: locationId, + p_start_time_utc: `${date}T00:00:00Z`, + p_end_time_utc: `${date}T23:59:59Z` + }) + + if (staffError) { + return NextResponse.json( + { error: staffError.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + staff: staff || [] + }) + } catch (error) { + console.error('Aperture staff GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/app/api/aperture/staff/schedule/route.ts b/app/api/aperture/staff/schedule/route.ts new file mode 100644 index 0000000..4dbd48b --- /dev/null +++ b/app/api/aperture/staff/schedule/route.ts @@ -0,0 +1,181 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabaseAdmin } from '@/lib/supabase/client' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const locationId = searchParams.get('location_id') + const staffId = searchParams.get('staff_id') + const startDate = searchParams.get('start_date') + const endDate = searchParams.get('end_date') + + let query = supabaseAdmin + .from('staff_availability') + .select('*') + .order('date', { ascending: true }) + + if (locationId) { + const 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 (staffId) { + query = query.eq('staff_id', staffId) + } + + if (startDate) { + query = query.gte('date', startDate) + } + + if (endDate) { + query = query.lte('date', endDate) + } + + const { data: availability, error } = await query + + if (error) { + return NextResponse.json( + { error: error.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + availability: availability || [] + }) + } catch (error) { + console.error('Aperture staff schedule GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { + staff_id, + date, + start_time, + end_time, + is_available, + reason + } = body + + if (!staff_id || !date || !start_time || !end_time) { + return NextResponse.json( + { error: 'Missing required fields: staff_id, date, start_time, end_time' }, + { status: 400 } + ) + } + + const { data: existing, error: checkError } = await supabaseAdmin + .from('staff_availability') + .select('*') + .eq('staff_id', staff_id) + .eq('date', date) + .single() + + if (existing && !is_available) { + await supabaseAdmin + .from('staff_availability') + .update({ + start_time, + end_time, + is_available, + reason + }) + .eq('staff_id', staff_id) + .eq('date', date) + .single() + + return NextResponse.json({ + success: true, + availability: existing + }) + } + + if (checkError) { + return NextResponse.json( + { error: checkError.message }, + { status: 500 } + ) + } + + const { data: availability, error } = await supabaseAdmin + .from('staff_availability') + .insert({ + staff_id, + date, + start_time, + end_time, + is_available, + reason + }) + .select() + .single() + + if (error || !availability) { + return NextResponse.json( + { error: error?.message || 'Failed to create staff availability' }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + availability + }, { status: 201 }) + } catch (error) { + console.error('Aperture staff schedule POST error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + + if (!id) { + return NextResponse.json( + { error: 'Missing required parameter: id' }, + { status: 400 } + ) + } + + const { error } = await supabaseAdmin + .from('staff_availability') + .delete() + .eq('id', id) + + if (error) { + return NextResponse.json( + { error: error.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + message: 'Staff availability deleted successfully' + }) + } catch (error) { + console.error('Aperture staff schedule DELETE error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +}