Ir al contenido

Revisar Solicitud

Acepta o rechaza una solicitud de acceso enviada por un asesor.

  1. Ve a Configuración → Asesores → Recibidas.
  2. Localiza la solicitud con estado “Pendiente”.
  3. Clic en “Revisar” — navega a página de revisión detallada.
  4. Revisa información del asesor:
    • Nombre del asesor
    • Email (si disponible)
    • Permisos solicitados
  5. Ajusta los permisos si es necesario.
  6. Clic en “Aceptar Solicitud” o “Rechazar”.
CampoDescripción
Nombre del asesorNombre proporcionado en la solicitud
NegocioNombre del negocio al que solicita acceso
Fecha de solicitudCuándo se envió la solicitud
Permisos solicitadosLista de secciones y nivel de acceso pedido
ExpiraDías restantes antes de expiración (7 días)
  1. Revisa/ajusta los permisos.
  2. Clic en “Aceptar Solicitud” — se crea vínculo activo.
  3. Asesor recibe notificación de aceptación.
  4. Asesor obtiene acceso inmediato al negocio.
  1. Clic en “Rechazar” — se abre confirmación.
  2. Confirma rechazo — solicitud marcada como rechazada.
  3. Asesor recibe notificación de rechazo.
  4. Solicitud ya no aparece en pendientes.
  • Toast: “Solicitud aceptada correctamente”.
  • Asesor aparece en pestaña “Activos”.
  • Solicitud desaparece de “Recibidas”.
  • Asesor puede acceder al negocio desde su panel.
  • Toast: “Solicitud rechazada”.
  • Solicitud desaparece de “Recibidas”.
  • Asesor recibe notificación del rechazo.
MensajeCausaSolución
”Solicitud no encontrada”Token inválido o solicitud eliminadaVerifica en lista de recibidas
”Solicitud expirada”Pasaron más de 7 díasAsesor debe enviar nueva solicitud
”Error al procesar solicitud”Error de servidorReintenta o contacta soporte
Ver implementación técnica

Página de revisión: src/panels/owner/pages/ReviewAdvisorRequest.tsx

Edge Function: respond-advisor-to-owner-invitation

Navegación:

// Desde OwnerAdvisorInvitationsManager
const handleReviewRequest = (invitation: ReceivedInvitation) => {
navigate(`/review-advisor-request/${invitation.token}`);
};

Input de respuesta:

{
token: string; // Token de la solicitud
response: 'accepted' | 'rejected';
finalPermissions?: Array<{ // Solo si accepted
section: string;
access: 'none' | 'read' | 'edit';
}>;
}

Lógica de aceptación:

// 1. Valida token y estado
const { data: invitation } = await supabase
.from('advisor_to_owner_invitations')
.select('*')
.eq('token', token)
.eq('status', 'pending')
.single();
if (!invitation || new Date(invitation.expires_at) < new Date()) {
throw new Error('Solicitud inválida o expirada');
}
// 2. Actualiza estado de solicitud
await supabase
.from('advisor_to_owner_invitations')
.update({
status: 'accepted',
accepted_at: new Date().toISOString(),
final_permissions: finalPermissions
})
.eq('token', token);
// 3. Crea vínculo activo
const { data: link } = await supabase
.from('advisor_client_links')
.insert({
advisor_id: invitation.advisor_id,
client_business_id: invitation.business_id,
invited_by: invitation.owner_id,
status: 'active',
linked_at: new Date().toISOString()
})
.select()
.single();
// 4. Crea permisos granulares
const permissionsToInsert = finalPermissions.map(p => ({
advisor_id: invitation.advisor_id,
client_business_id: invitation.business_id,
section: p.section,
access_level: p.access
}));
await supabase
.from('advisor_section_permissions')
.insert(permissionsToInsert);
// 5. Asigna rol de asesor si no lo tiene
const { data: existingRole } = await supabase
.from('user_roles')
.select('*')
.eq('user_id', invitation.advisor_id)
.eq('role', 'advisor')
.maybeSingle();
if (!existingRole) {
await supabase
.from('user_roles')
.insert({
user_id: invitation.advisor_id,
role: 'advisor'
});
}
// 6. Notifica al asesor
await supabase
.from('advisor_notifications')
.insert({
advisor_id: invitation.advisor_id,
client_business_id: invitation.business_id,
notification_type: 'access_granted',
title: 'Solicitud aceptada',
message: `Tu solicitud de acceso a ${businessName} ha sido aceptada`
});

Lógica de rechazo:

// Actualiza estado a rejected
await supabase
.from('advisor_to_owner_invitations')
.update({
status: 'rejected',
rejected_at: new Date().toISOString()
})
.eq('token', token);
// Notifica al asesor
await supabase
.from('advisor_notifications')
.insert({
advisor_id: invitation.advisor_id,
client_business_id: invitation.business_id,
notification_type: 'access_denied',
title: 'Solicitud rechazada',
message: `Tu solicitud de acceso a ${businessName} ha sido rechazada`
});

Tabla: advisor_to_owner_invitations

{
id: UUID;
advisor_id: UUID; // FK a auth.users
owner_id: UUID; // FK a auth.users
business_id: UUID; // FK a businesses
status: 'pending' | 'accepted' | 'rejected' | 'expired';
token: string; // UUID único
permissions: JSONB; // Permisos solicitados
final_permissions: JSONB; // Permisos finales aceptados
expires_at: TIMESTAMPTZ; // +7 días
sent_at: TIMESTAMPTZ;
accepted_at: TIMESTAMPTZ; // Cuando se acepta
rejected_at: TIMESTAMPTZ; // Cuando se rechaza
advisor_name: string; // Nombre del asesor
}

Query de diagnóstico:

-- Solicitud completa con detalles
SELECT
aoi.id,
aoi.status,
aoi.sent_at,
aoi.expires_at,
aoi.advisor_name,
aoi.permissions as requested_permissions,
aoi.final_permissions,
b.name as business_name,
p_owner.first_name || ' ' || p_owner.last_name as owner_name,
p_advisor.email as advisor_email
FROM advisor_to_owner_invitations aoi
JOIN businesses b ON aoi.business_id = b.id
JOIN profiles p_owner ON aoi.owner_id = p_owner.user_id
LEFT JOIN profiles p_advisor ON aoi.advisor_id = p_advisor.user_id
WHERE aoi.token = '<token>';
-- Verificar vínculo creado tras aceptación
SELECT
acl.*,
b.name as business_name
FROM advisor_client_links acl
JOIN businesses b ON acl.client_business_id = b.id
WHERE acl.advisor_id = '<advisor_id>'
AND acl.client_business_id = '<business_id>'
AND acl.status = 'active';