diff --git a/README.md b/README.md index 6258fb9..2a40768 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ El PRD es la fuente de verdad funcional. El README es la guía de ejecución. * `anchor23.mx` - Frontend institucional (landing page + páginas informativas) * `booking.anchor23.mx` - Frontend de reservas (The Boutique) - **Pendiente** * `kiosk.anchor23.mx` - Sistema de autoservicio (The Kiosk) +* `aperture.anchor23.mx` - Dashboard administrativo y CRM (The HQ) +* `api.anchor23.mx` - API pública para integraciones externas ### Experiencias diff --git a/TASKS.md b/TASKS.md index 3958c45..94e6c51 100644 --- a/TASKS.md +++ b/TASKS.md @@ -348,7 +348,7 @@ Validación Staff (rol Staff): - ⏳ Gestión de recursos y asignación ### ⏳ Pendiente -- ⏳ Implementar API pública (api.anchor23.mx) - Horarios, servicios, ubicaciones públicas +- ✅ 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 diff --git a/app/aperture/login/page.tsx b/app/aperture/login/page.tsx new file mode 100644 index 0000000..c7dc5a3 --- /dev/null +++ b/app/aperture/login/page.tsx @@ -0,0 +1,115 @@ +'use client' + +import { useState } from 'react' +import { useAuth } from '@/lib/auth/context' +import { useRouter } from 'next/navigation' +import { supabase } from '@/lib/supabase/client' + +export default function ApertureLogin() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const { signInWithPassword } = useAuth() + const router = useRouter() + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError('') + + try { + const { error } = await signInWithPassword(email, password) + + if (error) { + setError(error.message) + } else { + // Check user role from database + const { data: { user } } = await supabase.auth.getUser() + if (user) { + const { data: staff } = await supabase + .from('staff') + .select('role') + .eq('user_id', user.id) + .single() + + if (staff && ['admin', 'manager', 'staff'].includes(staff.role)) { + router.push('/aperture') + } else { + setError('Unauthorized access') + await supabase.auth.signOut() + } + } + } + } catch (err) { + setError('An error occurred') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

+ Aperture Login +

+

+ Staff, Manager, or Admin access +

+
+
+
+
+ + setEmail(e.target.value)} + className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" + placeholder="Email address" + /> +
+
+ + setPassword(e.target.value)} + className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" + placeholder="Password" + /> +
+
+ + {error && ( +
+ {error} +
+ )} + +
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/aperture/page.tsx b/app/aperture/page.tsx index b2825ac..17cf9bd 100644 --- a/app/aperture/page.tsx +++ b/app/aperture/page.tsx @@ -44,6 +44,12 @@ export default function ApertureDashboard() { ) } + useEffect(() => { + if (!user) { + router.push('/aperture/login') + } + }, [user, router]) + if (!user) { return null } diff --git a/app/api/public/availability/route.ts b/app/api/public/availability/route.ts new file mode 100644 index 0000000..bdc478c --- /dev/null +++ b/app/api/public/availability/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabase } from '@/lib/supabase/client' + +/** + * @description Public API - Retrieves basic availability information + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const locationId = searchParams.get('location_id') + + if (!locationId) { + return NextResponse.json( + { error: 'Missing required parameter: location_id' }, + { status: 400 } + ) + } + + // Get location details + const { data: location, error: locationError } = await supabase + .from('locations') + .select('id, name, timezone') + .eq('id', locationId) + .eq('is_active', true) + .single() + + if (locationError || !location) { + return NextResponse.json( + { error: 'Location not found' }, + { status: 404 } + ) + } + + // Get active services for this location + const { data: services, error: servicesError } = await supabase + .from('services') + .select('id, name, duration_minutes, base_price') + .eq('is_active', true) + .order('name', { ascending: true }) + + if (servicesError) { + console.error('Public availability services error:', servicesError) + return NextResponse.json( + { error: servicesError.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + location, + services: services || [], + note: 'Use /api/availability/time-slots for detailed availability' + }) + } catch (error) { + console.error('Public availability GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/public/locations/route.ts b/app/api/public/locations/route.ts new file mode 100644 index 0000000..0837815 --- /dev/null +++ b/app/api/public/locations/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabase } from '@/lib/supabase/client' + +/** + * @description Public API - Retrieves all active locations + */ +export async function GET(request: NextRequest) { + try { + const { data: locations, error } = await supabase + .from('locations') + .select('id, name, timezone, address, phone, is_active') + .eq('is_active', true) + .order('name', { ascending: true }) + + if (error) { + console.error('Public locations GET error:', error) + return NextResponse.json( + { error: error.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + locations: locations || [] + }) + } catch (error) { + console.error('Public locations GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/public/services/route.ts b/app/api/public/services/route.ts new file mode 100644 index 0000000..4b03a83 --- /dev/null +++ b/app/api/public/services/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server' +import { supabase } from '@/lib/supabase/client' + +/** + * @description Public API - Retrieves active services + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const locationId = searchParams.get('location_id') + + let query = supabase + .from('services') + .select('id, name, description, duration_minutes, base_price, is_active') + .eq('is_active', true) + .order('name', { ascending: true }) + + if (locationId) { + query = query.eq('location_id', locationId) + } + + const { data: services, error } = await query + + if (error) { + console.error('Public services GET error:', error) + return NextResponse.json( + { error: error.message }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + services: services || [] + }) + } catch (error) { + console.error('Public services GET error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index b2cf903..7dd771f 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -9,6 +9,7 @@ type AuthContextType = { session: Session | null loading: boolean signIn: (email: string) => Promise<{ error: any }> + signInWithPassword: (email: string, password: string) => Promise<{ error: any }> signOut: () => Promise } @@ -57,6 +58,14 @@ export function AuthProvider({ children }: { children: ReactNode }) { return { error } } + const signInWithPassword = async (email: string, password: string) => { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + return { error } + } + const signOut = async () => { const { error } = await supabase.auth.signOut() if (error) { @@ -69,6 +78,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { session, loading, signIn, + signInWithPassword, signOut, }