# Aperture Technical Specifications **Especificaciones técnicas completas para Aperture (HQ Dashboard)** **Última actualización: Enero 2026** --- ## 1. Objetivo Este documento define las especificaciones técnicas para el desarrollo de Aperture (aperture.anchor23.mx), el dashboard administrativo y CRM interno de AnchorOS. --- ## 2. Stack Tecnológico ### Frontend - **Framework**: Next.js 14 (App Router) - **UI Library**: Radix UI (componentes accesibles preconstruidos) - **Estilizado**: Tailwind CSS + Square UI custom styling - **Icons**: Lucide React (24px, stroke 2px) - **Charts**: Recharts o similar (para gráficos de rendimiento) - **PDF Generation**: PDFKit o similar (para cierre de caja) ### Backend - **Database**: Supabase (PostgreSQL + RLS) - **Auth**: Supabase Auth (magic links para clientes, password para staff/admin) - **API**: Next.js App Router API routes ### Integraciones - **Payments**: Stripe SDK - **Calendar**: Google Calendar API v3 (Service Account) - **Notifications**: WhatsApp API (Twilio / Meta) - Good to have, no priority --- ## 3. Horas Trabajadas - Respuesta a Pregunta 9 ### Calculo de Horas Trabajadas **Enfoque**: El sistema de nómina calcula el **tiempo efectivo de trabajo** basado en la comparación entre **tiempo programado** y **tiempo real utilizado**. #### Lógica de Cálculo 1. **Tiempo Programado**: - Basado en la duración de los servicios agendados - Calculado desde `bookings.start_time_utc` hasta `bookings.end_time_utc` - Excluye tiempos de espera entre citas 2. **Tiempo Real Utilizado**: - Actualizado manualmente por el staff después de completar una cita - Se almacena en tabla de control (opcional: `staff_time_tracking`) - Permite ajustes por diferencias en duración real #### Campos de Base de Datos **Tabla `bookings` (ya existe):** ```sql - id (UUID) - staff_id (UUID) - service_id (UUID) - start_time_utc (TIMESTAMPTZ) - end_time_utc (TIMESTAMPTZ) - scheduled_duration_minutes (INTEGER) - Calculado automáticamente - actual_duration_minutes (INTEGER) - Actualizado manualmente por staff - time_difference_minutes (INTEGER) - Diferencia calculada - status (TEXT) - 'confirmed', 'pending', 'in_progress', 'completed', 'no_show' ``` **Nueva tabla sugerida: `staff_time_tracking`** ```sql CREATE TABLE staff_time_tracking ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), booking_id UUID REFERENCES bookings(id) ON DELETE CASCADE, staff_id UUID REFERENCES staff(id) ON DELETE CASCADE, scheduled_duration_minutes INTEGER NOT NULL, actual_duration_minutes INTEGER NOT NULL, time_difference_minutes INTEGER NOT NULL, notes TEXT, created_by UUID REFERENCES staff(id) ON DELETE SET NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_staff_time_tracking_staff_date ON staff_time_tracking(staff_id, created_at); ``` #### Algoritmo de Cálculo ```sql -- Duración programada (automática) UPDATE bookings SET scheduled_duration_minutes = EXTRACT(EPOCH FROM (end_time_utc - start_time_utc)) / 60 WHERE scheduled_duration_minutes IS NULL; -- Diferencia de tiempo (automática) UPDATE bookings SET time_difference_minutes = actual_duration_minutes - scheduled_duration_minutes WHERE status = 'completed' AND actual_duration_minutes IS NOT NULL; ``` #### Cálculo de Nómina **Horas Totales por Periodo:** ```sql SELECT s.id, s.display_name, SUM(b.scheduled_duration_minutes) / 60 AS scheduled_hours, SUM(b.actual_duration_minutes) / 60 AS actual_hours, COUNT(b.id) AS total_bookings, SUM(CASE WHEN b.time_difference_minutes > 0 THEN b.time_difference_minutes ELSE 0 END) / 60 AS extra_hours FROM staff s LEFT JOIN bookings b ON b.staff_id = s.id WHERE b.status = 'completed' AND b.start_time_utc >= $1::TIMESTAMPTZ AND b.start_time_utc < $2::TIMESTAMPTZ GROUP BY s.id, s.display_name; ``` #### Reglas de Negocio 1. **Tiempo programado**: Base para el cálculo de nómina 2. **Tiempo real**: Ajustes permitidos por staff (por ex: cliente llegó tarde, servicio se extendió) 3. **Tiempo extra**: Se paga al 100% si fue trabajo adicional 4. **Tiempo faltante**: Se descuenta del pago (horarios no cubiertos por citas) 5. **Tiempo no-productivo**: No se paga (esperas, preparación post-cita) --- ## 4. Estructura del Sistema de POS (Punto de Venta) ### 4.1 Arquitectura del POS #### Componentes Principales **1. Service Selector** - Grid de categorías: Servicios, Productos de venta, Membresías, Giftcards - Búsqueda fonética de productos/servicios - Filtros por tipo y categoría **2. Customer Selection** - Buscador de clientes (email/teléfono) - Selección de cliente existente o registro de nuevo - Display de tier y saldo de créditos/membresía **3. Payment Processor** - Métodos de pago disponibles: - Efectivo - Transferencia - Membership (créditos de membresía) - Tarjeta (Stripe terminal) - Giftcard (código canjeable) - PIA (Paid in Advance - depósito ya pagado) - Cálculo automático de cambio **4. Receipt Options** - NO imprimir recibos físicos - Enviar por email (SendGrid, AWS SES, o similar) - Guardar en dashboard del cliente **5. Transaction History** - Historial de transacciones del día - Filtros por método de pago y cajero ### 4.2 Opciones de Pago #### 1. Efectivo - Registro manual del monto recibido - No requiere integración externa #### 2. Transferencia - Referencia bancaria del cliente - Comprobante de transferencia - Estado: "pendiente" hasta confirmación #### 3. Membership (Créditos de Membresía) - Verificar saldo disponible - Deducir créditos automáticamente - Restringir a clientes con membresía activa #### 4. Tarjeta (Stripe Terminal) - Integración con Stripe SDK para terminales físicas - Procesamiento de pago en tiempo real - Confirmación de transacción #### 5. Giftcard - Validar código de giftcard - Verificar saldo y estado (activo/inactivo/expirado) - Deducir saldo del giftcard #### 6. PIA (Paid in Advance) - Verificar depósito previamente pagado - Aplicar al saldo total de la transacción - No requiere pago adicional ### 4.3 Campos de Base de Datos **Nueva tabla: `pos_sales`** ```sql CREATE TABLE pos_sales ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), location_id UUID REFERENCES locations(id) ON DELETE CASCADE, staff_id UUID REFERENCES staff(id) ON DELETE CASCADE, customer_id UUID REFERENCES customers(id) ON DELETE SET NULL, -- Payment details payment_method TEXT NOT NULL CHECK (payment_method IN ('cash', 'transfer', 'membership', 'card', 'giftcard', 'pia')), payment_amount DECIMAL(10, 2) NOT NULL, payment_reference TEXT, payment_status TEXT NOT NULL DEFAULT 'completed' CHECK (payment_status IN ('pending', 'completed', 'failed')), -- Transaction details total_amount DECIMAL(10, 2) NOT NULL, discount_amount DECIMAL(10, 2) DEFAULT 0, tax_amount DECIMAL(10, 2) DEFAULT 0, tip_amount DECIMAL(10, 2) DEFAULT 0, -- Items sold items JSONB NOT NULL, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_pos_sales_location_date ON pos_sales(location_id, created_at); CREATE INDEX idx_pos_sales_staff_date ON pos_sales(staff_id, created_at); ``` **Formato de `items` JSONB:** ```json { "services": [ { "service_id": "uuid", "service_name": "Manicure", "quantity": 1, "unit_price": 150.00, "total": 150.00 } ], "products": [ { "product_id": "uuid", "product_name": "Cuticle Remover", "quantity": 2, "unit_price": 45.00, "total": 90.00 } ], "memberships": [ { "membership_id": "uuid", "membership_name": "VIP Monthly", "quantity": 1, "unit_price": 500.00, "total": 500.00 } ] } ``` **Nueva tabla: `giftcards`** ```sql CREATE TABLE giftcards ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), code TEXT UNIQUE NOT NULL, initial_balance DECIMAL(10, 2) NOT NULL, current_balance DECIMAL(10, 2) NOT NULL, purchased_by UUID REFERENCES customers(id) ON DELETE SET NULL, purchased_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_giftcards_code ON giftcards(code); ``` ### 4.4 API Endpoints **POS Sales:** ```typescript POST /api/aperture/pos/sales Body: { customer_id: UUID | null, items: { services: Array<{ service_id, quantity }>, products: Array<{ product_id, quantity }>, memberships: Array<{ membership_id, quantity }> }, payment_method: 'cash' | 'transfer' | 'membership' | 'card' | 'giftcard' | 'pia', payment_amount: number, tip_amount?: number, giftcard_code?: string } Response: { success, sale_id, items, total_amount, change } ``` **Daily Summary:** ```typescript GET /api/aperture/pos/daily-summary?date=YYYY-MM-DD&location_id=UUID Response: { success: true, summary: { total_sales, by_payment_method: { cash, transfer, membership, card, giftcard, pia }, transactions_count } } ``` --- ## 5. Sistema de Múltiples Cajeros ### 5.1 Arquitectura Cada cajero tiene su propia sesión y cierre de caja independiente. El sistema permite: - Múltiples cajeros trabajando simultáneamente - Control individual de transacciones por cajero - Rastreo de errores en cobros por usuario específico - Cierre de caja individual por cajero ### 5.2 Campos de Base de Datos **Nueva tabla: `daily_cash_close`** ```sql CREATE TABLE daily_cash_close ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), location_id UUID REFERENCES locations(id) ON DELETE CASCADE, cashier_id UUID REFERENCES staff(id) ON DELETE CASCADE, -- Cash balance tracking opening_balance DECIMAL(10, 2) NOT NULL DEFAULT 0, cash_sales DECIMAL(10, 2) NOT NULL DEFAULT 0, cash_refunds DECIMAL(10, 2) NOT NULL DEFAULT 0, closing_balance DECIMAL(10, 2) NOT NULL DEFAULT 0, cash_difference DECIMAL(10, 2) NOT NULL DEFAULT 0, -- Transaction summary total_sales DECIMAL(10, 2) NOT NULL DEFAULT 0, card_sales DECIMAL(10, 2) NOT NULL DEFAULT 0, transfer_sales DECIMAL(10, 2) NOT NULL DEFAULT 0, membership_sales DECIMAL(10, 2) NOT NULL DEFAULT 0, giftcard_sales DECIMAL(10, 2) NOT NULL DEFAULT 0, pia_sales DECIMAL(10, 2) NOT NULL DEFAULT 0, -- Timestamps date DATE NOT NULL, open_at TIMESTAMPTZ NOT NULL, closed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), closed_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_daily_cash_close_location_date ON daily_cash_close(location_id, date); CREATE INDEX idx_daily_cash_close_cashier_date ON daily_cash_close(cashier_id, date); CREATE UNIQUE INDEX idx_daily_cash_close_unique ON daily_cash_close(location_id, cashier_id, date); ``` ### 5.3 Flujo de Cierre de Caja #### 1. Apertura de Caja - Cajero registra monto de efectivo inicial (`opening_balance`) - Sistema crea registro de apertura (`open_at` timestamp) #### 2. Registro de Ventas - Todas las transacciones se asocian al `cashier_id` activo - Sistema calcula totales por método de pago en tiempo real #### 3. Cierre de Caja - Cajero cierra el día: 1. Conta efectivo en caja 2. Ingresa `closing_balance` real 3. Sistema calcula `cash_difference` 4. Genera reporte PDF automático 5. Envía reporte al dueño por email #### 4. Rastreo de Errores - Si `cash_difference` ≠ 0: - Sistema marca discrepancia - Asocia la transacción específica al cajero - Permite investigación del error con el usuario correcto ### 5.4 API Endpoints **Open Cash Register:** ```typescript POST /api/aperture/pos/open-cash-register Body: { opening_balance: number } Response: { success, cash_register_id, open_at } ``` **Close Cash Register:** ```typescript POST /api/aperture/pos/close-cash-register Body: { closing_balance: number, notes?: string } Response: { success: true, summary: { total_sales, cash_difference, transactions_count }, pdf_report_url } ``` **Get Active Cash Registers:** ```typescript GET /api/aperture/pos/active-cash-registers?location_id=UUID Response: { success: true, registers: [ { id, cashier_id, cashier_name, opening_balance, current_balance, open_at, location_name } ] } ``` --- ## 6. Sistema de Finanzas ### 6.1 Campos de Base de Datos **Nueva tabla: `expenses`** ```sql CREATE TABLE expenses ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), location_id UUID REFERENCES locations(id) ON DELETE CASCADE, category TEXT NOT NULL, description TEXT, amount DECIMAL(10, 2) NOT NULL, expense_date DATE NOT NULL, -- Recurring expenses is_recurring BOOLEAN DEFAULT false, recurring_frequency TEXT CHECK (recurring_frequency IN ('daily', 'weekly', 'monthly', 'yearly')), recurring_end_date DATE, created_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_expenses_location_date ON expenses(location_id, expense_date); CREATE INDEX idx_expenses_category ON expenses(category); ``` ### 6.2 Categorías de Gastos - **Renta**: Alquiler del local - **Insumos**: Productos para servicios (cuticles, esmaltes, etc.) - **Servicios**: Servicios externos contratados - **Personal**: Pagos de nómina (si se maneja por cash) - **Marketing**: Publicidad y promociones - **Utilidades**: Electricidad, agua, internet - **Otros**: Cualquier otro gasto ### 6.3 API Endpoints **Create Expense:** ```typescript POST /api/aperture/finance/expenses Body: { category, description, amount, expense_date, is_recurring?: boolean, recurring_frequency?: 'daily' | 'weekly' | 'monthly' | 'yearly', recurring_end_date?: string } Response: { success, expense_id } ``` **Get Financial Report:** ```typescript GET /api/aperture/finance/report?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD&location_id=UUID Response: { success: true, report: { total_revenue, total_expenses, net_margin, expenses_by_category, profit_margin_percentage } } ``` --- ## 7. Convenciones de Código ### TypeScript - **Strict mode**: Habilitado - **Interfaces**: Definir tipos para todas las respuestas de API - **Enums**: Usar enums para constantes (status, roles, métodos de pago) ### Naming Conventions - **Componentes**: PascalCase (ej: `StatsCard`, `BookingCard`) - **Funciones**: camelCase (ej: `fetchBookings`, `calculatePayroll`) - **Variables**: camelCase (ej: `totalSales`, `staffId`) - **Constantes**: UPPER_SNAKE_CASE (ej: `API_URL`, `DEFAULT_TIMEOUT`) ### SQL - **Table names**: snake_case (ej: `daily_cash_close`, `pos_sales`) - **Column names**: snake_case (ej: `opening_balance`, `cash_difference`) - **Functions**: snake_case (ej: `calculate_weekly_invitations_reset`) --- ## 8. Consideraciones de Seguridad 1. **Row Level Security (RLS)**: - Todas las tablas sensibles deben tener políticas RLS - Solo roles apropiados pueden acceder a datos financieros - Audit logging completo de todas las acciones 2. **Validaciones**: - Validar todos los inputs de usuario - Verificar permisos antes de permitir acciones - Validar montos de pagos (rangos aceptables) 3. **Auditoría**: - Todas las acciones financieras deben registrarse en `audit_logs` - Incluir: acción, usuario, timestamp, detalles --- ## 9. Checklist de Implementación ### Fase 0: Documentación y Configuración ✅ - [x] Crear documento de especificaciones técnicas - [x] Documentar cálculo de horas trabajadas - [x] Definir estructura de POS completa - [x] Documentar sistema de múltiples cajeros ### Fase 1-7: Pendiente - [ ] Instalar Radix UI - [ ] Crear componentes base Square UI - [ ] Implementar Dashboard Home - [ ] Implementar Calendario Maestro - [ ] Implementar Staff & Nómina - [ ] Implementar Clientes & Fidelización - [ ] Implementar POS - [ ] Implementar Finanzas - [ ] Implementar Marketing & Configuración - [ ] Testing completo --- ## 10. Documentos Relacionados - [TASKS.md](../TASKS.md) - Plan de ejecución por fases - [APERTURE_SQUARE_UI.md](./APERTURE_SQUARE_UI.md) - Guía de estilo Square UI - [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) - Sistema de diseño completo - [API.md](./API.md) - Documentación de APIs y endpoints - [PRD.md](./PRD.md) - Documento maestro de producto --- ## 11. Notas Importantes 1. **Precios Inteligentes**: - Configurables por servicio - Aplican a ambos canales (booking + POS) - Solo activables en temporada alta (backend toggle) 2. **Sin Impresión de Recibos**: - Email a cliente - Dashboard del cliente - Reporte PDF al dueño (cierre de caja) 3. **Múltiples Cajeros**: - Cada cajero con su propio cierre de caja - Rastreo de errores por usuario específico - Control de movimientos para investigación 4. **Horas Trabajadas**: - Automático desde bookings (tiempo programado) - Actualización manual de tiempo real por staff - Cálculo de diferencias - Nómina basada en horas reales trabajadas ## 7. Sistema de Permisos Granulares ### 7.1 Objetivo Sistema de permisos flexible que permite asignar permisos de forma granular a **cualquier usuario**, independientemente de su rol. Solo usuarios con rol `admin` pueden asignar permisos. ### 7.2 Principios - **Flexibilidad**: Permisos independientes del rol - **Control Total**: Solo admins pueden asignar permisos - **Auditoría**: Todos los cambios de permisos se registran - **UI Components**: Componentes reutilizables para verificar permisos ### 7.3 Categorías de Permisos #### 1. Dashboard y Estadísticas - 'dashboard.view' - Ver dashboard principal - 'dashboard.view_kpi' - Ver KPI cards - 'dashboard.view_charts' - Ver gráficos de rendimiento - 'dashboard.reports_sales' - Ver reportes de ventas - 'dashboard.reports_payments' - Ver reportes de pagos - 'dashboard.reports_payroll' - Ver reportes de nómina - 'dashboard.activity_feed' - Ver feed de actividad reciente - 'dashboard.export_data' - Exportar datos (CSV, Excel, PDF) #### 2. Calendario y Citas - 'calendar.view' - Ver calendario maestro - 'calendar.create_booking' - Crear nuevas citas - 'calendar.edit_booking' - Editar citas existentes - 'calendar.cancel_booking' - Cancelar citas - 'calendar.reschedule_booking' - Reprogramar citas - 'calendar.assign_resource' - Asignar recursos a citas - 'calendar.view_availability' - Ver disponibilidad #### 3. Gestión de Staff - 'staff.view_list' - Ver lista de staff - 'staff.view_profile' - Ver perfil de staff - 'staff.create' - Crear nuevo staff - 'staff.edit' - Editar staff existente - 'staff.delete' - Eliminar staff (soft delete) - 'staff.assign_role' - Asignar rol a staff - 'staff.view_schedule' - Ver horarios de staff - 'staff.edit_schedule' - Editar horarios de staff - 'staff.view_commissions' - Ver comisiones - 'staff.edit_commissions' - Editar comisiones #### 4. Gestión de Clientes - 'clients.view_list' - Ver lista de clientes - 'clients.view_profile' - Ver perfil de cliente - 'clients.create' - Crear nuevo cliente - 'clients.edit' - Editar cliente existente - 'clients.delete' - Eliminar cliente (soft delete) - 'clients.view_history' - Ver histórico de citas - 'clients.view_notes' - Ver notas técnicas - 'clients.view_gallery' - Ver galería de fotos (VIP/Black/Gold) - 'clients.upload_photos' - Subir fotos a galería - 'clients.view_memberships' - Ver membresías del cliente - 'clients.assign_membership' - Asignar membresía - 'clients.view_points' - Ver puntos del cliente - 'clients.redeem_points' - Redimir puntos - 'clients.edit_credits' - Editar créditos de membresía #### 5. POS y Ventas - 'pos.access' - Acceder a POS - 'pos.create_sale' - Crear venta en POS - 'pos.view_history' - Ver historial de ventas - 'pos.open_register' - Abrir registro de caja - 'pos.close_register' - Cerrar registro de caja - 'pos.view_daily_sales' - Ver ventas del día - 'pos.view_all_closers' - Ver todos los cierres de caja - 'pos.manage_own' - Gestionar cierre de caja propio #### 6. Finanzas - 'finance.view_expenses' - Ver gastos - 'finance.create_expense' - Crear gasto - 'finance.edit_expense' - Editar gasto - 'finance.delete_expense' - Eliminar gasto - 'finance.view_reports' - Ver reportes financieros - 'finance.view_profit_margin' - Ver márgen de beneficio - 'finance.view_monthly_report' - Ver reporte mensual #### 7. Marketing - 'marketing.view_campaigns' - Ver campañas - 'marketing.create_campaign' - Crear campaña - 'marketing.edit_campaign' - Editar campaña - 'marketing.delete_campaign' - Eliminar campaña - 'marketing.send_campaign' - Enviar campaña - 'marketing.view_pricing' - Ver precios inteligentes - 'marketing.edit_pricing' - Editar precios inteligentes - 'marketing.view_integrations' - Ver integraciones - 'marketing.configure_integrations' - Configurar integraciones #### 8. Configuración - 'settings.view_general' - Ver configuración general - 'settings.edit_general' - Editar configuración general - 'settings.view_locations' - Ver ubicaciones - 'settings.edit_locations' - Editar ubicaciones - 'settings.create_location' - Crear ubicación ### 7.4 Estructura de Base de Datos **Tabla: user_permissions** ```sql CREATE TABLE user_permissions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, permission_key TEXT NOT NULL, granted BOOLEAN NOT NULL DEFAULT true, granted_by UUID REFERENCES staff(id) ON DELETE SET NULL, granted_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT user_permissions_unique UNIQUE (user_id, permission_key), CONSTRAINT user_permissions_user_check CHECK ( EXISTS (SELECT 1 FROM auth.users WHERE id = user_id) ) ); CREATE INDEX idx_user_permissions_user ON user_permissions(user_id); CREATE INDEX idx_user_permissions_key ON user_permissions(permission_key); ``` ### 7.5 API Endpoints #### Verificar Permiso Individual ```typescript POST /api/aperture/permissions/check Body: { permission_key: string } Response: { success: boolean, has_permission: boolean } ``` #### Obtener Permisos del Usuario ```typescript GET /api/aperture/permissions/user Response: { success: boolean, permissions: Record // { 'dashboard.view': true, 'pos.access': false } } ``` #### Asignar Permiso (Solo Admin) ```typescript POST /api/aperture/permissions/assign Body: { user_id: UUID, permissions: Array<{ permission_key: string, granted: boolean }> } Response: { success: boolean, message: 'Permissions updated successfully' } ``` #### Obtener Todos los Permisos Disponibles ```typescript GET /api/aperture/permissions/list Response: { success: boolean, permissions: Array<{ key: string, category: string, description: string }> } ``` ### 7.6 Funciones Helper #### hasPermission(user_id, permission_key) ```typescript export async function hasPermission( user_id: string, permission_key: string ): Promise { const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) const { data } = await supabase .from('user_permissions') .select('granted') .eq('user_id', user_id) .eq('permission_key', permission_key) .single() return data?.granted ?? false } ``` #### hasPermissions(user_id, permission_keys) ```typescript export async function hasPermissions( user_id: string, permission_keys: string[] ): Promise> { const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) const { data } = await supabase .from('user_permissions') .select('permission_key', 'granted') .eq('user_id', user_id) .in('permission_key', permission_keys) const result: Record = {} if (data) { for (const item of data) { result[item.permission_key] = item.granted } } return result } ``` #### isAdmin(user_id) ```typescript export async function isAdmin(user_id: string): Promise { const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) const { data: staff } = await supabase .from('staff') .select('role') .eq('user_id', user_id) .single() return staff?.role === 'admin' } ``` ### 7.7 UI Components #### PermissionChecker ```typescript import { useAuth } from '@/lib/auth/context' interface PermissionCheckerProps { permission_key: string; fallback?: React.ReactNode; children: React.ReactNode; } export function PermissionChecker({ permission_key, fallback = null, children }: PermissionCheckerProps) { const { user } = useAuth() const [hasPermission, setHasPermission] = useState(false) const [loading, setLoading] = useState(true) useEffect(() => { checkPermission() }, [user?.id, permission_key]) const checkPermission = async () => { if (!user?.id) { setHasPermission(false) setLoading(false) return } const response = await fetch('/api/aperture/permissions/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ permission_key }), }) const result = await response.json() setHasPermission(result.has_permission) setLoading(false) } if (loading) return
Loading...
if (!hasPermission && fallback) return fallback if (!hasPermission) return null return <>{children} } ``` #### MultiPermissionChecker ```typescript import { useAuth } from '@/lib/auth/context' interface MultiPermissionCheckerProps { permission_keys: string[]; mode?: 'all' | 'any'; // Require all or any permissions fallback?: React.ReactNode; children: React.ReactNode; } export function MultiPermissionChecker({ permission_keys, mode = 'all', fallback = null, children }: MultiPermissionCheckerProps) { const { user } = useAuth() const [permissions, setPermissions] = useState>({}) const [loading, setLoading] = useState(true) useEffect(() => { checkPermissions() }, [user?.id, permission_keys]) const checkPermissions = async () => { if (!user?.id) { setLoading(false) return } const response = await fetch('/api/aperture/permissions/user', { method: 'GET', }) const result = await response.json() setPermissions(result.permissions) setLoading(false) } const hasAccess = mode === 'all' ? Object.values(permissions).every(Boolean) : Object.values(permissions).some(Boolean) if (loading) return
Loading...
if (!hasAccess && fallback) return fallback if (!hasAccess) return null return <>{children} } ``` ### 7.8 Ejemplos de Uso #### Verificar un permiso ```typescript export default function StaffManagement() { const { user } = useAuth() return ( No tienes permiso para eliminar staff } > ) } ``` #### Verificar múltiples permisos ```typescript export default function POSPage() { const { user } = useAuth() return ( No tienes acceso al POS } > ) } ``` #### Verificar permiso condicional ```typescript export default function DashboardPage() { const { user } = useAuth() return (
{/* Componente que requiere permiso */} {/* Otro componente que requiere permiso */} El POS no está disponible en este momento
} > ) } ```