fix architecture and write new tests

This commit is contained in:
root
2026-06-10 00:40:19 -04:00
parent 560da1cadf
commit 80a597bc10
377 changed files with 84020 additions and 1337 deletions
@@ -0,0 +1,104 @@
import { describe, expect, it } from 'vitest'
import {
appendInvitedMember,
applyActivationStats,
applyMemberActivation,
applyMemberRoleUpdate,
incrementInvitedStats,
removeTeamMemberState,
type TeamMember,
type TeamStats,
} from './useTeam'
function member(overrides: Partial<TeamMember>): TeamMember {
return {
id: 'member-1',
firstName: 'Amina',
lastName: 'Bennis',
email: 'amina@example.com',
phone: null,
role: 'AGENT',
isActive: true,
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
lastActiveAt: null,
invitationStatus: 'accepted',
...overrides,
}
}
const stats: TeamStats = { total: 3, active: 2, pending: 1, inactive: 0 }
describe('useTeam state helpers', () => {
it('appends invited employees and increments the pending counters immutably', () => {
const existing = [member({ id: 'owner', role: 'OWNER' })]
const invited = member({ id: 'invite-1', role: 'MANAGER', invitationStatus: 'pending', isActive: false })
expect(appendInvitedMember(existing, invited)).toEqual([...existing, invited])
expect(existing).toHaveLength(1)
expect(incrementInvitedStats(stats)).toEqual({ total: 4, active: 2, pending: 2, inactive: 0 })
})
it('updates roles and activation flags only on the targeted member', () => {
const members = [member({ id: 'a' }), member({ id: 'b', role: 'MANAGER', isActive: false })]
expect(applyMemberRoleUpdate(members, 'a', 'MANAGER')).toEqual([
{ ...members[0], role: 'MANAGER' },
members[1],
])
expect(applyMemberActivation(members, 'b', true)).toEqual([
members[0],
{ ...members[1], isActive: true },
])
})
it('moves counters between active and inactive for deactivate/reactivate actions', () => {
expect(applyActivationStats({ total: 5, active: 4, pending: 0, inactive: 1 }, 'deactivate')).toEqual({
total: 5,
active: 3,
pending: 0,
inactive: 2,
})
expect(applyActivationStats({ total: 5, active: 3, pending: 0, inactive: 2 }, 'reactivate')).toEqual({
total: 5,
active: 4,
pending: 0,
inactive: 1,
})
})
it('removes active, pending, and inactive accepted members with the correct counter impact', () => {
const members = [
member({ id: 'active', isActive: true, invitationStatus: 'accepted' }),
member({ id: 'pending', isActive: false, invitationStatus: 'pending' }),
member({ id: 'inactive', isActive: false, invitationStatus: 'accepted' }),
]
expect(removeTeamMemberState(members, { total: 3, active: 1, pending: 1, inactive: 1 }, 'active').stats).toEqual({
total: 2,
active: 0,
pending: 1,
inactive: 1,
})
expect(removeTeamMemberState(members, { total: 3, active: 1, pending: 1, inactive: 1 }, 'pending').stats).toEqual({
total: 2,
active: 1,
pending: 0,
inactive: 1,
})
expect(removeTeamMemberState(members, { total: 3, active: 1, pending: 1, inactive: 1 }, 'inactive').stats).toEqual({
total: 2,
active: 1,
pending: 1,
inactive: 0,
})
})
it('leaves counters untouched when asked to remove an unknown member', () => {
const members = [member({ id: 'only' })]
const result = removeTeamMemberState(members, { total: 1, active: 1, pending: 0, inactive: 0 }, 'missing')
expect(result.members).toEqual(members)
expect(result.stats).toEqual({ total: 1, active: 1, pending: 0, inactive: 0 })
})
})
+51 -29
View File
@@ -34,6 +34,46 @@ export interface InvitePayload {
role: 'MANAGER' | 'AGENT'
}
export function appendInvitedMember(members: TeamMember[], employee: TeamMember): TeamMember[] {
return [...members, employee]
}
export function incrementInvitedStats(stats: TeamStats): TeamStats {
return { ...stats, total: stats.total + 1, pending: stats.pending + 1 }
}
export function applyMemberRoleUpdate(members: TeamMember[], memberId: string, role: 'MANAGER' | 'AGENT'): TeamMember[] {
return members.map((member) => (member.id === memberId ? { ...member, role } : member))
}
export function applyMemberActivation(members: TeamMember[], memberId: string, isActive: boolean): TeamMember[] {
return members.map((member) => (member.id === memberId ? { ...member, isActive } : member))
}
export function applyActivationStats(stats: TeamStats, direction: 'deactivate' | 'reactivate'): TeamStats {
return direction === 'deactivate'
? { ...stats, active: stats.active - 1, inactive: stats.inactive + 1 }
: { ...stats, active: stats.active + 1, inactive: stats.inactive - 1 }
}
export function removeTeamMemberState(members: TeamMember[], stats: TeamStats, memberId: string): { members: TeamMember[]; stats: TeamStats } {
const target = members.find((member) => member.id === memberId)
return {
members: members.filter((member) => member.id !== memberId),
stats: {
...stats,
total: stats.total - (target ? 1 : 0),
active: target?.isActive ? stats.active - 1 : stats.active,
pending: target?.invitationStatus === 'pending' ? stats.pending - 1 : stats.pending,
inactive: !target?.isActive && target?.invitationStatus === 'accepted'
? stats.inactive - 1
: stats.inactive,
},
}
}
export function useTeam() {
const [members, setMembers] = useState<TeamMember[]>([])
const [stats, setStats] = useState<TeamStats>({ total: 0, active: 0, pending: 0, inactive: 0 })
@@ -64,12 +104,8 @@ export function useTeam() {
'/team/invite',
{ method: 'POST', body: JSON.stringify(payload) }
)
setMembers((prev) => [...prev, result.employee])
setStats((prev) => ({
...prev,
total: prev.total + 1,
pending: prev.pending + 1,
}))
setMembers((prev) => appendInvitedMember(prev, result.employee))
setStats(incrementInvitedStats)
return result
}, [])
@@ -78,9 +114,7 @@ export function useTeam() {
method: 'PATCH',
body: JSON.stringify({ role }),
})
setMembers((prev) =>
prev.map((m) => (m.id === memberId ? { ...m, role: updated.role } : m))
)
setMembers((prev) => applyMemberRoleUpdate(prev, memberId, updated.role as 'MANAGER' | 'AGENT'))
return updated
}, [])
@@ -88,10 +122,8 @@ export function useTeam() {
const updated = await apiFetch<TeamMember>(`/team/${memberId}/deactivate`, {
method: 'POST',
})
setMembers((prev) =>
prev.map((m) => (m.id === memberId ? { ...m, isActive: false } : m))
)
setStats((prev) => ({ ...prev, active: prev.active - 1, inactive: prev.inactive + 1 }))
setMembers((prev) => applyMemberActivation(prev, memberId, false))
setStats((prev) => applyActivationStats(prev, 'deactivate'))
return updated
}, [])
@@ -99,10 +131,8 @@ export function useTeam() {
const updated = await apiFetch<TeamMember>(`/team/${memberId}/reactivate`, {
method: 'POST',
})
setMembers((prev) =>
prev.map((m) => (m.id === memberId ? { ...m, isActive: true } : m))
)
setStats((prev) => ({ ...prev, active: prev.active + 1, inactive: prev.inactive - 1 }))
setMembers((prev) => applyMemberActivation(prev, memberId, true))
setStats((prev) => applyActivationStats(prev, 'reactivate'))
return updated
}, [])
@@ -110,18 +140,10 @@ export function useTeam() {
await apiFetch<{ success: boolean }>(`/team/${memberId}`, {
method: 'DELETE',
})
const target = members.find((m) => m.id === memberId)
setMembers((prev) => prev.filter((m) => m.id !== memberId))
setStats((prev) => ({
...prev,
total: prev.total - 1,
active: target?.isActive ? prev.active - 1 : prev.active,
pending: target?.invitationStatus === 'pending' ? prev.pending - 1 : prev.pending,
inactive: !target?.isActive && target?.invitationStatus === 'accepted'
? prev.inactive - 1
: prev.inactive,
}))
}, [members])
const nextState = removeTeamMemberState(members, stats, memberId)
setMembers(nextState.members)
setStats(nextState.stats)
}, [members, stats])
return {
members,