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:
96
frontend/src/lib/services/teamMemberService.ts
Normal file
96
frontend/src/lib/services/teamMemberService.ts
Normal 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;
|
||||
Reference in New Issue
Block a user