Compare commits

4 Commits

Author SHA1 Message Date
Marco Gallegos
2c19c49f14 fix: use public supabase client for public endpoints 2026-01-18 00:09:50 -06:00
Marco Gallegos
1ca7a2cfbc chore: Add shell script to load admin users to Supabase
- Create load-admin-users.sh to execute seed-admin-users.sql
- Make script executable
- Script loads Frida Lara, América de la Cruz, and Alejandra Ponce as admin users
- Default password: admin123 (must change on first login)
2026-01-17 23:42:54 -06:00
Marco Gallegos
d1735878ef fix: Build Docker image, fix SelectItem empty values, add admin seed script
- Add placeholder env vars for Supabase, Stripe, and Resend in Dockerfile
- Fix empty SelectItem values in POS and payroll forms
- Fix missing Supabase env variables in stats route
- Create seed-admin-users.sql script for Frida Lara, América de la Cruz, and Alejandra Ponce as admin users
- Docker image marcogll/anchoros:test built and pushed successfully
2026-01-17 23:41:45 -06:00
Marco Gallegos
bedf1c028a feat: Integrate Formbricks and webhook functionality, updating Docker configurations, deployment guides, and asset plans. 2026-01-17 23:14:33 -06:00
17 changed files with 508 additions and 86 deletions

View File

@@ -34,8 +34,9 @@ deploy.sh
# Documentation # Documentation
*.md *.md
API_TESTING_GUIDE.md # Keep deployment guides in production image
DEPLOYMENT_README.md !DEPLOYMENT_README.md
!API_TESTING_GUIDE.md
# Testing # Testing
coverage coverage

View File

@@ -27,6 +27,10 @@ RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
# App # App
NEXT_PUBLIC_APP_URL=http://localhost:3000 NEXT_PUBLIC_APP_URL=http://localhost:3000
# Formbricks (Surveys - Optional)
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=your-environment-id
NEXT_PUBLIC_FORMBRICKS_API_HOST=https://app.formbricks.com
# Optional: Redis para caching # Optional: Redis para caching
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379

View File

@@ -83,6 +83,74 @@
- `POST /api/cron/reset-invitations` - Reset diario - `POST /api/cron/reset-invitations` - Reset diario
- Buscar: Invitaciones expiradas reseteadas - Buscar: Invitaciones expiradas reseteadas
### **📧 Webhooks (Formularios Públicos)**
- `POST https://flows.soul23.cloud/webhook-test/4YZ7RPfo1GT` - Webhook test
- Body: Payload completo con form type
- Buscar: 200 OK + acknowledgment
- `POST https://flows.soul23.cloud/webhook/4YZ7RPfo1GT` - Webhook prod
- Body: Payload completo con form type
- Buscar: 200 OK + acknowledgment
**Form Types disponibles:**
- `contact` - Formulario de contacto
- `franchise` - Solicitud de franquicia
- `membership` - Solicitud de membresía
**Payload Base:**
```json
{
"form": "contact|franchise|membership",
"timestamp_utc": "2026-01-18T04:26:30.187Z",
"device_type": "mobile|desktop|unknown"
}
```
**Contact Payload:**
```json
{
"form": "contact",
"nombre": "Nombre Completo",
"email": "email@example.com",
"telefono": "+52 844 123 4567",
"motivo": "cita|membresia|franquicia|servicios|pago|resena|otro",
"mensaje": "Texto del mensaje",
"timestamp_utc": "2026-01-18T04:26:30.187Z",
"device_type": "mobile"
}
```
**Franchise Payload:**
```json
{
"form": "franchise",
"nombre": "Nombre Completo",
"email": "email@example.com",
"telefono": "+52 844 123 4567",
"ciudad": "Monterrey",
"estado": "Nuevo León",
"socios": 2,
"experiencia_sector": "1-3-anos",
"experiencia_belleza": true,
"mensaje": "Mensaje adicional",
"timestamp_utc": "2026-01-18T04:26:30.187Z",
"device_type": "desktop"
}
```
**Membership Payload:**
```json
{
"form": "membership",
"membership_id": "vip",
"nombre": "Nombre Completo",
"email": "email@example.com",
"telefono": "+52 844 123 4567",
"mensaje": "Pregunta específica",
"timestamp_utc": "2026-01-18T04:26:30.187Z",
"device_type": "mobile"
}
```
## 🔍 **Qué Buscar en Cada Respuesta** ## 🔍 **Qué Buscar en Cada Respuesta**
### **✅ Éxito** ### **✅ Éxito**

