feat(team-member): Complete Team Member Management capability

Implement full CRUD operations for team members with TDD approach:

Backend:
- TeamMemberController with REST API endpoints
- TeamMemberService for business logic extraction
- TeamMemberPolicy for authorization (superuser/manager access)
- 14 tests passing (8 API, 6 unit tests)

Frontend:
- Team member list with search and status filter
- Create/Edit modal with form validation
- Delete confirmation with constraint checking
- Currency formatting for hourly rates
- Real API integration with teamMemberService

Tests:
- E2E tests fixed with seed data helper
- All 157 tests passing (backend + frontend + E2E)

Closes #22
This commit is contained in:
2026-02-18 22:01:57 -05:00
parent 249e0ade8e
commit 3173d4250c
18 changed files with 1588 additions and 1100 deletions

View File

@@ -0,0 +1,96 @@
/**
* Team Member Service
*
* API operations for team member management.
*/
import { api } from './api';
export interface TeamMember {
id: string;
name: string;
role_id: number;
role?: {
id: number;
name: string;
};
hourly_rate: string;
active: boolean;
created_at: string;
updated_at: string;
}
export interface CreateTeamMemberRequest {
name: string;
role_id: number;
hourly_rate: number;
active?: boolean;
}
export interface UpdateTeamMemberRequest {
name?: string;
role_id?: number;
hourly_rate?: number;
active?: boolean;
}
export interface Role {
id: number;
name: string;
description?: string;
}
// Team member API methods
export const teamMemberService = {
/**
* Get all team members with optional active filter
*/
getAll: (active?: boolean) => {
const query = active !== undefined ? `?active=${active}` : '';
return api.get<TeamMember[]>(`/team-members${query}`);
},
/**
* Get a single team member by ID
*/
getById: (id: string) =>
api.get<TeamMember>(`/team-members/${id}`),
/**
* Create a new team member
*/
create: (data: CreateTeamMemberRequest) =>
api.post<TeamMember>('/team-members', data),
/**
* Update an existing team member
*/
update: (id: string, data: UpdateTeamMemberRequest) =>
api.put<TeamMember>(`/team-members/${id}`, data),
/**
* Delete a team member
*/
delete: (id: string) =>
api.delete<{ message: string }>(`/team-members/${id}`),
};
/**
* Format hourly rate as currency
*/
export function formatHourlyRate(rate: string | number): string {
const numRate = typeof rate === 'string' ? parseFloat(rate) : rate;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numRate);
}
/**
* Format hourly rate per hour (e.g., "$150.00/hr")
*/
export function formatHourlyRateWithUnit(rate: string | number): string {
return `${formatHourlyRate(rate)}/hr`;
}
export default teamMemberService;