mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 13:24:27 +00:00
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:
50
README.md
50
README.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
74
TASKS.md
74
TASKS.md
@@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 } }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 } }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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('')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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> &
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
152
docs/CLIENT_ONBOARDING.md
Normal 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*
|
||||||
174
docs/OPERATIONAL_PROCEDURES.md
Normal file
174
docs/OPERATIONAL_PROCEDURES.md
Normal 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
147
docs/STAFF_TRAINING.md
Normal 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
173
docs/TROUBLESHOOTING.md
Normal 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.
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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!,
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user