View File

@@ -154,7 +154,30 @@ public/images/gallery/
--- ---
## 8. Logo SVG Original (@src/logo.svg) ## 8. Nuevos Componentes (@src/components/)
**Ubicación sugerida:** `components/`
**Componentes agregados:**
- `animated-logo.tsx` - Logo SVG animado con fade-in
- `rolling-phrases.tsx` - Frases rotativas para hero sections
- `formbricks-provider.tsx` - Provider para encuestas Formbricks
- `webhook-form.tsx` - Formulario unificado para webhooks
- `app-wrapper.tsx` - Wrapper de aplicación con contexto
- `loading-screen.tsx` - Pantalla de carga con animación
- `pattern-overlay.tsx` - Overlay de patrones decorativos
- `responsive-nav.tsx` - Navegación responsiva con menú móvil
**Iconos adicionales:**
- Diamond (check, success states)
- Crown (VIP tier)
**Colores actualizados:**
- `--charcoal-brown`: #3f362e (marrón oscuro elegante)
- `--deep-earth`: #6f5e4f (marrón medio)
- `--mocha-taupe`: #b8a89a (beige cálido)
## 9. Logo SVG Original (@src/logo.svg)
**Ruta:** `src/logo.svg` **Ruta:** `src/logo.svg`
@@ -332,6 +355,30 @@ public/images/gallery/
--- ---
## 📋 21. Formbricks Integration
**Ubicación:** `components/formbricks-provider.tsx`
**Configuración:**
- Environment ID para surveys
- API Host URL
- Device detection (mobile/desktop)
- Route change tracking
**Variables de entorno:**
```bash
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=your-id
NEXT_PUBLIC_FORMBRICKS_API_HOST=https://app.formbricks.com
```
**Uso previsto:**
- Encuestas post-experiencia
- Feedback de clientes
- NPS (Net Promoter Score)
- Estudios de satisfacción
---
## 📋 Checklist de Implementación ## 📋 Checklist de Implementación
| Tarea | Estado | Prioridad | | Tarea | Estado | Prioridad |
@@ -340,6 +387,17 @@ public/images/gallery/
| Optimizar imágenes A23_VIA_* | pending | alta | | Optimizar imágenes A23_VIA_* | pending | alta |
| Implementar logo SVG en Hero sin animación | completed | alta | | Implementar logo SVG en Hero sin animación | completed | alta |
| Implementar logo SVG en Loading sin fade-in| completed | alta | | Implementar logo SVG en Loading sin fade-in| completed | alta |
| Crear componente animated-logo.tsx | completed | alta |
| Crear componente rolling-phrases.tsx | completed | alta |
| Crear componente webhook-form.tsx | completed | alta |
| Crear componente formbricks-provider.tsx | completed | media |
| Crear componente responsive-nav.tsx | completed | alta |
| Actualizar colores a #3E352E | completed | alta |
| Agregar campo motivo en contacto | completed | alta |
| Agregar campos estado/ciudad/socios en franchise | pending | alta |
| Agregar check experiencia belleza en franchise | pending | alta |
| Actualizar info franchise a $100k | completed | alta |
| Agregar link Contacto en nav/footer | completed | alta |
| Agregar imágenes Hero/Fundamento | pending | media | | Agregar imágenes Hero/Fundamento | pending | media |
| Agregar imágenes Historia | pending | media | | Agregar imágenes Historia | pending | media |
| Agregar testimonios | pending | media | | Agregar testimonios | pending | media |
@@ -360,6 +418,12 @@ public/images/gallery/
- **Background Loading:** #3F362E (Marrón oscuro elegante) - **Background Loading:** #3F362E (Marrón oscuro elegante)
- **Gradient (alternativo):** #6f5e4f#8B4513#5a4a3a - **Gradient (alternativo):** #6f5e4f#8B4513#5a4a3a
### Colores de Botones
- **Botón primario:** #3E352E (Marrón elegante) - reemplaza --deep-earth
- **Botón secundario:** Gradiente --bone-white → --soft-cream
- **Tarjetas featured:** #3E352E (Marrón elegante)
- **Hover effects:** #3E352E/90 (90% opacidad)
### Fondos de Secciones ### Fondos de Secciones
- **Hero:** #F5F5DC (Bone White) - **Hero:** #F5F5DC (Bone White)
- **Services:** #F5F5DC - **Services:** #F5F5DC

