feat(allocation): implement resource allocation feature

- Add AllocationController with CRUD + bulk endpoints
- Add AllocationValidationService for capacity/estimate validation
- Add AllocationMatrixService for optimized matrix queries
- Add AllocationPolicy for authorization
- Add AllocationResource for API responses
- Add frontend allocationService and matrix UI
- Add E2E tests for allocation matrix (20 tests)
- Add unit tests for validation service and policies
- Fix month format conversion (YYYY-MM to YYYY-MM-01)
This commit is contained in:
2026-02-25 16:28:47 -05:00
parent fedfc21425
commit 3324c4f156
35 changed files with 3337 additions and 67 deletions

View File

@@ -0,0 +1,92 @@
/**
* Allocation Service
*
* API operations for resource allocation management.
*/
import { api } from './api';
export interface Allocation {
id: string;
project_id: string;
team_member_id: string;
month: string;
allocated_hours: string;
created_at: string;
updated_at: string;
project?: {
id: string;
code: string;
title: string;
};
team_member?: {
id: string;
name: string;
};
}
export interface CreateAllocationRequest {
project_id: string;
team_member_id: string;
month: string;
allocated_hours: number;
}
export interface UpdateAllocationRequest {
allocated_hours: number;
}
export interface BulkAllocationRequest {
allocations: CreateAllocationRequest[];
}
// Allocation API methods
export const allocationService = {
/**
* Get all allocations, optionally filtered by month
*/
getAll: (month?: string) => {
const query = month ? `?month=${month}` : '';
return api.get<Allocation[]>(`/allocations${query}`);
},
/**
* Get a single allocation by ID
*/
getById: (id: string) =>
api.get<Allocation>(`/allocations/${id}`),
/**
* Create a new allocation
*/
create: (data: CreateAllocationRequest) =>
api.post<Allocation>('/allocations', data),
/**
* Update an existing allocation
*/
update: (id: string, data: UpdateAllocationRequest) =>
api.put<Allocation>(`/allocations/${id}`, data),
/**
* Delete an allocation
*/
delete: (id: string) =>
api.delete<{ message: string }>(`/allocations/${id}`),
/**
* Bulk create allocations
*/
bulkCreate: (data: BulkAllocationRequest) =>
api.post<Allocation[]>('/allocations/bulk', data),
};
/**
* Format allocated hours
*/
export function formatAllocatedHours(hours: string | number): string {
const numHours = typeof hours === 'string' ? parseFloat(hours) : hours;
return `${numHours}h`;
}
export default allocationService;

View File

@@ -144,7 +144,15 @@ interface ApiRequestOptions {
// Main API request function
export async function apiRequest<T>(endpoint: string, options: ApiRequestOptions = {}): Promise<T> {
const url = `${API_BASE_URL}${endpoint}`;
// Ensure we have an absolute URL for server-side rendering
let url = endpoint;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
// Get the base URL - works in both browser and server contexts
const baseUrl = typeof window !== 'undefined'
? ''
: process.env['ORIGIN'] || '';
url = `${baseUrl}${API_BASE_URL}${endpoint}`;
}
// Prepare headers
const headers: Record<string, string> = {