feat(backend): enhance allocation and project management

Update controllers and services for allocation fidelity:
- AllocationController: variance data in responses, bulk operations
- ProjectController: include plan data in responses
- ProjectMonthPlanController: planning grid API
- AllocationMatrixService: support untracked allocations
- ProjectResource/TeamMemberResource: include reconciliation data

Improved test coverage for allocation flows.
This commit is contained in:
2026-03-08 18:22:53 -04:00
parent 9b38e28117
commit 9b0f42fdf5
14 changed files with 414 additions and 376 deletions

View File

@@ -7,6 +7,7 @@ use App\Models\Project;
use App\Models\Role;
use App\Models\TeamMember;
use App\Services\AllocationMatrixService;
use App\Services\VarianceCalculator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@@ -24,11 +25,11 @@ class AllocationCacheInvalidationTest extends TestCase
Allocation::factory()->create([
'project_id' => $project->id,
'team_member_id' => $teamMember->id,
'month' => '2026-02',
'month' => '2026-02-01',
'allocated_hours' => 40,
]);
$matrixService = new AllocationMatrixService;
$matrixService = new AllocationMatrixService(new VarianceCalculator);
$result = $matrixService->getMatrix('2026-02');
$this->assertArrayHasKey('allocations', $result);

View File

@@ -21,6 +21,19 @@ test('project resource includes expected fields inside data wrapper', function (
expect($payload['data'])->toHaveKey('approved_estimate');
});
test('project resource includes scalar type_id and status_id', function () {
$project = Project::factory()->approved()->create();
$project->load(['status', 'type']);
$response = (new ProjectResource($project))->toResponse(Request::create('/'));
$payload = $response->getData(true);
expect($payload['data'])->toHaveKey('type_id');
expect($payload['data'])->toHaveKey('status_id');
expect($payload['data']['type_id'])->toBe($project->type_id);
expect($payload['data']['status_id'])->toBe($project->status_id);
});
test('project resource collection wraps multiple entries', function () {
$projects = Project::factory()->count(2)->create();

View File

@@ -21,6 +21,18 @@ test('team member resource wraps data and includes role when loaded', function (
expect($payload['data']['role']['id'])->toBe($role->id);
});
test('team member resource includes scalar role_id', function () {
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create(['role_id' => $role->id]);
$teamMember->load('role');
$response = (new TeamMemberResource($teamMember))->toResponse(Request::create('/'));
$payload = $response->getData(true);
expect($payload['data'])->toHaveKey('role_id');
expect($payload['data']['role_id'])->toBe($teamMember->role_id);
});
test('team member resource collection keeps data wrapper', function () {
$role = Role::factory()->create();
$teamMembers = TeamMember::factory()->count(2)->create(['role_id' => $role->id]);