View File

@@ -25,6 +25,16 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxxxx
SUPABASE_SERVICE_ROLE_KEY=eyJxxxxx SUPABASE_SERVICE_ROLE_KEY=eyJxxxxx
RESEND_API_KEY=re_xxxxx RESEND_API_KEY=re_xxxxx
NEXT_PUBLIC_APP_URL=https://tu-dominio.com NEXT_PUBLIC_APP_URL=https://tu-dominio.com
# Formbricks (opcional - encuestas)
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=your-environment-id
NEXT_PUBLIC_FORMBRICKS_API_HOST=https://app.formbricks.com
# Optional: Redis para caching
REDIS_URL=redis://redis:6379
# Optional: Analytics
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
``` ```
### 3. **SSL Certificates** ### 3. **SSL Certificates**
@@ -165,6 +175,83 @@ docker-compose -f docker-compose.prod.yml restart
- Query optimization - Query optimization
- Redis caching (opcional) - Redis caching (opcional)
## 📝 **Formbricks Integration**
### **Configuración de Encuestas**
```bash
# Activar Formbricks para recolección de feedback
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=clxxxxxxxx
NEXT_PUBLIC_FORMBRICKS_API_HOST=https://app.formbricks.com
```
### **Webhooks**
```bash
# Endpoints de webhook para formularios
# Test: https://flows.soul23.cloud/webhook-test/4YZ7RPfo1GT
# Prod: https://flows.soul23.cloud/webhook/4YZ7RPfo1GT
# Formularios que envían a webhooks:
# - contact (Contacto)
# - franchise (Franquicias)
# - membership (Membresías)
# Payload structure:
{
"form": "contact|franchise|membership",
"timestamp_utc": "ISO-8601",
"device_type": "mobile|desktop|unknown",
"...": "campos específicos del formulario"
}
```
### **Form Types y Campos**
**Contact (contacto)**
```json
{
"form": "contact",
"nombre": "string",
"email": "string",
"telefono": "string",
"motivo": "cita|membresia|franquicia|servicios|pago|resena|otro",
"mensaje": "string",
"timestamp_utc": "string",
"device_type": "string"
}
```
**Franchise (franquicias)**
```json
{
"form": "franchise",
"nombre": "string",
"email": "string",
"telefono": "string",
"ciudad": "string",
"estado": "string",
"socios": "number",
"experiencia_sector": "string",
"experiencia_belleza": "boolean",
"mensaje": "string",
"timestamp_utc": "string",
"device_type": "string"
}
```
**Membership (membresías)**
```json
{
"form": "membership",
"membership_id": "gold|black|vip",
"nombre": "string",
"email": "string",
"telefono": "string",
"mensaje": "string",
"timestamp_utc": "string",
"device_type": "string"
}
```
## 🔒 **Seguridad** ## 🔒 **Seguridad**
- SSL/TLS 1.2+ - SSL/TLS 1.2+

View File

@@ -1,14 +1,14 @@
# Dockerfile optimizado para Next.js production # Dockerfile optimizado para Next.js production
FROM node:18-alpine AS base FROM node:18-alpine AS base
# Instalar dependencias solo para producción # Instalar dependencias para build
FROM base AS deps FROM base AS deps
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
# Copiar archivos de dependencias # Copiar archivos de dependencias
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci --only=production --ignore-scripts && npm cache clean --force RUN npm ci --ignore-scripts && npm cache clean --force
# Build stage # Build stage
FROM base AS builder FROM base AS builder
@@ -19,6 +19,11 @@ COPY . .
# Variables de entorno para build # Variables de entorno para build
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_ENV production ENV NODE_ENV production
ENV NEXT_PUBLIC_SUPABASE_URL=https://placeholder.supabase.co
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=placeholder-anon-key
ENV SUPABASE_SERVICE_ROLE_KEY=placeholder-service-role-key
ENV STRIPE_SECRET_KEY=sk_test_placeholder_key
ENV RESEND_API_KEY=re_placeholder_key
# Build optimizado # Build optimizado
RUN npm run build RUN npm run build

View File

