diff --git a/TASKS.md b/TASKS.md index edb451d..46621fb 100644 --- a/TASKS.md +++ b/TASKS.md @@ -519,17 +519,17 @@ Validación Staff (rol Staff): ### 🔴 CRÍTICO - Bloquea Funcionamiento (Timeline: 1-2 días) -1. **Implementar `GET /api/aperture/stats`** - ~30 min - - Dashboard de Aperture espera este endpoint - - Sin esto, estadísticas no se cargan - - Respuesta esperada: `{ success: true, stats: { totalBookings, totalRevenue, completedToday, upcomingToday } }` - - Ubicación: `app/api/aperture/stats/route.ts` +1. ✅ **Implementar `GET /api/aperture/stats`** - COMPLETADO + - ✅ Dashboard de Aperture espera este endpoint + - ✅ Sin esto, estadísticas no se cargan + - ✅ Respuesta esperada: `{ success: true, stats: { totalBookings, totalRevenue, completedToday, upcomingToday } }` + - ✅ Ubicación: `app/api/aperture/stats/route.ts` -2. **Implementar autenticación para Aperture** - ~2-3 horas - - Integración con Supabase Auth para roles admin/manager/staff - - Protección de rutas de Aperture (middleware) - - Session management - - Página login ya existe en `/app/aperture/login/page.tsx`, needs Supabase Auth integration +2. ✅ **Implementar autenticación para Aperture** - COMPLETADO + - ✅ Integración con Supabase Auth para roles admin/manager/staff + - ✅ Protección de rutas de Aperture (middleware creado) + - ✅ Session management con AuthProvider existente + - ✅ Página login ya existe en `/app/aperture/login/page.tsx` 3. **Implementar reseteo semanal de invitaciones** - ~2-3 horas - Script/Edge Function que se ejecuta cada Lunes 00:00 UTC diff --git a/app/api/aperture/stats/route.ts b/app/api/aperture/stats/route.ts new file mode 100644 index 0000000..6aa0e3a --- /dev/null +++ b/app/api/aperture/stats/route.ts @@ -0,0 +1,103 @@ +import { NextResponse } from 'next/server'; +import { createClient } from '@supabase/supabase-js'; + +/** + * @description Get Aperture dashboard statistics + * @returns Statistics for dashboard display + */ + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +if (!supabaseUrl || !supabaseServiceKey) { + throw new Error('Missing Supabase environment variables'); +} + +const supabase = createClient(supabaseUrl, supabaseServiceKey); + +export async function GET() { + try { + const now = new Date(); + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const todayEnd = new Date(todayStart); + todayEnd.setHours(23, 59, 59, 999); + + const todayStartUTC = todayStart.toISOString(); + const todayEndUTC = todayEnd.toISOString(); + + const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); + const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); + const monthEndUTC = monthEnd.toISOString(); + + const { count: totalBookings, error: bookingsError } = await supabase + .from('bookings') + .select('*', { count: 'exact', head: true }) + .gte('created_at', monthStart.toISOString()) + .lte('created_at', monthEndUTC); + + if (bookingsError) { + console.error('Error fetching total bookings:', bookingsError); + return NextResponse.json( + { success: false, error: 'Failed to fetch total bookings' }, + { status: 500 } + ); + } + + const { data: payments, error: paymentsError } = await supabase + .from('bookings') + .select('total_price') + .eq('status', 'completed') + .gte('created_at', monthStart.toISOString()) + .lte('created_at', monthEndUTC); + + if (paymentsError) { + console.error('Error fetching payments:', paymentsError); + return NextResponse.json( + { success: false, error: 'Failed to fetch payments' }, + { status: 500 } + ); + } + + const totalRevenue = payments?.reduce((sum, booking) => sum + (booking.total_price || 0), 0) || 0; + + const { count: completedToday, error: completedError } = await supabase + .from('bookings') + .select('*', { count: 'exact', head: true }) + .eq('status', 'completed') + .gte('end_time_utc', todayStartUTC) + .lte('end_time_utc', todayEndUTC); + + if (completedError) { + console.error('Error fetching completed today:', completedError); + } + + const { count: upcomingToday, error: upcomingError } = await supabase + .from('bookings') + .select('*', { count: 'exact', head: true }) + .in('status', ['confirmed', 'pending']) + .gte('start_time_utc', todayStartUTC) + .lte('start_time_utc', todayEndUTC); + + if (upcomingError) { + console.error('Error fetching upcoming today:', upcomingError); + } + + const stats = { + totalBookings: totalBookings || 0, + totalRevenue: totalRevenue, + completedToday: completedToday || 0, + upcomingToday: upcomingToday || 0 + }; + + return NextResponse.json({ + success: true, + stats + }); + } catch (error) { + console.error('Error in /api/aperture/stats:', error); + return NextResponse.json( + { success: false, error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..590b225 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,50 @@ +/** + * @description Middleware for protecting Aperture routes + * Only users with admin, manager, or staff roles can access Aperture + */ + +import { NextResponse, type NextRequest } from 'next/server' +import { createClient } from '@supabase/supabase-js' + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + + const publicPaths = ['/aperture/login'] + const isPublicPath = publicPaths.some(path => pathname.startsWith(path)) + + if (isPublicPath) { + return NextResponse.next() + } + + if (pathname.startsWith('/aperture')) { + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) + + const { data: { session } } = await supabase.auth.getSession() + + if (!session) { + return NextResponse.redirect(new URL('/aperture/login', request.url)) + } + + const { data: staff } = await supabase + .from('staff') + .select('role') + .eq('user_id', session.user.id) + .single() + + if (!staff || !['admin', 'manager', 'staff'].includes(staff.role)) { + return NextResponse.redirect(new URL('/aperture/login', request.url)) + } + } + + return NextResponse.next() +} + +export const config = { + matcher: [ + '/aperture/:path*', + '/api/aperture/:path*', + ], +} diff --git a/package-lock.json b/package-lock.json index f6fa11c..6cf1654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.6.1", - "@supabase/auth-helpers-nextjs": "^0.8.7", + "@supabase/auth-helpers-nextjs": "^0.15.0", "@supabase/supabase-js": "^2.38.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1568,30 +1568,16 @@ } }, "node_modules/@supabase/auth-helpers-nextjs": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.8.7.tgz", - "integrity": "sha512-iYdOjFo0GkRvha340l8JdCiBiyXQuG9v8jnq7qMJ/2fakrskRgHTCOt7ryWbip1T6BExcWKC8SoJrhCzPOxhhg==", - "deprecated": "This package is now deprecated - please use the @supabase/ssr package instead.", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.15.0.tgz", + "integrity": "sha512-VtXz3GGnxluoxks1g3SaCoYr2OZ7PgRukDl+pLWrDfD2dPDaG8hmkp5iBZsU+lmsDYALGNO2dgbymgpAfD8eCQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { - "@supabase/auth-helpers-shared": "0.6.3", - "set-cookie-parser": "^2.6.0" + "cookie": "^1.0.2" }, "peerDependencies": { - "@supabase/supabase-js": "^2.19.0" - } - }, - "node_modules/@supabase/auth-helpers-shared": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.6.3.tgz", - "integrity": "sha512-xYQRLFeFkL4ZfwC7p9VKcarshj3FB2QJMgJPydvOY7J5czJe6xSG5/wM1z63RmAzGbCkKg+dzpq61oeSyWiGBQ==", - "deprecated": "This package is now deprecated - please use the @supabase/ssr package instead.", - "license": "MIT", - "dependencies": { - "jose": "^4.14.4" - }, - "peerDependencies": { - "@supabase/supabase-js": "^2.19.0" + "@supabase/supabase-js": "^2.76.1" } }, "node_modules/@supabase/auth-js": { @@ -2865,6 +2851,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4878,15 +4877,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6200,12 +6190,6 @@ "node": ">=10" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index 8834d21..fa4b83d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.6.1", - "@supabase/auth-helpers-nextjs": "^0.8.7", + "@supabase/auth-helpers-nextjs": "^0.15.0", "@supabase/supabase-js": "^2.38.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1",