Skip to main content
Version: 1.0.0

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 RoleCan Edit TeamCan Add MemberCan Remove MemberCan 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:

ColumnRead ModeEdit ModeRender Logic
First NameText (bold if current user)Text (bold if current user)Compare email to currentUserEmail
Last NameText (bold if current user)Text (bold if current user)Compare email to currentUserEmail
RoleTag (color-coded)Select dropdownClient Admin cannot be edited; dropdown shows available roles
StatusTag (green/blue/red)Tag (green/blue/red)"Active", "Pending", "Deleted"
JoinedFormatted dateFormatted dateParse Firestore timestamp or JS date
ActionsDropdown if canEditDropdown if canEditResend (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:

  1. Direct notification to affected member:

    await createTeamNotification({
    type: 'role_changed' | 'removed',
    teamId, teamName, recipientEmail, senderEmail, newRole?
    });
  2. 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

  1. Fetch available profiles: fetchAvailableClientProfiles(clientRef, teamId)
  2. User selects profile from dropdown
  3. Confirmation modal shows same flow as new user
  4. On confirm, call addExistingMemberToTeam() instead
  5. No email invitation sent (already has account)
  6. 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 RoleCan Edit RoleCan 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');
};