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
- Disponibilidad dual capa: 100%
- API de reservas: 100%
- The Boutique: 100% (completo con pagos)
- Integración Pagos (Stripe): 100%
- The Boutique: 90% (frontend completo, autenticación y pagos parcialmente implementados)
- Integración Pagos (Stripe): 90% (depósitos implementados, webhooks pendientes)
- Integración Calendar: 20% (en progreso)
- 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.
@@ -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.
@@ -367,7 +407,7 @@ https://kiosk.anchor23.mx/{location-id}
---
## 13. Filosofía Operativa
## 14. Filosofía Operativa
SalonOS no busca volumen.

View File

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

View File

@@ -9,6 +9,7 @@ import { format } from 'date-fns'
import { es } from 'date-fns/locale'
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() {
const { user, loading: authLoading, signOut } = useAuth()
const router = useRouter()

View File

@@ -17,6 +17,9 @@ async function validateAdmin(request: NextRequest) {
return true
}
/**
* @description Retrieves kiosks with filters for admin
*/
export async function GET(request: NextRequest) {
try {
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) {
try {
const isAdmin = await validateAdmin(request)

View File

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

View File

@@ -17,6 +17,9 @@ async function validateAdmin(request: NextRequest) {
return true
}
/**
* @description Retrieves staff users with filters for admin
*/
export async function GET(request: NextRequest) {
try {
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) {
try {
const isAdmin = await validateAdmin(request)

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Fetches bookings with filters for dashboard view
*/
export async function GET(request: NextRequest) {
try {
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() {
return NextResponse.json({
success: true,
@@ -44,6 +47,9 @@ export async function GET() {
})
}
/**
* @description Toggles a specific permission for a role
*/
export async function POST(request: NextRequest) {
const { roleId, permId } = await request.json()

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server'
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() {
try {
// Get total sales

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/client'
/**
* @description Retrieves staff availability schedule with optional filters
*/
export async function GET(request: NextRequest) {
try {
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) {
try {
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) {
try {
const { searchParams } = new URL(request.url)

View File

@@ -17,6 +17,9 @@ async function validateAdmin(request: NextRequest) {
return true
}
/**
* @description Creates a booking block for a resource
*/
export async function POST(request: NextRequest) {
try {
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) {
try {
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) {
try {
const isAdmin = await validateAdmin(request)

View File

@@ -17,6 +17,9 @@ async function validateAdminOrStaff(request: NextRequest) {
return true
}
/**
* @description Marks staff as unavailable for a time period
*/
export async function POST(request: NextRequest) {
try {
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) {
try {
const hasAccess = await validateAdminOrStaff(request)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,9 @@ async function validateKiosk(request: NextRequest) {
return kiosk
}
/**
* @description Retrieves pending/confirmed bookings for kiosk
*/
export async function GET(request: NextRequest) {
try {
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) {
try {
const kiosk = await validateKiosk(request)

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import { format } from 'date-fns'
import { es } from 'date-fns/locale'
import { useAuth } from '@/lib/auth/context'
/** @description Booking confirmation and payment page component for completing appointment reservations. */
export default function CitaPage() {
const { user, loading: authLoading } = useAuth()
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 { es } from 'date-fns/locale'
/** @description Booking confirmation page component displaying appointment details and important information after successful booking. */
export default function ConfirmacionPage() {
const [bookingDetails, setBookingDetails] = useState<any>(null)
const [loading, setLoading] = useState(true)

View File

@@ -8,6 +8,7 @@ import { Label } from '@/components/ui/label'
import { Mail, CheckCircle } from 'lucide-react'
import { useAuth } from '@/lib/auth/context'
/** @description Login page component for customer authentication using magic link emails. */
export default function LoginPage() {
const { signIn } = useAuth()
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 { es } from 'date-fns/locale'
/** @description Customer appointments management page component for viewing and managing existing bookings. */
export default function MisCitasPage() {
const [bookings, setBookings] = useState<any[]>([])
const [loading, setLoading] = useState(false)

View File

@@ -11,6 +11,7 @@ import { format } from 'date-fns'
import { es } from 'date-fns/locale'
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() {
const { user, loading: authLoading } = useAuth()
const router = useRouter()

View File

@@ -3,6 +3,7 @@
import { useState } from '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() {
const [formData, setFormData] = useState({
nombre: '',

View File

@@ -3,6 +3,7 @@
import { useState } from '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() {
const [formData, setFormData] = useState({
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() {
return (
<div className="section">

View File

@@ -10,62 +10,30 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge'
import { format } from 'date-fns'
import { Calendar, Clock, MapPin, Users, CheckCircle, XCircle, AlertCircle } from 'lucide-react'
import type { Location, Staff, Booking } from '@/lib/db/types'
interface Booking {
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 {
type ApiStaff = {
staff_id: string
staff_name: string
role: string
work_hours_start: string | null
work_hours_end: string | null
work_days: string | null
location_id: string
work_hours_start?: string
work_hours_end?: 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() {
const [locations, setLocations] = useState<Location[]>([])
const [staffList, setStaffList] = useState<Staff[]>([])
const [bookings, setBookings] = useState<Booking[]>([])
const [staffList, setStaffList] = useState<ApiStaff[]>([])
const [bookings, setBookings] = useState<ApiBooking[]>([])
const [selectedLocation, setSelectedLocation] = useState<string>('')
const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd'))
const [loading, setLoading] = useState(false)
@@ -117,11 +85,11 @@ export default function HQDashboard() {
})
const data = await response.json()
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)
return bookingDate >= new Date(startDate) && bookingDate < new Date(endDate)
})
setBookings(filtered)
setBookings(filtered as Booking[])
}
} catch (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')}
</span>
</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">
{booking.customer.first_name} {booking.customer.last_name} ({booking.customer.email})
{booking.customer?.first_name} {booking.customer?.last_name} ({booking.customer?.email})
</p>
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Users className="w-4 h-4" />
{booking.staff.display_name}
{booking.staff?.display_name || 'Staff'}
</div>
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
@@ -289,10 +257,10 @@ export default function HQDashboard() {
</div>
<div className="text-right">
<p className="text-lg font-semibold">
${booking.service.base_price.toFixed(2)}
${booking.service?.base_price?.toFixed(2) || '0.00'}
</p>
<p className="text-xs text-gray-500">
{booking.service.duration_minutes} min
<p className="text-sm text-gray-500">
{booking.service?.duration_minutes || 0} min
</p>
</div>
</div>
@@ -329,9 +297,9 @@ export default function HQDashboard() {
</Badge>
{getStatusBadge(booking.status)}
</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">
{booking.customer.first_name} {booking.customer.last_name}
{booking.customer?.first_name} {booking.customer?.last_name}
</p>
</div>
<div className="text-right">
@@ -339,7 +307,7 @@ export default function HQDashboard() {
{format(new Date(booking.start_time_utc), 'HH:mm')}
</p>
<p className="text-sm text-gray-500">
{booking.staff.display_name}
{booking.staff?.display_name || 'Staff'}
</p>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import { BookingConfirmation } from '@/components/kiosk/BookingConfirmation'
import { WalkInFlow } from '@/components/kiosk/WalkInFlow'
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 } }) {
const [apiKey, setApiKey] = useState<string | null>(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() {
return (
<div className="section">

View File

@@ -3,6 +3,7 @@
import { useState } from '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() {
const [selectedTier, setSelectedTier] = useState<string | null>(null)
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() {
return (
<>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
/**
* Input component that renders a styled input element.
*/
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
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"
)
/**
* Label component that renders a styled label element.
*/
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &

View File

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

View File

@@ -5,6 +5,9 @@ import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
/**
* TabsList component that renders the container for tab triggers.
*/
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
@@ -20,6 +23,9 @@ const TabsList = React.forwardRef<
))
TabsList.displayName = TabsPrimitive.List.displayName
/**
* TabsTrigger component for individual tab buttons.
*/
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
@@ -35,6 +41,9 @@ const TabsTrigger = React.forwardRef<
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
/**
* TabsContent component for the content of each tab panel.
*/
const TabsContent = React.forwardRef<
React.ElementRef<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)
/**
* AuthProvider component that manages authentication state and provides it to children.
*/
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | 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>
}
/**
* useAuth hook that returns the current authentication context.
*/
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {

View File

@@ -1,5 +1,6 @@
// Types based on SalonOS database schema
/** User roles in the system */
export type UserRole = 'admin' | 'manager' | 'staff' | 'artist' | 'customer' | 'kiosk'
export type CustomerTier = 'free' | 'gold' | 'black' | 'VIP'
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 AuditAction = 'create' | 'update' | 'delete' | 'reset_invitations' | 'payment' | 'status_change'
/** Represents a salon location with timezone and contact info */
export interface Location {
id: string
name: string
@@ -100,6 +102,7 @@ export interface Invitation {
inviter?: Customer
}
/** Represents a customer booking with service, staff, and resource assignments */
export interface Booking {
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 supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
// Public Supabase client for client-side operations
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Admin Supabase client for server-side operations with service role
export const supabaseAdmin = createClient(
supabaseUrl,
process.env.SUPABASE_SERVICE_ROLE_KEY!,

View File

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

View File

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