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
+
+
+
+
+
+ )
+}
\ 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,
}