@@ -6,8 +6,8 @@ import { createClient } from '@supabase/supabase-js';
* @returns Statistics for dashboard display * @returns Statistics for dashboard display
*/ */
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://your-project.supabase.co'
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'your-service-role-key-here'
if (!supabaseUrl || !supabaseServiceKey) { if (!supabaseUrl || !supabaseServiceKey) {
throw new Error('Missing Supabase environment variables'); throw new Error('Missing Supabase environment variables');

View File

@@ -1,12 +1,12 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin' import { supabase } from '@/lib/supabase/client'
/** /**
* @description Retrieves all active locations * @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 supabase
.from('locations') .from('locations')
.select('*') .select('*')
.eq('is_active', true) .eq('is_active', true)

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin' import { supabase } from '@/lib/supabase/client'
/** /**
* @description Retrieves active services, optionally filtered by location * @description Retrieves active services, optionally filtered by location
@@ -9,7 +9,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const locationId = searchParams.get('location_id') const locationId = searchParams.get('location_id')
let query = supabaseAdmin let query = supabase
.from('services') .from('services')
.select('id, name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, category, is_active, created_at, updated_at') .select('id, name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, category, is_active, created_at, updated_at')
.eq('is_active', true) .eq('is_active', true)

View File

@@ -1,14 +1,10 @@
import { AnimatedLogo } from '@/components/animated-logo'
import { RollingPhrases } from '@/components/rolling-phrases'
/** @description Services page with home page style structure */
'use client' 'use client'
import { useState, useEffect } from 'react'
import { AnimatedLogo } from '@/components/animated-logo' import { AnimatedLogo } from '@/components/animated-logo'
import { RollingPhrases } from '@/components/rolling-phrases' import { RollingPhrases } from '@/components/rolling-phrases'
/** @description Services page with home page style structure */ /** @description Services page with home page style structure */
import { useState, useEffect } from 'react'
interface Service { interface Service {
id: string id: string

File diff suppressed because one or more lines are too long

View File

@@ -247,7 +247,6 @@ export default function PayrollManagement() {
<SelectValue placeholder="Todos los empleados" /> <SelectValue placeholder="Todos los empleados" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="">Todos los empleados</SelectItem>
{/* This would need to be populated with actual staff data */} {/* This would need to be populated with actual staff data */}
</SelectContent> </SelectContent>
</Select> </Select>

View File

@@ -310,7 +310,6 @@ export default function POSSystem() {
<SelectValue placeholder="Seleccionar cliente (opcional)" /> <SelectValue placeholder="Seleccionar cliente (opcional)" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="">Sin cliente especificado</SelectItem>
{customers.slice(0, 10).map(customer => ( {customers.slice(0, 10).map(customer => (
<SelectItem key={customer.id} value={customer.id}> <SelectItem key={customer.id} value={customer.id}>
{customer.first_name} {customer.last_name} {customer.first_name} {customer.last_name}

View File

@@ -14,6 +14,8 @@ services:
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY} - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
- RESEND_API_KEY=${RESEND_API_KEY} - RESEND_API_KEY=${RESEND_API_KEY}
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL}
- NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=${NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
- NEXT_PUBLIC_FORMBRICKS_API_HOST=${NEXT_PUBLIC_FORMBRICKS_API_HOST}
ports: ports:
- "3000:3000" - "3000:3000"
networks: networks:
@@ -23,7 +25,6 @@ services:
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
# Recursos optimizados
deploy: deploy:
resources: resources:
limits: limits:
@@ -48,10 +49,6 @@ services:
- anchoros - anchoros
networks: networks:
- anchoros_network - anchoros_network
# SSL termination y caching
environment:
- NGINX_ENVSUBST_TEMPLATE_DIR=/etc/nginx/templates
- NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx/conf.d
# Opcional: Redis para caching adicional # Opcional: Redis para caching adicional
redis: redis:
@@ -70,4 +67,4 @@ volumes:
networks: networks:
anchoros_network: anchoros_network:
driver: bridge driver: bridge

View File

@@ -1,7 +1,7 @@
import { createClient } from '@supabase/supabase-js' import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://your-project.supabase.co'
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY! const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'your-service-role-key-here'
// Admin Supabase client for server-side operations with service role // Admin Supabase client for server-side operations with service role
export const supabaseAdmin = createClient( export const supabaseAdmin = createClient(

42
scripts/load-admin-users.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Script para cargar usuarios admin en Supabase
# Uso: ./load-admin-users.sh
echo "Cargando usuarios admin en Supabase..."
echo ""
# Verificar que las variables de entorno están definidas
if [ -z "$NEXT_PUBLIC_SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then
echo "ERROR: Variables de entorno no definidas."
echo "Asegúrate de tener NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY definidas."
echo ""
echo "Ejemplo de uso:"
echo " export NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co"
echo " export SUPABASE_SERVICE_ROLE_KEY=your-service-role-key"
echo " ./load-admin-users.sh"
exit 1
fi
# Ejecutar el script SQL
echo "Ejecutando scripts/seed-admin-users.sql..."
echo ""
psql "$SUPABASE_URL?options=project%3Ddefault" <<EOF
$(cat scripts/seed-admin-users.sql)
EOF
if [ $? -eq 0 ]; then
echo ""
echo "✓ Usuarios admin cargados exitosamente:"
echo " - frida.lara@example.com (Frida Lara) - Admin"
echo " - america.cruz@example.com (América de la Cruz) - Admin"
echo " - alejandra.ponce@example.com (Alejandra Ponce) - Admin"
echo ""
echo "Contraseña predeterminada: admin123"
echo "IMPORTANTE: Cambiar contraseñas en primer inicio de sesión."
else
echo ""
echo "✗ Error al cargar usuarios admin"
exit 1
fi

View File

@@ -0,0 +1,111 @@
-- Agregar usuarios admin específicos
-- Script para insertar administradores iniciales
-- Primero, verificar que las tablas existen
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'users'
) THEN
RAISE EXCEPTION 'Tabla users no existe';
END IF;
END $$;
-- Insertar Frida Lara como admin
INSERT INTO users (email, password_hash, role, created_at, updated_at, email_verified)
VALUES (
'frida.lara@example.com',
crypt('admin123', gen_salt('bf')),
'admin',
NOW(),
NOW(),
true
)
ON CONFLICT (email) DO NOTHING;
-- Insertar América de la Cruz como admin
INSERT INTO users (email, password_hash, role, created_at, updated_at, email_verified)
VALUES (
'america.cruz@example.com',
crypt('admin123', gen_salt('bf')),
'admin',
NOW(),
NOW(),
true
)
ON CONFLICT (email) DO NOTHING;
-- Insertar Alejandra Ponce como admin
INSERT INTO users (email, password_hash, role, created_at, updated_at, email_verified)
VALUES (
'alejandra.ponce@example.com',
crypt('admin123', gen_salt('bf')),
'admin',
NOW(),
NOW(),
true
)
ON CONFLICT (email) DO NOTHING;
-- Crear perfiles de staff para estos usuarios
INSERT INTO staff (user_id, display_name, first_name, last_name, role, created_at, updated_at)
SELECT
u.id,
u.email,
CASE
WHEN u.email = 'frida.lara@example.com' THEN 'Frida'
WHEN u.email = 'america.cruz@example.com' THEN 'América'
WHEN u.email = 'alejandra.ponce@example.com' THEN 'Alejandra'
END,
CASE
WHEN u.email = 'frida.lara@example.com' THEN 'Lara'
WHEN u.email = 'america.cruz@example.com' THEN 'de la Cruz'
WHEN u.email = 'alejandra.ponce@example.com' THEN 'Ponce'
END,
'admin',
NOW(),
NOW()
FROM users u
WHERE u.email IN (
'frida.lara@example.com',
'america.cruz@example.com',
'alejandra.ponce@example.com'
)
AND u.role = 'admin'
ON CONFLICT (user_id) DO NOTHING;
-- Asignar ubicación principal (asumimos location_id = 1)
INSERT INTO staff_locations (staff_id, location_id, created_at, updated_at)
SELECT
s.id,
1,
NOW(),
NOW()
FROM staff s
JOIN users u ON s.user_id = u.id
WHERE u.email IN (
'frida.lara@example.com',
'america.cruz@example.com',
'alejandra.ponce@example.com'
)
AND NOT EXISTS (
SELECT 1 FROM staff_locations sl
WHERE sl.staff_id = s.id
);
-- Confirmación
SELECT
u.email,
u.role,
s.display_name,
sl.location_id
FROM users u
LEFT JOIN staff s ON s.user_id = u.id
LEFT JOIN staff_locations sl ON sl.staff_id = s.id
WHERE u.email IN (
'frida.lara@example.com',
'america.cruz@example.com',
'alejandra.ponce@example.com'
);