mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 10:24:26 +00:00
TASK 4.2: Document granular permissions system - COMPLETED - Add Section 7: Granular Permissions System to APERTURE_SPECS.md - Defines flexible permission system allowing granular permission assignment to ANY user - Only users with admin role can assign permissions - Permissions are independent of user roles (not inherited) Key Features: - User-based permissions (not role-based) - Admin-only permission assignment - Audit logging of permission changes - Reusable UI components for permission checking Permissions Categories Documented: 1. Dashboard & Stats (8 permissions) 2. Calendar & Bookings (6 permissions) 3. Staff Management (10 permissions) 4. Client Management (11 permissions) 5. POS & Sales (8 permissions) 6. Finance (6 permissions) 7. Marketing (9 permissions) 8. Configuration (4 permissions) Database Schema Added: - user_permissions table - Supports user_id, permission_key, granted, granted_by, granted_at - Unique constraint on (user_id, permission_key) - Check constraint to verify user exists in auth.users API Endpoints: - GET /api/aperture/permissions/check - Check single permission - GET /api/aperture/permissions/user - Get user permissions - POST /api/aperture/permissions/assign - Assign permissions (admin only) - GET /api/aperture/permissions/list - Get all available permissions Helper Functions Documented: - hasPermission(user_id, permission_key) - Check single permission - hasPermissions(user_id, permission_keys) - Check multiple permissions - isAdmin(user_id) - Check if user is admin role UI Components Documented: - PermissionChecker - Single permission check with fallback - MultiPermissionChecker - Multiple permissions check (all/any mode) - Usage examples for Staff, POS, Dashboard pages Security Considerations: - Row Level Security (RLS) for all sensitive tables - Only admin can assign permissions - All financial actions must be audited - Validation before allowing actions Files Modified: - docs/APERTURE_SPECS.md Next: Task 4 - Update APERTURE_SQUARE_UI.md with Radix UI
1012 lines
28 KiB
Markdown
1012 lines
28 KiB
Markdown
# 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<string, boolean> // { '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<boolean> {
|
|
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<Record<string, boolean>> {
|
|
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<string, boolean> = {}
|
|
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<boolean> {
|
|
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 <div>Loading...</div>
|
|
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<Record<string, boolean>>({})
|
|
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 <div>Loading...</div>
|
|
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 (
|
|
<PermissionChecker
|
|
permission_key="staff.delete"
|
|
user_id={user.id}
|
|
fallback={
|
|
<div className="text-red-500">
|
|
No tienes permiso para eliminar staff
|
|
</div>
|
|
}
|
|
>
|
|
<Button onClick={() => deleteStaff(staffId)}>
|
|
Eliminar Staff
|
|
</Button>
|
|
</PermissionChecker>
|
|
)
|
|
}
|
|
```
|
|
|
|
#### Verificar múltiples permisos
|
|
```typescript
|
|
export default function POSPage() {
|
|
const { user } = useAuth()
|
|
|
|
return (
|
|
<MultiPermissionChecker
|
|
user_id={user.id}
|
|
permission_keys={['pos.access', 'pos.create_sale']}
|
|
mode="all"
|
|
fallback={
|
|
<div className="text-center text-gray-500">
|
|
No tienes acceso al POS
|
|
</div>
|
|
}
|
|
>
|
|
<Button onClick={() => openPOSScreen()}>
|
|
Abrir POS
|
|
</Button>
|
|
</MultiPermissionChecker>
|
|
)
|
|
}
|
|
```
|
|
|
|
#### Verificar permiso condicional
|
|
```typescript
|
|
export default function DashboardPage() {
|
|
const { user } = useAuth()
|
|
|
|
return (
|
|
<div>
|
|
{/* Componente que requiere permiso */}
|
|
<PermissionChecker permission_key="dashboard.view" user_id={user.id}>
|
|
<StatsCard />
|
|
</PermissionChecker>
|
|
|
|
{/* Otro componente que requiere permiso */}
|
|
<MultiPermissionChecker
|
|
user_id={user.id}
|
|
permission_keys={['pos.access', 'pos.create_sale']}
|
|
mode="any"
|
|
fallback={
|
|
<div className="text-center text-gray-500">
|
|
El POS no está disponible en este momento
|
|
</div>
|
|
}
|
|
>
|
|
<Button>Ver POS</Button>
|
|
</MultiPermissionChecker>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|