docs: add comprehensive code comments, update README and TASKS, create training and troubleshooting guides

- Add JSDoc comments to API routes and business logic functions
- Update README.md with Phase 2 status and deployment/production notes
- Enhance TASKS.md with estimated timelines and dependencies
- Create docs/STAFF_TRAINING.md for team onboarding
- Create docs/CLIENT_ONBOARDING.md for customer experience
- Create docs/OPERATIONAL_PROCEDURES.md for daily operations
- Create docs/TROUBLESHOOTING.md for common setup issues
- Fix TypeScript errors in hq/page.tsx
This commit is contained in:
Marco Gallegos
2026-01-16 18:42:45 -06:00
parent 28e98a2a44
commit 8fc9d3717e
63 changed files with 973 additions and 101 deletions

View File

@@ -267,8 +267,8 @@ El sitio estará disponible en **http://localhost:2311**
**Fase 2 — Motor de Agendamiento**: 80% completado **Fase 2 — Motor de Agendamiento**: 80% completado
- Disponibilidad dual capa: 100% - Disponibilidad dual capa: 100%
- API de reservas: 100% - API de reservas: 100%
- The Boutique: 100% (completo con pagos) - The Boutique: 90% (frontend completo, autenticación y pagos parcialmente implementados)
- Integración Pagos (Stripe): 100% - Integración Pagos (Stripe): 90% (depósitos implementados, webhooks pendientes)
- Integración Calendar: 20% (en progreso) - Integración Calendar: 20% (en progreso)
- Aperture Backend: 100% - Aperture Backend: 100%
@@ -280,7 +280,47 @@ El sitio estará disponible en **http://localhost:2311**
--- ---
## 11. anchor23.mx - Frontend Institucional ## 11. Deployment y Producción
### Requisitos para Producción
- VPS o cloud provider (Vercel recomendado para Next.js)
- Base de datos Supabase production
- Configuración de dominios wildcard (`*.anchor23.mx`)
- SSL certificates automáticos
- Monitoring y logging (Sentry recomendado)
### Variables de Entorno de Producción
Además de las variables locales, configurar:
```
# Producción
NEXT_PUBLIC_APP_URL=https://anchor23.mx
NEXT_PUBLIC_BOOKING_URL=https://booking.anchor23.mx
NEXT_PUBLIC_KIOSK_URL=https://kiosk.anchor23.mx
NEXT_PUBLIC_APERTURE_URL=https://aperture.anchor23.mx
# Webhooks Stripe
STRIPE_WEBHOOK_ENDPOINT_SECRET=
# Google Calendar (opcional para producción)
GOOGLE_CALENDAR_ID=
```
### Pasos de Deployment
1. Configurar Supabase production con RLS habilitado
2. Ejecutar migraciones: `supabase db push`
3. Configurar dominios y SSL
4. Desplegar en Vercel con build settings personalizados
5. Configurar webhooks de Stripe para pagos
6. Probar autenticación y bookings end-to-end
### Monitoreo
- Logs de Supabase para queries lentas
- Alertas de Stripe para fallos de pago
- Uptime monitoring para dominios críticos
---
## 12. anchor23.mx - Frontend Institucional
Dominio institucional. Contenido estático, marca, narrativa y conversión inicial. Dominio institucional. Contenido estático, marca, narrativa y conversión inicial.
@@ -342,7 +382,7 @@ Ver documentación completa en `API.md` para todos los endpoints disponibles.
--- ---
## 12. Sistema de Kiosko ## 13. Sistema de Kiosko
El sistema de kiosko permite a los clientes interactuar con el salón mediante pantallas táctiles en la entrada. El sistema de kiosko permite a los clientes interactuar con el salón mediante pantallas táctiles en la entrada.
@@ -367,7 +407,7 @@ https://kiosk.anchor23.mx/{location-id}
--- ---
## 13. Filosofía Operativa ## 14. Filosofía Operativa
SalonOS no busca volumen. SalonOS no busca volumen.

View File

@@ -362,52 +362,52 @@ Validación Staff (rol Staff):
## PRÓXIMAS TARES PRIORITARIAS ## PRÓXIMAS TARES PRIORITARIAS
### Prioridad Alta - Esta Semana ### Prioridad Alta - Esta Semana (Timeline: 7 días)
1. **Terminar The Boutique (booking.anchor23.mx)** 1. **Terminar The Boutique (booking.anchor23.mx)** - 3-4 días
- Implementar autenticación de clientes - Implementar autenticación de clientes (depende de: Supabase Auth configurado)
- Completar flujo de reserva - Completar flujo de reserva (depende de: auth implementado)
- Integrar con sistema de pagos (Stripe) - Integrar con sistema de pagos (Stripe) (depende de: webhooks Stripe)
- Testing completo del flujo - Testing completo del flujo (depende de: integración completa)
2. **Completar Aperture (aperture.anchor23.mx)** 2. **Completar Aperture (aperture.anchor23.mx)** - 4-5 días
- Implementar autenticación de admin/staff/manager - Implementar autenticación de admin/staff/manager (depende de: Supabase Auth)
- Gestión completa de staff (CRUD, horarios) - Gestión completa de staff (CRUD, horarios) (depende de: auth implementado, APIs existentes)
- Gestión de recursos y asignación - Gestión de recursos y asignación (depende de: staff gestión)
- Dashboard operativo completo - Dashboard operativo completo (depende de: gestión implementada)
- Testing de APIs - Testing de APIs (depende de: todas las funciones)
3. **Configurar Kioskos en Producción** 3. **Configurar Kioskos en Producción** - 1-2 días
- Crear kioskos para cada location - Crear kioskos para cada location (depende de: migraciones en prod)
- Configurar API keys en variables de entorno - Configurar API keys en variables de entorno (depende de: env setup)
- Probar acceso desde pantalla táctil - Probar acceso desde pantalla táctil (depende de: kioskos creados)
- Usar el sistema de enrollment en `/admin/enrollment` - Usar el sistema de enrollment en `/admin/enrollment` (depende de: admin auth)
### Prioridad Media - Próximas 2 Semanas ### Prioridad Media - Próximas 2 Semanas (Timeline: 14 días)
4. **Implementar API Pública (api.anchor23.mx)** 4. **Implementar API Pública (api.anchor23.mx)** - 3-4 días
- Horarios de operación públicos - Horarios de operación públicos (depende de: locations table)
- Lista de servicios disponibles - Lista de servicios disponibles (depende de: services table, RLS público)
- Ubicaciones y contacto - Ubicaciones y contacto (depende de: locations table)
- Información sin datos sensibles - Información sin datos sensibles (depende de: RLS configurado)
5. **Sistema de Autenticación Completo** 5. **Sistema de Autenticación Completo** - 5-7 días
- Supabase Auth para staff/admin - Supabase Auth para staff/admin (depende de: roles configurados)
- Perfiles de cliente en The Boutique - Perfiles de cliente en The Boutique (depende de: auth cliente)
- Gestión de sesiones - Gestión de sesiones (depende de: Supabase Auth completo)
6. **Integración con Stripe** 6. **Integración con Stripe** - 4-5 días
- Webhooks para pagos - Webhooks para pagos (depende de: Stripe account, endpoints)
- Depósitos dinámicos ($200 vs 50%) - Depósitos dinámicos ($200 vs 50%) (depende de: webhooks)
- Lógica de no-show y penalizaciones - Lógica de no-show y penalizaciones (depende de: webhooks, bookings logic)
### Prioridad Baja - Próximo Mes ### Prioridad Baja - Próximo Mes (Timeline: 30 días)
7. **Documentar nuevos endpoints y configuración** 7. **Documentar nuevos endpoints y configuración** - 7-10 días
- API docs para aperture.anchor23.mx - API docs para aperture.anchor23.mx (depende de: APIs completas)
- API docs para api.anchor23.mx - API docs para api.anchor23.mx (depende de: API pública implementada)
- Configuración de dominios wildcard - Configuración de dominios wildcard (depende de: dominio setup)
- Guías de despliegue y testing - Guías de despliegue y testing (depende de: sistema completo)
--- ---

