Fix team page
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { useTenant } from '../../stores/tenant.store';
|
import { useTenant } from '../../stores/tenant.store';
|
||||||
import { useToast } from '../../hooks/ui/useToast';
|
import { useToast } from '../../hooks/ui/useToast';
|
||||||
import { ChevronDown, Building2, Check, AlertCircle } from 'lucide-react';
|
import { ChevronDown, Building2, Check, AlertCircle } from 'lucide-react';
|
||||||
@@ -13,6 +14,13 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
|||||||
showLabel = true,
|
showLabel = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState<{
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right?: number;
|
||||||
|
width: number;
|
||||||
|
isMobile: boolean;
|
||||||
|
}>({ top: 0, left: 0, width: 288, isMobile: false });
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@@ -37,25 +45,103 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
|||||||
|
|
||||||
// Handle click outside to close dropdown
|
// Handle click outside to close dropdown
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
|
const target = event.target as Node;
|
||||||
if (
|
if (
|
||||||
dropdownRef.current &&
|
dropdownRef.current &&
|
||||||
!dropdownRef.current.contains(event.target as Node) &&
|
!dropdownRef.current.contains(target) &&
|
||||||
!buttonRef.current?.contains(event.target as Node)
|
!buttonRef.current?.contains(target)
|
||||||
) {
|
) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEscape = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('touchstart', handleClickOutside);
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('touchstart', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleEscape);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Recalculate position on window resize
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (isOpen) {
|
||||||
|
calculateDropdownPosition();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Calculate dropdown position
|
||||||
|
const calculateDropdownPosition = () => {
|
||||||
|
if (!buttonRef.current) return;
|
||||||
|
|
||||||
|
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const isMobile = viewportWidth < 768; // md breakpoint
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
// On mobile, use full width with margins and position from top
|
||||||
|
// Check if dropdown would go off bottom of screen
|
||||||
|
const dropdownHeight = Math.min(400, viewportHeight * 0.7); // Max 70vh
|
||||||
|
const spaceBelow = viewportHeight - buttonRect.bottom - 8;
|
||||||
|
const shouldPositionAbove = spaceBelow < dropdownHeight && buttonRect.top > dropdownHeight;
|
||||||
|
|
||||||
|
setDropdownPosition({
|
||||||
|
top: shouldPositionAbove ? buttonRect.top - dropdownHeight - 8 : buttonRect.bottom + 8,
|
||||||
|
left: 16, // 1rem margin from screen edge
|
||||||
|
right: 16, // For full width calculation
|
||||||
|
width: viewportWidth - 32, // Full width minus margins
|
||||||
|
isMobile: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Desktop positioning - align right edge of dropdown with right edge of button
|
||||||
|
const dropdownWidth = 320; // w-80 (20rem * 16px) - slightly wider for desktop
|
||||||
|
let left = buttonRect.right - dropdownWidth;
|
||||||
|
|
||||||
|
// Ensure dropdown doesn't go off the left edge of the screen
|
||||||
|
if (left < 16) {
|
||||||
|
left = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure dropdown doesn't go off the right edge
|
||||||
|
if (left + dropdownWidth > viewportWidth - 16) {
|
||||||
|
left = viewportWidth - dropdownWidth - 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDropdownPosition({
|
||||||
|
top: buttonRect.bottom + 8,
|
||||||
|
left: left,
|
||||||
|
width: dropdownWidth,
|
||||||
|
isMobile: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle dropdown open/close
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (!isOpen) {
|
||||||
|
calculateDropdownPosition();
|
||||||
|
}
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle tenant switch
|
// Handle tenant switch
|
||||||
const handleTenantSwitch = async (tenantId: string) => {
|
const handleTenantSwitch = async (tenantId: string) => {
|
||||||
if (tenantId === currentTenant?.id) {
|
if (tenantId === currentTenant?.id) {
|
||||||
@@ -104,7 +190,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
|||||||
{/* Trigger Button */}
|
{/* Trigger Button */}
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={toggleDropdown}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border-secondary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border-secondary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
@@ -124,18 +210,28 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
{/* Dropdown Menu - Rendered in portal to avoid stacking context issues */}
|
||||||
{isOpen && (
|
{isOpen && createPortal(
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className="absolute right-0 mt-2 w-72 bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-50"
|
className={`fixed bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-[9999] ${
|
||||||
|
dropdownPosition.isMobile ? 'mx-4' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
top: `${dropdownPosition.top}px`,
|
||||||
|
left: `${dropdownPosition.left}px`,
|
||||||
|
width: `${dropdownPosition.width}px`,
|
||||||
|
maxHeight: dropdownPosition.isMobile ? '70vh' : '80vh',
|
||||||
|
}}
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-label="Available tenants"
|
aria-label="Available tenants"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-3 py-2 border-b border-border-primary">
|
<div className={`border-b border-border-primary ${dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'}`}>
|
||||||
<h3 className="text-sm font-semibold text-text-primary">Switch Organization</h3>
|
<h3 className={`font-semibold text-text-primary ${dropdownPosition.isMobile ? 'text-base' : 'text-sm'}`}>
|
||||||
<p className="text-xs text-text-secondary">
|
Switch Organization
|
||||||
|
</h3>
|
||||||
|
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm mt-1' : 'text-xs'}`}>
|
||||||
Select the organization you want to work with
|
Select the organization you want to work with
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,13 +253,15 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tenant List */}
|
{/* Tenant List */}
|
||||||
<div className="max-h-80 overflow-y-auto">
|
<div className={`overflow-y-auto ${dropdownPosition.isMobile ? 'max-h-[60vh]' : 'max-h-80'}`}>
|
||||||
{availableTenants.map((tenant) => (
|
{availableTenants.map((tenant) => (
|
||||||
<button
|
<button
|
||||||
key={tenant.id}
|
key={tenant.id}
|
||||||
onClick={() => handleTenantSwitch(tenant.id)}
|
onClick={() => handleTenantSwitch(tenant.id)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full px-3 py-3 text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className={`w-full text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
|
||||||
|
dropdownPosition.isMobile ? 'px-4 py-4 active:bg-bg-tertiary' : 'px-3 py-3'
|
||||||
|
}`}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={tenant.id === currentTenant?.id}
|
aria-selected={tenant.id === currentTenant?.id}
|
||||||
>
|
>
|
||||||
@@ -193,15 +291,20 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-3 py-2 border-t border-border-primary bg-bg-secondary rounded-b-lg">
|
<div className={`border-t border-border-primary bg-bg-secondary rounded-b-lg ${
|
||||||
<p className="text-xs text-text-secondary">
|
dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'
|
||||||
|
}`}>
|
||||||
|
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm' : 'text-xs'}`}>
|
||||||
Need to add a new organization?{' '}
|
Need to add a new organization?{' '}
|
||||||
<button className="text-color-primary hover:text-color-primary-dark underline">
|
<button className={`text-color-primary hover:text-color-primary-dark underline ${
|
||||||
|
dropdownPosition.isMobile ? 'active:text-color-primary-dark' : ''
|
||||||
|
}`}>
|
||||||
Contact Support
|
Contact Support
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading Overlay */}
|
{/* Loading Overlay */}
|
||||||
|
|||||||
@@ -1,76 +1,256 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
|
import { Users, Plus, Search, Mail, Phone, Shield, Trash2, Crown, X, UserCheck } from 'lucide-react';
|
||||||
import { Button, Card, Badge, Input } from '../../../../components/ui';
|
import { Button, Card, Badge, Input, StatusCard, getStatusColor } from '../../../../components/ui';
|
||||||
import { PageHeader } from '../../../../components/layout';
|
import { PageHeader } from '../../../../components/layout';
|
||||||
import { useTeamMembers } from '../../../../api/hooks/tenant';
|
import { useTeamMembers, useAddTeamMember, useRemoveTeamMember, useUpdateMemberRole } from '../../../../api/hooks/tenant';
|
||||||
import { useAllUsers, useUpdateUser } from '../../../../api/hooks/user';
|
import { useAllUsers } from '../../../../api/hooks/user';
|
||||||
import { useAuthUser } from '../../../../stores/auth.store';
|
import { useAuthUser } from '../../../../stores/auth.store';
|
||||||
|
import { useCurrentTenant, useCurrentTenantAccess } from '../../../../stores/tenant.store';
|
||||||
import { useToast } from '../../../../hooks/ui/useToast';
|
import { useToast } from '../../../../hooks/ui/useToast';
|
||||||
|
import { TENANT_ROLES } from '../../../../types/roles';
|
||||||
|
|
||||||
const TeamPage: React.FC = () => {
|
const TeamPage: React.FC = () => {
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const user = useAuthUser();
|
const currentTenant = useCurrentTenant();
|
||||||
const tenantId = user?.tenant_id || '';
|
const currentTenantAccess = useCurrentTenantAccess();
|
||||||
|
const tenantId = currentTenant?.id || '';
|
||||||
|
|
||||||
const { data: teamMembers = [], isLoading, error } = useTeamMembers(tenantId, true, { enabled: !!tenantId });
|
const { data: teamMembers = [], isLoading } = useTeamMembers(tenantId, false, { enabled: !!tenantId }); // Show all members including inactive
|
||||||
const { data: allUsers = [] } = useAllUsers();
|
const { data: allUsers = [] } = useAllUsers();
|
||||||
const updateUserMutation = useUpdateUser();
|
|
||||||
|
// Mutations
|
||||||
|
const addMemberMutation = useAddTeamMember();
|
||||||
|
const removeMemberMutation = useRemoveTeamMember();
|
||||||
|
const updateRoleMutation = useUpdateMemberRole();
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [selectedRole, setSelectedRole] = useState('all');
|
const [selectedRole, setSelectedRole] = useState('all');
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const [selectedUserToAdd, setSelectedUserToAdd] = useState('');
|
||||||
|
const [selectedRoleToAdd, setSelectedRoleToAdd] = useState<string>(TENANT_ROLES.MEMBER);
|
||||||
|
|
||||||
|
|
||||||
|
// Enhanced team members that includes owner information
|
||||||
|
const enhancedTeamMembers = useMemo(() => {
|
||||||
|
const members = [...teamMembers];
|
||||||
|
|
||||||
|
// If tenant owner is not in the members list, add them
|
||||||
|
if (currentTenant?.owner_id) {
|
||||||
|
const ownerInMembers = members.find(m => m.user_id === currentTenant.owner_id);
|
||||||
|
if (!ownerInMembers) {
|
||||||
|
// Find owner user data
|
||||||
|
const ownerUser = allUsers.find(u => u.id === currentTenant.owner_id);
|
||||||
|
if (ownerUser) {
|
||||||
|
// Add owner as a member
|
||||||
|
members.push({
|
||||||
|
id: `owner-${currentTenant.owner_id}`,
|
||||||
|
tenant_id: tenantId,
|
||||||
|
user_id: currentTenant.owner_id,
|
||||||
|
role: TENANT_ROLES.OWNER,
|
||||||
|
is_active: true,
|
||||||
|
joined_at: currentTenant.created_at,
|
||||||
|
user_email: ownerUser.email,
|
||||||
|
user_full_name: ownerUser.full_name,
|
||||||
|
user: ownerUser, // Add full user object for compatibility
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
} else if (ownerInMembers.role !== TENANT_ROLES.OWNER) {
|
||||||
|
// Update existing member to owner role
|
||||||
|
ownerInMembers.role = TENANT_ROLES.OWNER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return members;
|
||||||
|
}, [teamMembers, currentTenant, allUsers, tenantId]);
|
||||||
|
|
||||||
const roles = [
|
const roles = [
|
||||||
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
|
{ value: 'all', label: 'Todos los Roles', count: enhancedTeamMembers.length },
|
||||||
{ value: 'owner', label: 'Propietario', count: teamMembers.filter(m => m.role === 'owner').length },
|
{ value: TENANT_ROLES.OWNER, label: 'Propietario', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.OWNER).length },
|
||||||
{ value: 'admin', label: 'Administrador', count: teamMembers.filter(m => m.role === 'admin').length },
|
{ value: TENANT_ROLES.ADMIN, label: 'Administrador', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.ADMIN).length },
|
||||||
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
|
{ value: TENANT_ROLES.MEMBER, label: 'Miembro', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.MEMBER).length },
|
||||||
{ value: 'employee', label: 'Empleado', count: teamMembers.filter(m => m.role === 'employee').length }
|
{ value: TENANT_ROLES.VIEWER, label: 'Observador', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.VIEWER).length }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Permission checks
|
||||||
|
const isOwner = currentTenantAccess?.role === TENANT_ROLES.OWNER;
|
||||||
|
const canManageTeam = isOwner || currentTenantAccess?.role === TENANT_ROLES.ADMIN;
|
||||||
|
|
||||||
const teamStats = {
|
const teamStats = {
|
||||||
total: teamMembers.length,
|
total: enhancedTeamMembers.length,
|
||||||
active: teamMembers.filter(m => m.status === 'active').length,
|
active: enhancedTeamMembers.filter(m => m.is_active).length,
|
||||||
departments: {
|
owners: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.OWNER).length,
|
||||||
production: teamMembers.filter(m => m.department === 'Producción').length,
|
admins: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.ADMIN).length,
|
||||||
sales: teamMembers.filter(m => m.department === 'Ventas').length,
|
members: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.MEMBER).length,
|
||||||
admin: teamMembers.filter(m => m.department === 'Administración').length
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRoleBadgeColor = (role: string) => {
|
|
||||||
switch (role) {
|
|
||||||
case 'manager': return 'purple';
|
|
||||||
case 'baker': return 'green';
|
|
||||||
case 'cashier': return 'blue';
|
|
||||||
case 'assistant': return 'yellow';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
return status === 'active' ? 'green' : 'red';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRoleLabel = (role: string) => {
|
const getRoleLabel = (role: string) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'manager': return 'Gerente';
|
case TENANT_ROLES.OWNER: return 'Propietario';
|
||||||
case 'baker': return 'Panadero';
|
case TENANT_ROLES.ADMIN: return 'Administrador';
|
||||||
case 'cashier': return 'Cajero';
|
case TENANT_ROLES.MEMBER: return 'Miembro';
|
||||||
case 'assistant': return 'Asistente';
|
case TENANT_ROLES.VIEWER: return 'Observador';
|
||||||
default: return role;
|
default: return role;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredMembers = teamMembers.filter(member => {
|
// StatusCard configuration for team members
|
||||||
|
const getMemberStatusConfig = (member: any) => {
|
||||||
|
if (member.role === TENANT_ROLES.OWNER) {
|
||||||
|
return {
|
||||||
|
color: getStatusColor('completed'), // Purple/primary for owner
|
||||||
|
text: getRoleLabel(member.role),
|
||||||
|
icon: Crown,
|
||||||
|
isCritical: false,
|
||||||
|
isHighlight: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.role === TENANT_ROLES.ADMIN) {
|
||||||
|
return {
|
||||||
|
color: getStatusColor('inProgress'), // Blue for admin
|
||||||
|
text: getRoleLabel(member.role),
|
||||||
|
icon: Shield,
|
||||||
|
isCritical: false,
|
||||||
|
isHighlight: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.role === TENANT_ROLES.MEMBER) {
|
||||||
|
return {
|
||||||
|
color: getStatusColor('normal'), // Green for member
|
||||||
|
text: getRoleLabel(member.role),
|
||||||
|
icon: Users,
|
||||||
|
isCritical: false,
|
||||||
|
isHighlight: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIEWER or other roles
|
||||||
|
return {
|
||||||
|
color: getStatusColor('pending'), // Yellow for viewer
|
||||||
|
text: getRoleLabel(member.role),
|
||||||
|
icon: Users,
|
||||||
|
isCritical: false,
|
||||||
|
isHighlight: false
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMemberActions = (member: any) => {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
// Role change actions (only for non-owners and if user can manage team)
|
||||||
|
if (canManageTeam && member.role !== TENANT_ROLES.OWNER) {
|
||||||
|
if (member.role !== TENANT_ROLES.ADMIN) {
|
||||||
|
actions.push({
|
||||||
|
label: 'Hacer Admin',
|
||||||
|
icon: Shield,
|
||||||
|
onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.ADMIN),
|
||||||
|
priority: 'secondary' as const,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.role !== TENANT_ROLES.MEMBER) {
|
||||||
|
actions.push({
|
||||||
|
label: 'Hacer Miembro',
|
||||||
|
icon: Users,
|
||||||
|
onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.MEMBER),
|
||||||
|
priority: 'secondary' as const,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.role !== TENANT_ROLES.VIEWER) {
|
||||||
|
actions.push({
|
||||||
|
label: 'Hacer Observador',
|
||||||
|
onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.VIEWER),
|
||||||
|
priority: 'tertiary' as const,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove member action (only for owners)
|
||||||
|
if (isOwner && member.role !== TENANT_ROLES.OWNER) {
|
||||||
|
actions.push({
|
||||||
|
label: 'Remover',
|
||||||
|
icon: Trash2,
|
||||||
|
onClick: () => {
|
||||||
|
if (confirm('¿Estás seguro de que deseas remover este miembro?')) {
|
||||||
|
handleRemoveMember(member.user_id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
priority: 'tertiary' as const,
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredMembers = enhancedTeamMembers.filter(member => {
|
||||||
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
|
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
|
||||||
const matchesSearch = member.user.first_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const userName = member.user?.full_name || member.user_full_name || '';
|
||||||
member.user.last_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const userEmail = member.user?.email || member.user_email || '';
|
||||||
member.user.email.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = userName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
userEmail.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
return matchesRole && matchesSearch;
|
return matchesRole && matchesSearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Available users for adding (exclude current members)
|
||||||
|
const availableUsers = allUsers.filter(u =>
|
||||||
|
!enhancedTeamMembers.some(m => m.user_id === u.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Member action handlers
|
||||||
|
const handleAddMember = async () => {
|
||||||
|
if (!selectedUserToAdd || !selectedRoleToAdd || !tenantId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addMemberMutation.mutateAsync({
|
||||||
|
tenantId,
|
||||||
|
userId: selectedUserToAdd,
|
||||||
|
role: selectedRoleToAdd,
|
||||||
|
});
|
||||||
|
|
||||||
|
addToast('Miembro agregado exitosamente', { type: 'success' });
|
||||||
|
setShowAddForm(false);
|
||||||
|
setSelectedUserToAdd('');
|
||||||
|
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
|
||||||
|
} catch (error) {
|
||||||
|
addToast('Error al agregar miembro', { type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveMember = async (memberUserId: string) => {
|
||||||
|
if (!tenantId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeMemberMutation.mutateAsync({
|
||||||
|
tenantId,
|
||||||
|
memberUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
addToast('Miembro removido exitosamente', { type: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
addToast('Error al remover miembro', { type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateRole = async (memberUserId: string, newRole: string) => {
|
||||||
|
if (!tenantId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateRoleMutation.mutateAsync({
|
||||||
|
tenantId,
|
||||||
|
memberUserId,
|
||||||
|
newRole,
|
||||||
|
});
|
||||||
|
|
||||||
|
addToast('Rol actualizado exitosamente', { type: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
addToast('Error al actualizar rol', { type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
@@ -88,30 +268,19 @@ const TeamPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatLastLogin = (timestamp: string) => {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const now = new Date();
|
|
||||||
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
if (diffInDays === 0) {
|
|
||||||
return 'Hoy ' + date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
|
|
||||||
} else if (diffInDays === 1) {
|
|
||||||
return 'Ayer';
|
|
||||||
} else {
|
|
||||||
return `hace ${diffInDays} días`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Gestión de Equipo"
|
title="Gestión de Equipo"
|
||||||
description="Administra los miembros del equipo, roles y permisos"
|
description="Administra los miembros del equipo, roles y permisos"
|
||||||
action={
|
actions={
|
||||||
<Button onClick={() => setShowForm(true)}>
|
canManageTeam && availableUsers.length > 0 ? [{
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
id: 'add-member',
|
||||||
Nuevo Miembro
|
label: 'Agregar Miembro',
|
||||||
</Button>
|
icon: Plus,
|
||||||
|
onClick: () => setShowAddForm(true)
|
||||||
|
}] : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -144,11 +313,11 @@ const TeamPage: React.FC = () => {
|
|||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Producción</p>
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Administradores</p>
|
||||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{teamStats.departments.production}</p>
|
<p className="text-3xl font-bold text-[var(--color-primary)]">{teamStats.admins}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||||
<Users className="h-6 w-6 text-[var(--color-primary)]" />
|
<Shield className="h-6 w-6 text-[var(--color-primary)]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -156,11 +325,11 @@ const TeamPage: React.FC = () => {
|
|||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Ventas</p>
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Propietarios</p>
|
||||||
<p className="text-3xl font-bold text-purple-600">{teamStats.departments.sales}</p>
|
<p className="text-3xl font-bold text-purple-600">{teamStats.owners}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||||
<Users className="h-6 w-6 text-purple-600" />
|
<Crown className="h-6 w-6 text-purple-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -199,107 +368,148 @@ const TeamPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Team Members List */}
|
{/* Team Members List - Responsive grid */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6">
|
||||||
{filteredMembers.map((member) => (
|
{filteredMembers.map((member) => (
|
||||||
<Card key={member.id} className="p-6">
|
<StatusCard
|
||||||
<div className="flex items-start justify-between">
|
key={member.id}
|
||||||
<div className="flex items-start space-x-4 flex-1">
|
id={`team-member-${member.id}`}
|
||||||
<div className="w-12 h-12 bg-[var(--bg-quaternary)] rounded-full flex items-center justify-center">
|
statusIndicator={getMemberStatusConfig(member)}
|
||||||
<Users className="w-6 h-6 text-[var(--text-tertiary)]" />
|
title={member.user?.full_name || member.user_full_name}
|
||||||
</div>
|
subtitle={member.user?.email || member.user_email}
|
||||||
|
primaryValue={member.is_active ? 'Activo' : 'Inactivo'}
|
||||||
<div className="flex-1">
|
primaryValueLabel="Estado"
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
secondaryInfo={{
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{member.user.first_name} {member.user.last_name}</h3>
|
label: 'Se unió',
|
||||||
<Badge variant={getStatusColor(member.status)}>
|
value: new Date(member.joined_at).toLocaleDateString('es-ES')
|
||||||
{member.status === 'active' ? 'Activo' : 'Inactivo'}
|
}}
|
||||||
</Badge>
|
metadata={[
|
||||||
</div>
|
`Email: ${member.user?.email || member.user_email}`,
|
||||||
|
`Teléfono: ${member.user?.phone || 'No disponible'}`,
|
||||||
<div className="space-y-1 mb-3">
|
...(member.role === TENANT_ROLES.OWNER ? ['🏢 Propietario de la organización'] : [])
|
||||||
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
]}
|
||||||
<Mail className="w-4 h-4 mr-2" />
|
actions={getMemberActions(member)}
|
||||||
{member.user.email}
|
className={`
|
||||||
</div>
|
${!member.is_active ? 'opacity-75' : ''}
|
||||||
<div className="flex items-center text-sm text-[var(--text-secondary)]">
|
transition-all duration-200 hover:scale-[1.02]
|
||||||
<Phone className="w-4 h-4 mr-2" />
|
`}
|
||||||
{member.user.phone || 'No disponible'}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 mb-3">
|
|
||||||
<Badge variant={getRoleBadgeColor(member.role)}>
|
|
||||||
{getRoleLabel(member.role)}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="gray">
|
|
||||||
{member.department}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-[var(--text-tertiary)] mb-3">
|
|
||||||
<p>Se unió: {new Date(member.joined_at).toLocaleDateString('es-ES')}</p>
|
|
||||||
<p>Estado: {member.is_active ? 'Activo' : 'Inactivo'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Permissions */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">Permisos:</p>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
<span className="px-2 py-1 bg-[var(--color-info)]/10 text-[var(--color-info)] text-xs rounded-full">
|
|
||||||
{getRoleLabel(member.role)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Member Info */}
|
|
||||||
<div className="text-xs text-[var(--text-tertiary)]">
|
|
||||||
<p className="font-medium mb-1">Información adicional:</p>
|
|
||||||
<p>ID: {member.id}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm" variant="outline">
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className={member.status === 'active' ? 'text-[var(--color-error)] hover:text-[var(--color-error)]' : 'text-[var(--color-success)] hover:text-[var(--color-success)]'}
|
|
||||||
>
|
|
||||||
{member.status === 'active' ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredMembers.length === 0 && (
|
{filteredMembers.length === 0 && (
|
||||||
<Card className="p-12 text-center">
|
<StatusCard
|
||||||
<Users className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
|
id="empty-state"
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No se encontraron miembros</h3>
|
statusIndicator={{
|
||||||
<p className="text-[var(--text-secondary)]">
|
color: getStatusColor('pending'),
|
||||||
No hay miembros del equipo que coincidan con los filtros seleccionados.
|
text: searchTerm || selectedRole !== 'all' ? 'Sin coincidencias' : 'Equipo vacío',
|
||||||
</p>
|
icon: Users,
|
||||||
</Card>
|
isCritical: false,
|
||||||
|
isHighlight: false
|
||||||
|
}}
|
||||||
|
title="No se encontraron miembros"
|
||||||
|
subtitle={searchTerm || selectedRole !== 'all'
|
||||||
|
? "No hay miembros que coincidan con los filtros seleccionados"
|
||||||
|
: "Este tenant aún no tiene miembros del equipo"
|
||||||
|
}
|
||||||
|
primaryValue="0"
|
||||||
|
primaryValueLabel="Miembros"
|
||||||
|
actions={canManageTeam && availableUsers.length > 0 ? [{
|
||||||
|
label: 'Agregar Primer Miembro',
|
||||||
|
icon: Plus,
|
||||||
|
onClick: () => setShowAddForm(true),
|
||||||
|
priority: 'primary' as const,
|
||||||
|
}] : []}
|
||||||
|
className="col-span-full"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add Member Modal Placeholder */}
|
{/* Add Member Modal */}
|
||||||
{showForm && (
|
{showAddForm && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<Card className="p-6 max-w-md w-full mx-4">
|
<Card className="p-6 max-w-md w-full mx-4">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Nuevo Miembro del Equipo</h3>
|
<div className="flex items-center justify-between mb-4">
|
||||||
<p className="text-[var(--text-secondary)] mb-4">
|
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Agregar Miembro al Equipo</h3>
|
||||||
Formulario para agregar un nuevo miembro del equipo.
|
<Button
|
||||||
</p>
|
size="sm"
|
||||||
<div className="flex space-x-2">
|
variant="ghost"
|
||||||
<Button size="sm" onClick={() => setShowForm(false)}>
|
onClick={() => setShowAddForm(false)}
|
||||||
Guardar
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => setShowForm(false)}>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* User Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Usuario
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedUserToAdd}
|
||||||
|
onChange={(e) => setSelectedUserToAdd(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-border-secondary rounded-lg bg-bg-primary focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Seleccionar usuario...</option>
|
||||||
|
{availableUsers.map(user => (
|
||||||
|
<option key={user.id} value={user.id}>
|
||||||
|
{user.full_name} ({user.email})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{availableUsers.length === 0 && (
|
||||||
|
<p className="text-sm text-[var(--text-tertiary)] mt-1">
|
||||||
|
No hay usuarios disponibles para agregar
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||||
|
Rol
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedRoleToAdd}
|
||||||
|
onChange={(e) => setSelectedRoleToAdd(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-border-secondary rounded-lg bg-bg-primary focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20"
|
||||||
|
>
|
||||||
|
<option value={TENANT_ROLES.MEMBER}>Miembro - Acceso estándar</option>
|
||||||
|
<option value={TENANT_ROLES.ADMIN}>Administrador - Gestión de equipo</option>
|
||||||
|
<option value={TENANT_ROLES.VIEWER}>Observador - Solo lectura</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role Description */}
|
||||||
|
<div className="p-3 bg-bg-secondary rounded-lg">
|
||||||
|
<p className="text-xs text-[var(--text-secondary)]">
|
||||||
|
{selectedRoleToAdd === TENANT_ROLES.ADMIN &&
|
||||||
|
'Los administradores pueden gestionar miembros del equipo y configuraciones.'}
|
||||||
|
{selectedRoleToAdd === TENANT_ROLES.MEMBER &&
|
||||||
|
'Los miembros tienen acceso completo para trabajar con datos y funcionalidades.'}
|
||||||
|
{selectedRoleToAdd === TENANT_ROLES.VIEWER &&
|
||||||
|
'Los observadores solo pueden ver datos, sin realizar cambios.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2 mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={handleAddMember}
|
||||||
|
disabled={!selectedUserToAdd || addMemberMutation.isPending}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{addMemberMutation.isPending ? 'Agregando...' : 'Agregar Miembro'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddForm(false);
|
||||||
|
setSelectedUserToAdd('');
|
||||||
|
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -330,7 +330,6 @@ async def add_team_member_enhanced(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/tenants/{tenant_id}/members", response_model=List[TenantMemberResponse])
|
@router.get("/tenants/{tenant_id}/members", response_model=List[TenantMemberResponse])
|
||||||
@track_endpoint_metrics("tenant_get_members")
|
|
||||||
async def get_team_members_enhanced(
|
async def get_team_members_enhanced(
|
||||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||||
active_only: bool = Query(True, description="Only return active members"),
|
active_only: bool = Query(True, description="Only return active members"),
|
||||||
|
|||||||
@@ -305,29 +305,32 @@ class EnhancedTenantService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create membership using repository
|
# Create membership using repository
|
||||||
membership_data = {
|
async with self.database_manager.get_session() as db_session:
|
||||||
"tenant_id": tenant_id,
|
await self._init_repositories(db_session)
|
||||||
"user_id": user_id,
|
|
||||||
"role": role,
|
|
||||||
"invited_by": invited_by,
|
|
||||||
"is_active": True
|
|
||||||
}
|
|
||||||
|
|
||||||
member = await self.member_repo.create_membership(membership_data)
|
membership_data = {
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"role": role,
|
||||||
|
"invited_by": invited_by,
|
||||||
|
"is_active": True
|
||||||
|
}
|
||||||
|
|
||||||
# Publish event
|
member = await self.member_repo.create_membership(membership_data)
|
||||||
try:
|
|
||||||
await publish_member_added(tenant_id, user_id, role)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Failed to publish member added event", error=str(e))
|
|
||||||
|
|
||||||
logger.info("Team member added successfully",
|
# Publish event
|
||||||
tenant_id=tenant_id,
|
try:
|
||||||
user_id=user_id,
|
await publish_member_added(tenant_id, user_id, role)
|
||||||
role=role,
|
except Exception as e:
|
||||||
invited_by=invited_by)
|
logger.warning("Failed to publish member added event", error=str(e))
|
||||||
|
|
||||||
return TenantMemberResponse.from_orm(member)
|
logger.info("Team member added successfully",
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
role=role,
|
||||||
|
invited_by=invited_by)
|
||||||
|
|
||||||
|
return TenantMemberResponse.from_orm(member)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@@ -359,12 +362,15 @@ class EnhancedTenantService:
|
|||||||
"""Get all team members for a tenant"""
|
"""Get all team members for a tenant"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
async with self.database_manager.get_session() as session:
|
||||||
|
# Initialize repositories with session
|
||||||
|
await self._init_repositories(session)
|
||||||
|
|
||||||
members = await self.member_repo.get_tenant_members(
|
members = await self.member_repo.get_tenant_members(
|
||||||
tenant_id, active_only=active_only
|
tenant_id, active_only=active_only
|
||||||
)
|
)
|
||||||
|
|
||||||
return [TenantMemberResponse.from_orm(member) for member in members]
|
return [TenantMemberResponse.from_orm(member) for member in members]
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user