My Teams
Author: Carmine Antonio Bonavoglia Creation Date: 10/11/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 10/11/2025
TeamsSection Component (Complex - My Teams)
Overview & Complexity
TeamsSection is the most complex component in the Profile folder. It handles:
- Hierarchical role mapping (Super Admin → Admin → Team Admin → Member)
- Multi-level state management (pending changes, editing state, modal coordination)
- RBAC permission matrix (who can edit, add, remove)
- Notification orchestration (team member + all admin notifications)
- Optimistic updates with rollback capability
- Local staging of changes before saving (role changes buffered in
pendingChanges)
Props
{
teams: Array<{
id: string,
name: string,
status: "active" | "pending" | "deleted",
members: Array<Member>,
memberCount: number,
createdAt: Firestore timestamp,
clientRef: string,
subscriptionName: string,
subscriptionRef: string,
viewContext: {
overallRole: "Super Admin" | "Admin Client" | "Team Admin" | "Member"
}
}>,
loading: boolean,
onUpdateTeams: (teams) => void,
onRefreshTeams: async () => void
}
State Management
const [editingTeam, setEditingTeam] = useState(null); // Currently editing team ID
const [editingTeamName, setEditingTeamName] = useState(""); // Buffered team name
const [pendingChanges, setPendingChanges] = useState({}); // {email → newRole} map
const [originalTeamData, setOriginalTeamData] = useState(null); // For cancel restoration
const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false);
const [addMemberTeamId, setAddMemberTeamId] = useState(null);
const [addMemberTeamName, setAddMemberTeamName] = useState("");
const [addMemberClientRef, setAddMemberClientRef] = useState(null);
const [addMemberUserRole, setAddMemberUserRole] = useState(null);
const [isAddingMember, setIsAddingMember] = useState(false);
const [deleteConfirmVisible, setDeleteConfirmVisible] = useState(false);
const [memberToDelete, setMemberToDelete] = useState(null);
Role Hierarchy & Mapping
Server-side roles (in database):
- "Admin" (Profiles.role) → Super Admin
- "Cliente" (Profiles.role) with isMainProfile=true → Admin Client
- "Cliente" (Profiles.role) with isMainProfile=false → Team Member
Team member roles (in Team.members):
- isClientAdmin=true → Client Admin (cannot be removed/edited)
- isAdmin=true → Team Admin (can manage other members)
- isAdmin=false → Member (standard access)
Mapping function:
const mapRole = (serverRole, isClientAdmin = false, isTeamAdmin = false) => {
if (isClientAdmin) return "Client Admin";
if (isTeamAdmin) return "Team Admin";
return "Member"; // All others
};
Permission Matrix
| User Role | Can Edit Team | Can Add Member | Can Remove Member | Can Change Roles |
|---|---|---|---|---|
| Super Admin | ✅ | ✅ | ✅ (except Client Admin) | ✅ |
| Admin Client | ✅ | ✅ | ✅ (except Client Admin) | ✅ |
| Team Admin | ✅ | ✅ | ✅ (except Client Admin) | ✅ |
| Member | ❌ | ✅ | ❌ | ❌ |
Workflow: Edit Team Name & Member Roles
Step 1: Enter Edit Mode
const handleStartEditing = (team) => {
setEditingTeam(team.id);
setEditingTeamName(team.name);
setPendingChanges({});
setOriginalTeamData({
name: team.name,
members: JSON.parse(JSON.stringify(team.members)), // Deep copy
});
};
Step 2: Stage Role Changes (Not Saved Yet)
const handleRoleChange = (teamId, memberEmail, newRole) => {
setPendingChanges((prev) => ({
...prev,
[memberEmail]: newRole,
}));
};
Step 3: Save All Changes
const handleSaveChanges = async (teamId) => {
// 1. Update team name if changed
if (editingTeamName !== currentTeam.name) {
await updateTeamName(teamId, editingTeamName);
}
// 2. For each pending role change:
for (const [memberEmail, newRole] of Object.entries(pendingChanges)) {
// Update database
await updateMemberRole(teamId, memberEmail, newRole);
// Notify member of role change
await createTeamNotification({
type: "role_changed",
teamId,
teamName,
memberEmail,
newRole,
});
// Notify ALL team admins
await notifyAllTeamAdmins({
type: "role_changed",
teamId,
teamName,
memberEmail,
newRole,
});
}
// 3. Update local state
const updatedTeams = teams.map((team) => {
if (team.id === teamId) {
const updatedMembers = team.members.map((member) => {
if (pendingChanges[member.email]) {
return {
...member,
isAdmin: pendingChanges[member.email] === "Team Admin",
};
}
return member;
});
return { ...team, name: editingTeamName, members: updatedMembers };
}
return team;
});
onUpdateTeams(updatedTeams);
// 4. Reset editing state
setEditingTeam(null);
setPendingChanges({});
setOriginalTeamData(null);
};
Step 4: Cancel (Restore Original)
const handleCancelChanges = (teamId) => {
if (originalTeamData && onUpdateTeams) {
const updatedTeams = teams.map((team) =>
team.id === teamId
? {
...team,
name: originalTeamData.name,
members: originalTeamData.members,
}
: team
);
onUpdateTeams(updatedTeams);
}
setEditingTeam(null);
setPendingChanges({});
};
Members Table Display
Dynamic columns based on state:
| Column | Read Mode | Edit Mode | Render Logic |
|---|---|---|---|
| First Name | Text (bold if current user) | Text (bold if current user) | Compare email to currentUserEmail |
| Last Name | Text (bold if current user) | Text (bold if current user) | Compare email to currentUserEmail |
| Role | Tag (color-coded) | Select dropdown | Client Admin cannot be edited; dropdown shows available roles |
| Status | Tag (green/blue/red) | Tag (green/blue/red) | "Active", "Pending", "Deleted" |
| Joined | Formatted date | Formatted date | Parse Firestore timestamp or JS date |
| Actions | Dropdown if canEdit | Dropdown if canEdit | Resend (pending only), Remove (not Client Admin) |
Role tag colors:
- Client Admin → red
- Team Admin → blue
- Member → green
Status tag colors:
- Active/Accepted → success (green)
- Pending → processing (blue)
- Deleted → error (red)
Workflow: Remove Member
Step 1: Show Delete Confirmation
const showDeleteConfirmation = (teamId, memberEmail) => {
setMemberToDelete({ teamId, memberEmail });
setDeleteConfirmVisible(true);
};
Step 2: Remove from Database & Notify
const handleRemoveMember = async () => {
const { teamId, memberEmail } = memberToDelete;
// 1. Call API
await removeMemberFromTeam(teamId, memberEmail);
// 2. Notify removed member
await createTeamNotification({
type: "removed",
teamId,
teamName,
recipientEmail: memberEmail,
senderEmail: currentUserEmail,
});
// 3. Notify ALL team admins
await notifyAllTeamAdmins({
type: "removed",
teamId,
teamName,
senderEmail: currentUserEmail,
targetMemberEmail: memberEmail,
});
// 4. Update local state
const updatedTeams = teams.map((team) =>
team.id === teamId
? {
...team,
members: team.members.filter((m) => m.email !== memberEmail),
memberCount: team.members.length - 1,
}
: team
);
onUpdateTeams(updatedTeams);
};
Workflow: Resend Invite (Pending Members)
const handleResendInvite = async (teamId, memberEmail) => {
try {
const inviteApiUrl = process.env.REACT_APP_INVITE_MEMBER_TEAM;
const response = await fetch(inviteApiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ teamId, email: memberEmail }),
});
if (!response.ok) throw new Error(await response.text());
message.success("Invitation sent successfully!");
} catch (error) {
message.error("Failed to send invitation");
}
};
Notification Orchestration
Two-level notification system:
-
Direct notification to affected member:
await createTeamNotification({
type: 'role_changed' | 'removed',
teamId, teamName, recipientEmail, senderEmail, newRole?
}); -
Broadcast to ALL team admins (Client Admin + Team Admins):
await notifyAllTeamAdmins({
type: 'role_changed' | 'removed',
teamId, teamName, senderEmail, targetMemberEmail, newRole?
});
Error handling: Notifications failures don't block the operation. Logged but non-fatal.
AddMemberModal Component (Complex)
Overview
Handles both inviting new users and adding existing users to a team. Complex workflow with:
- Two-step process: Form submission → Confirmation modal → Firebase function call
- Member type selection: "Invite New User" vs "Add Existing User" (radio toggle)
- Email domain management: Fetched from Client doc, supports 1+ domains
- Role assignment: Team Admin checkbox (only for authorized users)
- Tutorial panel: Sticky right sidebar with instructions
Props
{
visible: boolean,
onCancel: () => void,
onSave: async (memberData) => void, // Called by TeamsSection
teamId: string,
teamName: string,
clientRef: string,
userRole: "Super Admin" | "Admin Client" | "Team Admin" | "Member",
loading: boolean,
onRefreshTeams: async () => void
}
Workflow: New User Invitation
Step 1: Form Submission
const handleSubmit = async (values) => {
// Build member data
const memberData = {
email: `${values.email}${selectedDomain}`,
firstName: values.firstName,
lastName: values.lastName,
isAdmin: canSetAdmin ? values.isAdmin : false,
teamId,
isExisting: false,
};
setPendingMemberData(memberData);
setShowConfirmInvitation(true); // Show confirmation modal
};
Step 2: Confirmation Modal
Are you sure you want to invite FirstName LastName (email@domain.com) to this team?
They will be added as [Team Admin | Member] with pending status and receive an invitation email.
[Cancel] [Send Invitation]
Step 3: On Confirm - Add to Database
// Call onSave (parent's callback)
await onSave(pendingMemberData);
// Parent handles: createUser in Profiles, addMemberToTeam in Team
Step 4: Send Invitation Email via Firebase Function
const inviteApiUrl = process.env.REACT_APP_INVITE_MEMBER_TEAM;
const response = await fetch(inviteApiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
teamId: teamId,
email: pendingMemberData.email,
}),
});
Error handling:
- If Firebase function fails, still show success (member was added)
- Log error but don't block
- Message: "Member created successfully! (Email notification failed)"
Workflow: Add Existing User
- Fetch available profiles:
fetchAvailableClientProfiles(clientRef, teamId) - User selects profile from dropdown
- Confirmation modal shows same flow as new user
- On confirm, call
addExistingMemberToTeam()instead - No email invitation sent (already has account)
- Status: "Active" (not "Pending")
MemberDetailsModal Component (Complex)
Overview
Detailed member view with:
- Read & edit modes for role and catalogues
- Dynamic subscription calculation based on assigned catalogues
- Add catalogues modal (nested)
- Tutorial panel (sticky right sidebar)
- Permissions-based UI (what can user edit?)
Props
{
visible: boolean,
onClose: () => void,
member: {
email: string,
firstName: string,
lastName: string,
role: string,
isMainProfile: boolean,
isAdmin?: boolean, // For team members
catalogues: Array<{ id, name, status }>,
licenses: Array<{ id, name, status }>,
clientRef: string
},
mapRole: (role, isMainProfile) => string,
getRoleColor: (mappedRole) => string,
currentUserRole: string,
onSave: async (updatedMember) => void,
onRefresh?: async () => void
}
Catalogue → Subscription Auto-Calculation
Problem: When user assigns catalogues to member, subscriptions should auto-update based on which subscriptions cover those catalogues.
Solution: useEffect chain
// 1. When modal opens, load all client catalogues
useEffect(() => {
const loadClientData = async () => {
const catalogues = await fetchClientCatalogues(member.clientRef);
setAllClientCatalogues(catalogues);
};
if (visible && member?.clientRef) {
loadClientData();
}
}, [visible, member?.clientRef]);
// 2. When member data or catalogues load, initialize edit state
useEffect(() => {
if (member && visible && !cataloguesLoading) {
setEditedRole(member.role);
setEditedIsMainProfile(member.isMainProfile);
setAssignedCatalogues(member.catalogues || []);
// Compute available (unassigned) catalogues
const memberCatalogueIds = new Set(member.catalogues.map((c) => c.id));
const available = allClientCatalogues.filter(
(c) => !memberCatalogueIds.has(c.id)
);
setAvailableCataloguesState(available);
setEditedSubscriptions(member.licenses || []);
}
}, [member, visible, allClientCatalogues, cataloguesLoading]);
// 3. When assigned catalogues change IN EDIT MODE, recalculate subscriptions
useEffect(() => {
if (isEditing && !isFirstEditRender.current) {
const updateSubscriptions = async () => {
const catalogueIds = assignedCatalogues.map((cat) => cat.id);
const updatedSubscriptions = await calculateSubscriptions(
catalogueIds
);
setEditedSubscriptions(updatedSubscriptions);
};
updateSubscriptions();
} else if (isEditing) {
// Skip first render when entering edit mode
isFirstEditRender.current = false;
}
}, [assignedCatalogues, isEditing]);
Permissions Model
| User Role | Can Edit Role | Can Edit Catalogues |
|---|---|---|
| Super Admin | ✅ | ✅ |
| Admin | ✅ | ✅ |
| Member (viewing peer) | ❌ | ❌ |
| Other roles | ❌ | ❌ |
Workflow: Save Changes
const handleSave = async () => {
// 1. Prepare update
const catalogueIds = assignedCatalogues.map(cat => cat.id);
const updatedMember = {
...member,
role: editedRole,
isMainProfile: editedIsMainProfile,
catalogues: assignedCatalogues,
licenses: editedSubscriptions,
list_catalogues: catalogueIds
};
// 2. Update Firestore
await updateProfileDetails(member.email, {
role: editedRole,
isMainProfile: editedIsMainProfile,
list_catalogues: catalogueIds
});
// 3. Notify parent via callback
await onSave(updatedMember);
// 4. Optionally refresh
if (onRefresh && (changes detected)) {
await onRefresh();
}
setIsEditing(false);
message.success('Member updated successfully');
};