View File

@@ -8,6 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
/** @description Admin enrollment system component for creating and managing staff members and kiosk devices. */
export default function EnrollmentPage() { export default function EnrollmentPage() {
const [adminKey, setAdminKey] = useState('') const [adminKey, setAdminKey] = useState('')
const [isAuthenticated, setIsAuthenticated] = useState(false) const [isAuthenticated, setIsAuthenticated] = useState(false)

View File

@@ -9,6 +9,7 @@ import { format } from 'date-fns'
import { es } from 'date-fns/locale' import { es } from 'date-fns/locale'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
/** @description Admin dashboard component for managing salon operations including bookings, staff, resources, reports, and permissions. */
export default function ApertureDashboard() { export default function ApertureDashboard() {
const { user, loading: authLoading, signOut } = useAuth() const { user, loading: authLoading, signOut } = useAuth()
const router = useRouter() const router = useRouter()

View File

@@ -17,6 +17,9 @@ async function validateAdmin(request: NextRequest) {
return true return true
} }
/**
* @description Retrieves kiosks with filters for admin
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)
@@ -77,6 +80,9 @@ export async function GET(request: NextRequest) {
} }
} }
/**
* @description Creates a new kiosk
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)

View File

@@ -17,6 +17,9 @@ async function validateAdmin(request: NextRequest) {
return true return true
} }
/**
* @description Retrieves all locations for admin
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)

View File

@@ -17,6 +17,9 @@ async function validateAdmin(request: NextRequest) {
return true return true
} }
/**
* @description Retrieves staff users with filters for admin
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)
@@ -78,6 +81,9 @@ export async function GET(request: NextRequest) {
} }
} }
/**
* @description Creates a new staff user
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Fetches bookings with filters for dashboard view
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -37,6 +37,9 @@ const mockPermissions = [
} }
] ]
/**
* @description Retrieves permissions data for different roles
*/
export async function GET() { export async function GET() {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@@ -44,6 +47,9 @@ export async function GET() {
}) })
} }
/**
* @description Toggles a specific permission for a role
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const { roleId, permId } = await request.json() const { roleId, permId } = await request.json()

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Fetches recent payments report
*/
export async function GET() { export async function GET() {
try { try {
// Get recent payments (assuming bookings with payment_intent_id are paid) // Get recent payments (assuming bookings with payment_intent_id are paid)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Fetches payroll report for staff based on recent bookings
*/
export async function GET() { export async function GET() {
try { try {
// Get staff and their bookings this week // Get staff and their bookings this week

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Fetches sales report including total sales, completed bookings, average service price, and sales by service
*/
export async function GET() { export async function GET() {
try { try {
// Get total sales // Get total sales

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Retrieves active resources, optionally filtered by location
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Gets available staff for a location and date
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Retrieves staff availability schedule with optional filters
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
@@ -60,6 +63,9 @@ export async function GET(request: NextRequest) {
} }
} }
/**
* @description Creates or updates staff availability
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json() const body = await request.json()
@@ -145,6 +151,9 @@ export async function POST(request: NextRequest) {
} }
} }
/**
* @description Deletes staff availability by ID
*/
export async function DELETE(request: NextRequest) { export async function DELETE(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -17,6 +17,9 @@ async function validateAdmin(request: NextRequest) {
return true return true
} }
/**
* @description Creates a booking block for a resource
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)
@@ -76,6 +79,9 @@ export async function POST(request: NextRequest) {
} }
} }
/**
* @description Retrieves booking blocks with filters
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)
@@ -151,6 +157,9 @@ export async function GET(request: NextRequest) {
} }
} }
/**
* @description Deletes a booking block by ID
*/
export async function DELETE(request: NextRequest) { export async function DELETE(request: NextRequest) {
try { try {
const isAdmin = await validateAdmin(request) const isAdmin = await validateAdmin(request)

View File

@@ -17,6 +17,9 @@ async function validateAdminOrStaff(request: NextRequest) {
return true return true
} }
/**
* @description Marks staff as unavailable for a time period
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const hasAccess = await validateAdminOrStaff(request) const hasAccess = await validateAdminOrStaff(request)
@@ -119,6 +122,9 @@ export async function POST(request: NextRequest) {
} }
} }
/**
* @description Retrieves staff unavailability records
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const hasAccess = await validateAdminOrStaff(request) const hasAccess = await validateAdminOrStaff(request)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Retrieves available staff for a time range
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Retrieves detailed availability time slots for a date
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Updates the status of a specific booking
*/
export async function PATCH( export async function PATCH(
request: NextRequest, request: NextRequest,
{ params }: { params: { id: string } } { params }: { params: { id: string } }

View File

@@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
import { generateShortId } from '@/lib/utils/short-id' import { generateShortId } from '@/lib/utils/short-id'
/**
* @description Creates a new booking
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json() const body = await request.json()
@@ -70,6 +73,7 @@ export async function POST(request: NextRequest) {
const endTimeUtc = endTime.toISOString() const endTimeUtc = endTime.toISOString()
// Check staff availability for the requested time slot
const { data: availableStaff, error: staffError } = await supabaseAdmin.rpc('get_available_staff', { const { data: availableStaff, error: staffError } = await supabaseAdmin.rpc('get_available_staff', {
p_location_id: location_id, p_location_id: location_id,
p_start_time_utc: start_time_utc, p_start_time_utc: start_time_utc,
@@ -93,6 +97,7 @@ export async function POST(request: NextRequest) {
const assignedStaff = availableStaff[0] const assignedStaff = availableStaff[0]
// Check resource availability with service priority
const { data: availableResources, error: resourcesError } = await supabaseAdmin.rpc('get_available_resources_with_priority', { const { data: availableResources, error: resourcesError } = await supabaseAdmin.rpc('get_available_resources_with_priority', {
p_location_id: location_id, p_location_id: location_id,
p_start_time: start_time_utc, p_start_time: start_time_utc,
@@ -117,6 +122,7 @@ export async function POST(request: NextRequest) {
const assignedResource = availableResources[0] const assignedResource = availableResources[0]
// Create or find customer based on email
const { data: customer, error: customerError } = await supabaseAdmin const { data: customer, error: customerError } = await supabaseAdmin
.from('customers') .from('customers')
.upsert({ .upsert({
@@ -141,6 +147,7 @@ export async function POST(request: NextRequest) {
const shortId = await generateShortId() const shortId = await generateShortId()
// Create the booking record with all assigned resources
const { data: booking, error: bookingError } = await supabaseAdmin const { data: booking, error: bookingError } = await supabaseAdmin
.from('bookings') .from('bookings')
.insert({ .insert({
@@ -208,6 +215,9 @@ export async function POST(request: NextRequest) {
} }
} }
/**
* @description Retrieves bookings with filters
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -4,6 +4,11 @@ import { supabaseAdmin } from '@/lib/supabase/client'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
/**
* @description Creates a Stripe payment intent for booking deposit (50% of service price, max $200)
* @param {NextRequest} request - Request containing booking details
* @returns {NextResponse} Payment intent client secret and amount
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { const {

View File

@@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
import { Kiosk } from '@/lib/db/types' import { Kiosk } from '@/lib/db/types'
/**
* @description Authenticates a kiosk using API key
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json() const body = await request.json()

View File

@@ -18,6 +18,9 @@ async function validateKiosk(request: NextRequest) {
return kiosk return kiosk
} }
/**
* @description Confirms a pending booking by short ID for kiosk
*/
export async function POST( export async function POST(
request: NextRequest, request: NextRequest,
{ params }: { params: { shortId: string } } { params }: { params: { shortId: string } }

View File

@@ -18,6 +18,9 @@ async function validateKiosk(request: NextRequest) {
return kiosk return kiosk
} }
/**
* @description Retrieves pending/confirmed bookings for kiosk
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const kiosk = await validateKiosk(request) const kiosk = await validateKiosk(request)
@@ -72,6 +75,9 @@ export async function GET(request: NextRequest) {
} }
} }
/**
* @description Creates a new booking for kiosk
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const kiosk = await validateKiosk(request) const kiosk = await validateKiosk(request)

View File

@@ -18,6 +18,9 @@ async function validateKiosk(request: NextRequest) {
return kiosk return kiosk
} }
/**
* @description Retrieves available resources for kiosk, filtered by time and service
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const kiosk = await validateKiosk(request) const kiosk = await validateKiosk(request)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* Validates kiosk API key and returns kiosk info if valid
*/
async function validateKiosk(request: NextRequest) { async function validateKiosk(request: NextRequest) {
const apiKey = request.headers.get('x-kiosk-api-key') const apiKey = request.headers.get('x-kiosk-api-key')
@@ -18,6 +21,9 @@ async function validateKiosk(request: NextRequest) {
return kiosk return kiosk
} }
/**
* @description Creates a walk-in booking for kiosk
*/
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const kiosk = await validateKiosk(request) const kiosk = await validateKiosk(request)
@@ -45,6 +51,7 @@ export async function POST(request: NextRequest) {
) )
} }
// Validate service exists and is active
const { data: service, error: serviceError } = await supabaseAdmin const { data: service, error: serviceError } = await supabaseAdmin
.from('services') .from('services')
.select('*') .select('*')
@@ -75,6 +82,7 @@ export async function POST(request: NextRequest) {
const assignedStaff = availableStaff[0] const assignedStaff = availableStaff[0]
// For walk-ins, booking starts immediately
const startTime = new Date() const startTime = new Date()
const endTime = new Date(startTime) const endTime = new Date(startTime)
endTime.setMinutes(endTime.getMinutes() + service.duration_minutes) endTime.setMinutes(endTime.getMinutes() + service.duration_minutes)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Retrieves all active locations
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { data: locations, error } = await supabaseAdmin const { data: locations, error } = await supabaseAdmin

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Retrieves active services, optionally filtered by location
*/
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)

View File

@@ -12,6 +12,7 @@ import { format } from 'date-fns'
import { es } from 'date-fns/locale' import { es } from 'date-fns/locale'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
/** @description Booking confirmation and payment page component for completing appointment reservations. */
export default function CitaPage() { export default function CitaPage() {
const { user, loading: authLoading } = useAuth() const { user, loading: authLoading } = useAuth()
const router = useRouter() const router = useRouter()

View File

@@ -7,6 +7,7 @@ import { CheckCircle2, Calendar, Clock, MapPin, User, Mail } from 'lucide-react'
import { format } from 'date-fns' import { format } from 'date-fns'
import { es } from 'date-fns/locale' import { es } from 'date-fns/locale'
/** @description Booking confirmation page component displaying appointment details and important information after successful booking. */
export default function ConfirmacionPage() { export default function ConfirmacionPage() {
const [bookingDetails, setBookingDetails] = useState<any>(null) const [bookingDetails, setBookingDetails] = useState<any>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)

View File

@@ -8,6 +8,7 @@ import { Label } from '@/components/ui/label'
import { Mail, CheckCircle } from 'lucide-react' import { Mail, CheckCircle } from 'lucide-react'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
/** @description Login page component for customer authentication using magic link emails. */
export default function LoginPage() { export default function LoginPage() {
const { signIn } = useAuth() const { signIn } = useAuth()
const [email, setEmail] = useState('') const [email, setEmail] = useState('')

View File

@@ -7,6 +7,7 @@ import { Calendar, Clock, MapPin, User, DollarSign } from 'lucide-react'
import { format } from 'date-fns' import { format } from 'date-fns'
import { es } from 'date-fns/locale' import { es } from 'date-fns/locale'
/** @description Customer appointments management page component for viewing and managing existing bookings. */
export default function MisCitasPage() { export default function MisCitasPage() {
const [bookings, setBookings] = useState<any[]>([]) const [bookings, setBookings] = useState<any[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)

View File

@@ -11,6 +11,7 @@ import { format } from 'date-fns'
import { es } from 'date-fns/locale' import { es } from 'date-fns/locale'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
/** @description Customer profile management page component for viewing and editing personal information and booking history. */
export default function PerfilPage() { export default function PerfilPage() {
const { user, loading: authLoading } = useAuth() const { user, loading: authLoading } = useAuth()
const router = useRouter() const router = useRouter()

View File

@@ -3,6 +3,7 @@
import { useState } from 'react' import { useState } from 'react'
import { MapPin, Phone, Mail, Clock } from 'lucide-react' import { MapPin, Phone, Mail, Clock } from 'lucide-react'
/** @description Contact page component with contact information and contact form for inquiries. */
export default function ContactoPage() { export default function ContactoPage() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nombre: '', nombre: '',

View File

@@ -3,6 +3,7 @@
import { useState } from 'react' import { useState } from 'react'
import { Building2, Map, CheckCircle, Mail, Phone } from 'lucide-react' import { Building2, Map, CheckCircle, Mail, Phone } from 'lucide-react'
/** @description Franchise information and application page component for potential franchise partners. */
export default function FranchisesPage() { export default function FranchisesPage() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nombre: '', nombre: '',

View File

@@ -1,3 +1,4 @@
/** @description Company history and philosophy page component explaining the brand's foundation and values. */
export default function HistoriaPage() { export default function HistoriaPage() {
return ( return (
<div className="section"> <div className="section">

View File

@@ -10,62 +10,30 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { format } from 'date-fns' import { format } from 'date-fns'
import { Calendar, Clock, MapPin, Users, CheckCircle, XCircle, AlertCircle } from 'lucide-react' import { Calendar, Clock, MapPin, Users, CheckCircle, XCircle, AlertCircle } from 'lucide-react'
import type { Location, Staff, Booking } from '@/lib/db/types'
interface Booking { type ApiStaff = {
id: string
short_id: string
status: string
start_time_utc: string
end_time_utc: string
notes: string | null
is_paid: boolean
customer: {
id: string
email: string
first_name: string | null
last_name: string | null
phone: string | null
}
service: {
id: string
name: string
duration_minutes: number
base_price: number
}
resource?: {
id: string
name: string
type: string
}
staff: {
id: string
display_name: string
}
location: {
id: string
name: string
}
}
interface Location {
id: string
name: string
}
interface Staff {
staff_id: string staff_id: string
staff_name: string staff_name: string
role: string role: string
work_hours_start: string | null work_hours_start?: string
work_hours_end: string | null work_hours_end?: string
work_days: string | null
location_id: string
} }
type ApiBooking = Omit<Booking, 'status'> & {
status: string
service?: any
customer?: any
staff?: any
location?: any
resource?: any
}
/** @description HQ operations dashboard component for managing bookings, staff availability, and location operations. */
export default function HQDashboard() { export default function HQDashboard() {
const [locations, setLocations] = useState<Location[]>([]) const [locations, setLocations] = useState<Location[]>([])
const [staffList, setStaffList] = useState<Staff[]>([]) const [staffList, setStaffList] = useState<ApiStaff[]>([])
const [bookings, setBookings] = useState<Booking[]>([]) const [bookings, setBookings] = useState<ApiBooking[]>([])
const [selectedLocation, setSelectedLocation] = useState<string>('') const [selectedLocation, setSelectedLocation] = useState<string>('')
const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd')) const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd'))
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -117,11 +85,11 @@ export default function HQDashboard() {
}) })
const data = await response.json() const data = await response.json()
if (data.bookings) { if (data.bookings) {
const filtered = data.bookings.filter((b: Booking) => { const filtered = data.bookings.filter((b: any) => {
const bookingDate = new Date(b.start_time_utc) const bookingDate = new Date(b.start_time_utc)
return bookingDate >= new Date(startDate) && bookingDate < new Date(endDate) return bookingDate >= new Date(startDate) && bookingDate < new Date(endDate)
}) })
setBookings(filtered) setBookings(filtered as Booking[])
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch bookings:', error) console.error('Failed to fetch bookings:', error)
@@ -267,14 +235,14 @@ export default function HQDashboard() {
{format(new Date(booking.start_time_utc), 'HH:mm')} - {format(new Date(booking.end_time_utc), 'HH:mm')} {format(new Date(booking.start_time_utc), 'HH:mm')} - {format(new Date(booking.end_time_utc), 'HH:mm')}
</span> </span>
</div> </div>
<h3 className="font-semibold text-lg">{booking.service.name}</h3> <h3 className="font-semibold text-lg">{booking.service?.name || 'Service'}</h3>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
{booking.customer.first_name} {booking.customer.last_name} ({booking.customer.email}) {booking.customer?.first_name} {booking.customer?.last_name} ({booking.customer?.email})
</p> </p>
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500"> <div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Users className="w-4 h-4" /> <Users className="w-4 h-4" />
{booking.staff.display_name} {booking.staff?.display_name || 'Staff'}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<MapPin className="w-4 h-4" /> <MapPin className="w-4 h-4" />
@@ -289,10 +257,10 @@ export default function HQDashboard() {
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-lg font-semibold"> <p className="text-lg font-semibold">
${booking.service.base_price.toFixed(2)} ${booking.service?.base_price?.toFixed(2) || '0.00'}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-sm text-gray-500">
{booking.service.duration_minutes} min {booking.service?.duration_minutes || 0} min
</p> </p>
</div> </div>
</div> </div>
@@ -329,9 +297,9 @@ export default function HQDashboard() {
</Badge> </Badge>
{getStatusBadge(booking.status)} {getStatusBadge(booking.status)}
</div> </div>
<h3 className="font-semibold">{booking.service.name}</h3> <h3 className="font-semibold">{booking.service?.name || 'Service'}</h3>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{booking.customer.first_name} {booking.customer.last_name} {booking.customer?.first_name} {booking.customer?.last_name}
</p> </p>
</div> </div>
<div className="text-right"> <div className="text-right">
@@ -339,7 +307,7 @@ export default function HQDashboard() {
{format(new Date(booking.start_time_utc), 'HH:mm')} {format(new Date(booking.start_time_utc), 'HH:mm')}
</p> </p>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{booking.staff.display_name} {booking.staff?.display_name || 'Staff'}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ import { BookingConfirmation } from '@/components/kiosk/BookingConfirmation'
import { WalkInFlow } from '@/components/kiosk/WalkInFlow' import { WalkInFlow } from '@/components/kiosk/WalkInFlow'
import { Calendar, UserPlus, MapPin, Clock } from 'lucide-react' import { Calendar, UserPlus, MapPin, Clock } from 'lucide-react'
/** @description Kiosk interface component for location-based check-in confirmations and walk-in booking creation. */
export default function KioskPage({ params }: { params: { locationId: string } }) { export default function KioskPage({ params }: { params: { locationId: string } }) {
const [apiKey, setApiKey] = useState<string | null>(null) const [apiKey, setApiKey] = useState<string | null>(null)
const [location, setLocation] = useState<any>(null) const [location, setLocation] = useState<any>(null)

View File

@@ -1,3 +1,4 @@
/** @description Legal terms and conditions page component outlining salon policies and user agreements. */
export default function LegalPage() { export default function LegalPage() {
return ( return (
<div className="section"> <div className="section">

View File

@@ -3,6 +3,7 @@
import { useState } from 'react' import { useState } from 'react'
import { Crown, Star, Award, Diamond } from 'lucide-react' import { Crown, Star, Award, Diamond } from 'lucide-react'
/** @description Membership tiers page component displaying exclusive membership options and application forms. */
export default function MembresiasPage() { export default function MembresiasPage() {
const [selectedTier, setSelectedTier] = useState<string | null>(null) const [selectedTier, setSelectedTier] = useState<string | null>(null)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({

View File

@@ -1,3 +1,4 @@
/** @description Home page component for the salon website, featuring hero section, services preview, and testimonials. */
export default function HomePage() { export default function HomePage() {
return ( return (
<> <>

View File

@@ -1,3 +1,4 @@
/** @description Privacy policy page component explaining data collection, usage, and user rights. */
export default function PrivacyPolicyPage() { export default function PrivacyPolicyPage() {
return ( return (
<div className="section"> <div className="section">

View File

@@ -1,3 +1,4 @@
/** @description Static services page component displaying available salon services and categories. */
export default function ServiciosPage() { export default function ServiciosPage() {
const services = [ const services = [
{ {

View File

@@ -11,6 +11,9 @@ interface BookingConfirmationProps {
onCancel: () => void onCancel: () => void
} }
/**
* BookingConfirmation component that allows confirming a booking by short ID.
*/
export function BookingConfirmation({ apiKey, onConfirm, onCancel }: BookingConfirmationProps) { export function BookingConfirmation({ apiKey, onConfirm, onCancel }: BookingConfirmationProps) {
const [shortId, setShortId] = useState('') const [shortId, setShortId] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)

View File

@@ -16,6 +16,9 @@ interface ResourceAssignmentProps {
end_time: string end_time: string
} }
/**
* ResourceAssignment component that displays available resources for booking.
*/
export function ResourceAssignment({ resources, start_time, end_time }: ResourceAssignmentProps) { export function ResourceAssignment({ resources, start_time, end_time }: ResourceAssignmentProps) {
const formatDateTime = (dateTime: string) => { const formatDateTime = (dateTime: string) => {
const date = new Date(dateTime) const date = new Date(dateTime)

View File

@@ -13,6 +13,9 @@ interface WalkInFlowProps {
onCancel: () => void onCancel: () => void
} }
/**
* WalkInFlow component that manages the walk-in booking process in steps.
*/
export function WalkInFlow({ apiKey, onComplete, onCancel }: WalkInFlowProps) { export function WalkInFlow({ apiKey, onComplete, onCancel }: WalkInFlowProps) {
const [step, setStep] = useState<'services' | 'customer' | 'confirm' | 'success'>('services') const [step, setStep] = useState<'services' | 'customer' | 'confirm' | 'success'>('services')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)

View File

@@ -27,6 +27,9 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
/**
* Badge component that renders a styled badge with support for different variants.
*/
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <div className={cn(badgeVariants({ variant }), className)} {...props} />

View File

@@ -38,6 +38,9 @@ export interface ButtonProps
asChild?: boolean asChild?: boolean
} }
/**
* Button component that renders a styled button with various variants and sizes.
*/
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"

View File

@@ -2,6 +2,9 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
/**
* Card component that renders a container with border, background, and shadow.
*/
const Card = React.forwardRef< const Card = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
@@ -17,6 +20,9 @@ const Card = React.forwardRef<
)) ))
Card.displayName = "Card" Card.displayName = "Card"
/**
* CardHeader component for the header section of a card.
*/
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
@@ -29,6 +35,9 @@ const CardHeader = React.forwardRef<
)) ))
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader"
/**
* CardTitle component for the title within a card header.
*/
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement> React.HTMLAttributes<HTMLHeadingElement>
@@ -44,6 +53,9 @@ const CardTitle = React.forwardRef<
)) ))
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle"
/**
* CardDescription component for the description text in a card.
*/
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
@@ -56,6 +68,9 @@ const CardDescription = React.forwardRef<
)) ))
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription"
/**
* CardContent component for the main content area of a card.
*/
const CardContent = React.forwardRef< const CardContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
@@ -64,6 +79,9 @@ const CardContent = React.forwardRef<
)) ))
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent"
/**
* CardFooter component for the footer section of a card.
*/
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>

View File

@@ -4,6 +4,9 @@ import { cn } from "@/lib/utils"
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {}
/**
* Input component that renders a styled input element.
*/
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
return ( return (

View File

@@ -7,6 +7,9 @@ const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
) )
/**
* Label component that renders a styled label element.
*/
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &

View File

@@ -10,6 +10,9 @@ const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value
/**
* SelectTrigger component that renders the button to open the select dropdown.
*/
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
@@ -30,6 +33,9 @@ const SelectTrigger = React.forwardRef<
)) ))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
/**
* SelectScrollUpButton component for the up scroll button in the select content.
*/
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
@@ -47,6 +53,9 @@ const SelectScrollUpButton = React.forwardRef<
)) ))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
/**
* SelectScrollDownButton component for the down scroll button in the select content.
*/
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
@@ -64,6 +73,9 @@ const SelectScrollDownButton = React.forwardRef<
)) ))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
/**
* SelectContent component that renders the dropdown content of the select.
*/
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
@@ -96,6 +108,9 @@ const SelectContent = React.forwardRef<
)) ))
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName
/**
* SelectLabel component for labels within the select content.
*/
const SelectLabel = React.forwardRef< const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
@@ -108,6 +123,9 @@ const SelectLabel = React.forwardRef<
)) ))
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName
/**
* SelectItem component for individual selectable items in the select.
*/
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
@@ -131,6 +149,9 @@ const SelectItem = React.forwardRef<
)) ))
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName
/**
* SelectSeparator component for separators between select items.
*/
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>

View File

@@ -5,6 +5,9 @@ import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root
/**
* TabsList component that renders the container for tab triggers.
*/
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
@@ -20,6 +23,9 @@ const TabsList = React.forwardRef<
)) ))
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName
/**
* TabsTrigger component for individual tab buttons.
*/
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
@@ -35,6 +41,9 @@ const TabsTrigger = React.forwardRef<
)) ))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
/**
* TabsContent component for the content of each tab panel.
*/
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>

152
docs/CLIENT_ONBOARDING.md Normal file
View File

@@ -0,0 +1,152 @@
# Guía de Onboarding para Clientes — SalonOS
Bienvenido a Anchor23. Esta guía te ayudará a familiarizarte con nuestros sistemas de reserva y servicios.
## Primeros Pasos
### Registro Inicial
1. Visita `anchor23.mx` para conocer nuestros servicios
2. Elige membresía según tus necesidades:
- **Free**: Acceso básico, invitaciones requeridas
- **Gold**: Beneficios premium, descuentos en servicios
- **Black**: Servicios prioritarios, acceso VIP
- **VIP**: Experiencia exclusiva, concierge personal
### Reserva de Primera Cita
1. Ir a `booking.anchor23.mx`
2. Seleccionar servicio deseado
3. Elegir fecha y hora disponible
4. Completar información personal
5. Realizar depósito (50% o $200 máximo)
## Uso de The Boutique (Sistema de Reservas)
### Navegación
- **Servicios**: Explora catálogo completo
- **Mi Perfil**: Gestiona información personal
- **Mis Citas**: Historial y gestión de reservas
- **Membresías**: Actualiza o mejora tu tier
### Hacer una Reserva
1. Seleccionar servicio y duración
2. Elegir location preferida
3. Ver slots disponibles en calendario
4. Confirmar datos personales
5. Pagar depósito requerido
### Modificar Reserva
- Cambios permitidos hasta 12h antes
- Penalización por no-show: retención de depósito
- Contactar staff para cambios especiales
## Sistema de Invitaciones
### Cómo Funciona
- Sistema de crecimiento controlado
- Invitaciones semanales por tier
- Código único por invitado
- Seguimiento de referrals
### Enviar Invitación
1. Acceder a sección de invitaciones
2. Generar código único
3. Compartir con potencial cliente
4. Recibir beneficios por conversiones
## Membresías y Beneficios
### Tiers Disponibles
- **Free**: 1 invitación/semana, servicios estándar
- **Gold**: 3 invitaciones/semana, 10% descuento
- **Black**: 5 invitaciones/semana, prioridad en agenda, 20% descuento
- **VIP**: Invitaciones ilimitadas, concierge, descuentos exclusivos
### Upgrade de Membresía
- Basado en gasto acumulado
- Beneficios inmediatos al ascender
- Renovación automática anual
## Pagos y Depósitos
### Método de Pago
- Stripe integration segura
- Tarjetas de crédito/débito
- Depósito requerido para confirmar
### Política de Depósitos
- 50% del valor del servicio
- Máximo $200 por reserva
- Reembolsable según política de cancelación
## Comunicación y Notificaciones
### Canales
- Email para confirmaciones
- WhatsApp para recordatorios (próximamente)
- SMS para urgencias
### Recordatorios Automáticos
- 24h antes de cita
- 2h antes de cita
- Confirmación de llegada vía kiosko
## Uso del Kiosko
### Llegada al Salón
1. Acercarse a pantalla táctil en entrada
2. Ingresar código de 6 dígitos (short ID)
3. Confirmar llegada
4. Recibir instrucciones de staff
### Reservas Walk-in
- Disponible para servicios express
- Pago en efectivo o tarjeta en recepción
- Asignación automática de staff y recursos
## Políticas Importantes
### Cancelación
- Hasta 12h antes: reembolso completo
- Menos de 12h: retención de depósito
- No-show: penalización completa
### Puntuación y Privacidad
- Datos protegidos por RLS
- Acceso limitado por rol
- Auditoría completa de cambios
## Soporte al Cliente
### Contacto
- **Email**: hello@anchor23.mx
- **Teléfono**: [Número de contacto]
- **WhatsApp**: [Número de WhatsApp]
- **Horarios**: Lunes-Domingo, 9AM-8PM
### Problemas Comunes
- Reserva no aparece: verificar email de confirmación
- Cambio de horario: contactar 24h antes
- Problemas técnicos: soporte disponible 24/7
## Consejos para Mejor Experiencia
### Planificación
- Reservar con anticipación para servicios populares
- Considerar tiempos de viaje al salón
- Traer identificación para primera visita
### Preparación
- Confirmar detalles 24h antes
- Llegar 15 minutos temprano
- Comunicar alergias o necesidades especiales
### Post-Servicio
- Feedback valorado para mejorar
- Invitar amigos para beneficios adicionales
- Mantener membresía activa
---
¡Gracias por elegir Anchor23! Tu satisfacción es nuestra prioridad.
*Última actualización: Enero 2026*

View File

@@ -0,0 +1,174 @@
# Procedimientos Operativos — SalonOS
Guía interna para operaciones diarias del salón usando el sistema SalonOS.
## Apertura Diaria
### Checklist Matutino
- [ ] Verificar conexión a internet y sistemas
- [ ] Revisar bookings del día en Aperture dashboard
- [ ] Confirmar staff asignado y horarios
- [ ] Verificar disponibilidad de recursos (estaciones, equipos)
- [ ] Probar kioskos táctiles
- [ ] Revisar inventario crítico
### Monitoreo Continuo
- Monitorear ocupación en tiempo real
- Gestionar walk-ins según disponibilidad
- Resolver conflictos de asignación inmediatamente
## Gestión de Reservas
### Flujo Estándar
1. **Recepción**: Cliente confirmado vía kiosko o staff
2. **Asignación**: Staff y recurso asignados automáticamente
3. **Servicio**: Tracking de tiempo y progreso
4. **Pago**: Procesamiento final si no pagado
5. **Cierre**: Actualización de status y notas
### Casos Especiales
- **Sobreasignación**: Reasignar recursos premium
- **Cancelación última hora**: Liberar slot, notificar siguiente cliente
- **No-show**: Marcar automáticamente, aplicar penalización
## Manejo de Recursos
### Tipos de Recursos
- **Stations**: Puestos de trabajo fijos
- **Rooms**: Salas privadas para servicios premium
- **Equipment**: Herramientas móviles (secadores, etc.)
### Mantenimiento
- Limpieza entre clientes
- Reporte de equipos dañados
- Programación de mantenimiento preventivo
### Optimización
- Maximizar uso de recursos premium
- Balancear carga entre locations
- Anticipar demanda por temporada
## Gestión de Personal
### Asignación Diaria
- Revisar schedule semanal
- Ajustar por ausencias o sobrecarga
- Comunicar cambios inmediatamente
### Rendimiento
- Tracking de bookings completados
- Medición de tiempo por servicio
- Feedback de clientes por staff
### Capacitación
- Onboarding para nuevo personal
- Actualizaciones de procedimientos
- Certificación en uso de sistemas
## Finanzas y Reportes
### Cierre Diario
- Verificar todos los pagos procesados
- Generar reporte de ingresos
- Revisar depósitos pendientes
### Reportes Semanales
- Utilización de recursos
- Rendimiento por staff
- Tendencias de demanda
- Análisis de no-shows
### Contabilidad
- Reconciliación con Stripe
- Seguimiento de depósitos retenidos
- Cálculo de comisiones por staff
## Manejo de Clientes VIP
### Protocolo Especial
- Confirmación personal por manager
- Asignación de mejores recursos
- Comunicación premium (WhatsApp, email personalizado)
- The Vault para fotos privadas
### Seguimiento
- Historial detallado de preferencias
- Notas privadas de staff
- Programa de lealtad personalizado
## Seguridad y Privacidad
### Protección de Datos
- RLS aplicado estrictamente
- Acceso limitado por rol
- Auditoría completa de cambios
### Seguridad Física
- Control de acceso a sistemas
- Monitoreo de kioskos
- Backup de datos crítico
## Contingencias
### Sistema Caído
1. Comunicación inmediata a todos los staff
2. Uso de backup manual (libreta, teléfono)
3. Priorizar clientes VIP y bookings confirmados
4. Notificar a soporte técnico
### Sobrecarga de Agenda
1. Evaluar capacidad real vs bookings
2. Contactar clientes para reagendar
3. Activar protocolo de espera
4. Documentar para análisis posterior
### Conflicto con Cliente
1. Mantener calma y profesionalismo
2. Escalar a manager si necesario
3. Documentar incidente completo
4. Aplicar compensación según política
## Mejores Prácticas
### Eficiencia
- Minimizar tiempo entre servicios
- Optimizar rutas de recursos
- Automatizar tareas repetitivas
### Calidad
- Verificación de preparación de staff
- Feedback continuo de clientes
- Mantenimiento de estándares de servicio
### Innovación
- Probar nuevas funcionalidades del sistema
- Recopilar feedback de staff y clientes
- Implementar mejoras operativas
## Métricas Clave
### KPIs Diarios
- Ocupación de recursos: >85%
- Tasa de no-show: <5%
- Satisfacción cliente: >4.8/5
### KPIs Semanales
- Ingresos vs presupuesto
- Utilización por staff
- Conversion de walk-ins
### KPIs Mensuales
- Retención de clientes
- Crecimiento de membresías
- ROI de inversiones
## Contactos de Emergencia
- **Soporte Técnico**: soporte@anchor23.mx | +52 [tel]
- **Manager General**: [Nombre] | [tel]
- **Contabilidad**: [Nombre] | [tel]
- **Legal**: [Nombre] | [tel]
---
*Revisar y actualizar mensualmente. Última actualización: Enero 2026*

147
docs/STAFF_TRAINING.md Normal file
View File

@@ -0,0 +1,147 @@
# Guía de Entrenamiento para Staff — SalonOS
Esta guía está diseñada para capacitar al personal del salón en el uso del sistema SalonOS.
## Introducción al Sistema
SalonOS es una plataforma integral de gestión que incluye:
- Sistema de reservas (The Boutique)
- Dashboard administrativo (Aperture)
- Sistema de kiosko para autoservicio
- Integraciones con pagos y calendario
## Roles y Permisos
### Tipos de Usuario
- **Admin**: Acceso completo a configuración y gestión
- **Manager**: Gestión de staff, recursos y reportes
- **Staff/Artist**: Acceso limitado a sus horarios y asignaciones
### Políticas de Seguridad
- Nunca compartir credenciales
- Usar autenticación de dos factores cuando disponible
- Reportar cualquier acceso sospechoso
## Uso Diario del Sistema
### Acceso al Dashboard (Aperture)
1. Ir a `aperture.anchor23.mx`
2. Iniciar sesión con credenciales asignadas
3. Navegar por las pestañas: Dashboard, Staff, Resources, Reports
### Gestión de Horarios
#### Ver Disponibilidad
- Usar calendario para ver bookings asignados
- Filtrar por fecha y staff member
- Colores indican status: confirmado (verde), pendiente (amarillo), cancelado (rojo)
#### Agregar Bloqueos
- Click derecho en slot vacío
- Seleccionar "Bloquear tiempo"
- Elegir motivo: descanso, capacitación, mantenimiento
### Gestión de Clientes
#### Buscar Cliente
- Usar search por nombre, email o teléfono
- Ver historial completo de visitas
- Acceder a notas privadas del cliente
#### Modificar Reserva
- Buscar booking por short ID o cliente
- Cambiar horario si hay disponibilidad
- Notificar cambios al cliente automáticamente
## Sistema de Kiosko
### Configuración Inicial
- Cada kiosko tiene API key única
- Configurar en pantalla táctil con enrollment code
- Probar conexión antes de uso público
### Monitoreo de Actividad
- Revisar logs de kiosk en admin dashboard
- Verificar reservas walk-in creadas correctamente
- Monitorear uso de recursos en tiempo real
## Manejo de Pagos y Depósitos
### Depósitos Dinámicos
- Automático: 50% del servicio o $200 máximo
- Verificar pago antes de confirmar servicio
- Manejar reembolsos según política
### Penalizaciones por No-Show
- Sistema automático marca después de ventana de 12h
- Retención de depósito según reglas
- Comunicación automática al cliente
## Reportes y Analytics
### Reportes Diarios
- Ingresos por día/servicio
- Utilización de recursos
- No-shows y cancelaciones
### Métricas de Rendimiento
- Por staff member: bookings completados, ingresos generados
- Por servicio: popularidad, tiempo promedio
- Por location: ocupación, rentabilidad
## Procedimientos de Emergencia
### Sistema Caído
- Contactar soporte técnico inmediatamente
- Usar backup manual para reservas críticas
- Comunicar a clientes vía teléfono/email
### Reserva Duplicada
- Verificar con cliente antes de cancelar
- Mantener la más reciente
- Documentar en notas del cliente
### Conflicto de Recursos
- Revisar asignaciones automáticamente
- Reasignar manualmente si necesario
- Notificar a staff afectado
## Mejores Prácticas
### Comunicación con Clientes
- Confirmar cambios inmediatamente
- Ofrecer alternativas cuando no hay disponibilidad
- Mantener tono profesional y empático
### Mantenimiento de Datos
- Actualizar información de contacto regularmente
- Limpiar bookings cancelados mensualmente
- Backup semanal de datos críticos
### Optimización de Agenda
- Maximizar ocupación de recursos premium
- Balancear carga entre staff members
- Anticipar demanda por día de la semana
## Capacitación Continua
### Actualizaciones del Sistema
- Revisar changelog mensual
- Participar en sesiones de training
- Reportar bugs o sugerencias
### Certificación
- Completar módulo básico de onboarding
- Recertificación anual requerida
- Bonos por perfect attendance
## Contacto y Soporte
- **Soporte Técnico**: soporte@anchor23.mx
- **Manager de Location**: [Nombre del manager]
- **Documentación**: docs/ en repositorio
---
*Última actualización: Enero 2026*

173
docs/TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,173 @@
# Guía de Troubleshooting — SalonOS
Esta guía ayuda a resolver problemas comunes durante el setup y desarrollo de SalonOS.
## Configuración de Entorno
### Supabase Auth Issues
#### Error: "Auth session not found"
- **Causa**: Variables de entorno de Supabase mal configuradas
- **Solución**:
- Verificar `NEXT_PUBLIC_SUPABASE_URL` y `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- Asegurarse que las URLs no tengan trailing slash
- Probar conexión: `curl https://your-project.supabase.co/rest/v1/`
#### Error: "RLS policy violation"
- **Causa**: Políticas de Row Level Security no aplicadas
- **Solución**:
- Ejecutar migraciones: `supabase db push`
- Verificar políticas en Supabase Dashboard > Authentication > Policies
- Para kioskos: asegurar API key válida en headers `x-kiosk-api-key`
#### Error: "Magic link not received"
- **Causa**: SMTP no configurado en Supabase
- **Solución**:
- Configurar SMTP en Supabase Dashboard > Authentication > Email Templates
- Usar servicio como SendGrid o AWS SES
- Probar con email de prueba en dashboard
## Integraciones Externas
### Stripe Webhooks
#### Error: "Webhook signature verification failed"
- **Causa**: Webhook secret mal configurado
- **Solución**:
- Obtener secret desde Stripe Dashboard > Developers > Webhooks
- Configurar `STRIPE_WEBHOOK_SECRET` en variables de entorno
- Verificar endpoint URL en Stripe coincida con producción
#### Error: "Payment intent not found"
- **Causa**: Cliente secret expirado o inválido
- **Solución**:
- Regenerar payment intent en backend
- Verificar tiempo de expiración (24h por defecto)
- Usar idempotency key para evitar duplicados
#### Error: "Deposit calculation incorrect"
- **Causa**: Lógica de depósito no actualizada
- **Solución**:
- Verificar regla: MIN(service_price * 0.5, 200)
- Probar con diferentes precios de servicio
- Revisar logs de webhook para valores
### Google Calendar
#### Error: "Service account authentication failed"
- **Causa**: Credenciales de Google incorrectas
- **Solución**:
- Descargar JSON de service account desde Google Cloud Console
- Configurar `GOOGLE_SERVICE_ACCOUNT_JSON` como string JSON
- Verificar permisos: Calendar API enabled, service account tiene acceso al calendar
#### Error: "Calendar sync conflicts"
- **Causa**: Eventos duplicados o sobrepuestos
- **Solución**:
- Implementar lógica de merge para conflictos
- Usar event ID como key para evitar duplicados
- Loggear conflictos para resolución manual
## Base de Datos
### Migraciones
#### Error: "Migration failed"
- **Causa**: Dependencias de migración no resueltas
- **Solución**:
- Ejecutar en orden: `supabase migration up`
- Verificar foreign keys existen antes de crear constraints
- Backup antes de migrar en producción
#### Error: "Duplicate key value violates unique constraint"
- **Causa**: Datos existentes violan nueva constraint
- **Solución**:
- Limpiar datos conflictivos antes de migrar
- Usar `ON CONFLICT` en inserts
- Revisar seeds data
### RPC Functions
#### Error: "Function does not exist"
- **Causa**: Función no creada en Supabase
- **Solución**:
- Ejecutar SQL de funciones desde migrations
- Verificar nombre exacto de función
- Probar directamente en Supabase SQL Editor
## Frontend Issues
### Next.js Build
#### Error: "Module not found"
- **Causa**: Dependencias faltantes
- **Solución**:
- Ejecutar `npm install` o `yarn install`
- Verificar package.json versiones compatibles
- Limpiar node_modules: `rm -rf node_modules && npm install`
#### Error: "TypeScript errors"
- **Causa**: Tipos desactualizados
- **Solución**:
- Regenerar types: `supabase gen types typescript --local > lib/db/types.ts`
- Verificar imports correctos
- Usar `any` temporalmente para debugging
## Kiosko System
#### Error: "Kiosk not authorized"
- **Causa**: API key inválida o expirada
- **Solución**:
- Generar nueva API key en admin dashboard
- Configurar en variables de entorno del kiosko
- Verificar headers: `x-kiosk-api-key`
#### Error: "No resources available"
- **Causa**: Recursos no asignados o bloqueados
- **Solución**:
- Verificar migración de recursos ejecutada
- Chequear disponibilidad por horario
- Revisar lógica de priority assignment
## Deployment
### Vercel Issues
#### Error: "Build failed"
- **Causa**: Variables de entorno faltantes
- **Solución**:
- Configurar todas las env vars en Vercel dashboard
- Verificar build logs para errores específicos
- Usar `--verbose` en build command
#### Error: "Domain configuration failed"
- **Causa**: Wildcard domains no soportados
- **Solución**:
- Configurar subdominios individuales
- Usar proxy reverso para wildcard routing
- Verificar DNS settings
## Logs y Debugging
### Verificar Logs
- **Supabase**: Dashboard > Logs > API/PostgreSQL
- **Vercel**: Dashboard > Functions > Logs
- **Stripe**: Dashboard > Developers > Logs
- **Local**: `npm run dev` con console.log
### Common Debug Steps
1. Verificar variables de entorno
2. Probar endpoints con curl/Postman
3. Revisar network tab en browser dev tools
4. Chequear logs de errores
5. Verificar permisos y políticas
## Contacto
Si el problema persiste, documentar:
- Pasos para reproducir
- Logs de error completos
- Configuración del entorno
- Versiones de dependencias
Crear issue en GitHub con esta información.

View File

@@ -14,6 +14,9 @@ type AuthContextType = {
const AuthContext = createContext<AuthContextType | undefined>(undefined) const AuthContext = createContext<AuthContextType | undefined>(undefined)
/**
* AuthProvider component that manages authentication state and provides it to children.
*/
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null) const [user, setUser] = useState<User | null>(null)
const [session, setSession] = useState<Session | null>(null) const [session, setSession] = useState<Session | null>(null)
@@ -72,6 +75,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider> return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
} }
/**
* useAuth hook that returns the current authentication context.
*/
export function useAuth() { export function useAuth() {
const context = useContext(AuthContext) const context = useContext(AuthContext)
if (context === undefined) { if (context === undefined) {

View File

@@ -1,5 +1,6 @@
// Types based on SalonOS database schema // Types based on SalonOS database schema
/** User roles in the system */
export type UserRole = 'admin' | 'manager' | 'staff' | 'artist' | 'customer' | 'kiosk' export type UserRole = 'admin' | 'manager' | 'staff' | 'artist' | 'customer' | 'kiosk'
export type CustomerTier = 'free' | 'gold' | 'black' | 'VIP' export type CustomerTier = 'free' | 'gold' | 'black' | 'VIP'
export type BookingStatus = 'pending' | 'confirmed' | 'cancelled' | 'completed' | 'no_show' export type BookingStatus = 'pending' | 'confirmed' | 'cancelled' | 'completed' | 'no_show'
@@ -7,6 +8,7 @@ export type InvitationStatus = 'pending' | 'used' | 'expired'
export type ResourceType = 'station' | 'room' | 'equipment' export type ResourceType = 'station' | 'room' | 'equipment'
export type AuditAction = 'create' | 'update' | 'delete' | 'reset_invitations' | 'payment' | 'status_change' export type AuditAction = 'create' | 'update' | 'delete' | 'reset_invitations' | 'payment' | 'status_change'
/** Represents a salon location with timezone and contact info */
export interface Location { export interface Location {
id: string id: string
name: string name: string
@@ -100,6 +102,7 @@ export interface Invitation {
inviter?: Customer inviter?: Customer
} }
/** Represents a customer booking with service, staff, and resource assignments */
export interface Booking { export interface Booking {
id: string id: string
short_id: string short_id: string

View File

@@ -3,8 +3,10 @@ import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
// Public Supabase client for client-side operations
export const supabase = createClient(supabaseUrl, supabaseAnonKey) export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Admin Supabase client for server-side operations with service role
export const supabaseAdmin = createClient( export const supabaseAdmin = createClient(
supabaseUrl, supabaseUrl,
process.env.SUPABASE_SERVICE_ROLE_KEY!, process.env.SUPABASE_SERVICE_ROLE_KEY!,

View File

@@ -1,6 +1,9 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
/**
* cn function that merges class names using clsx and tailwind-merge.
*/
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }

View File

@@ -1,5 +1,8 @@
import { supabaseAdmin } from '@/lib/supabase/client' import { supabaseAdmin } from '@/lib/supabase/client'
/**
* generateShortId function that generates a unique short ID using Supabase RPC.
*/
export async function generateShortId(): Promise<string> { export async function generateShortId(): Promise<string> {
const { data, error } = await supabaseAdmin.rpc('generate_short_id') const { data, error } = await supabaseAdmin.rpc('generate_short_id')