fix(capacity): Fix three defects - caching, save, filters
1. Slow Team Member Dropdown - Fixed - Added cached team members store with 5-minute TTL - Dropdown now loads instantly on subsequent visits 2. Error Preventing Capacity Save - Fixed - Added saveAvailability API endpoint - Added backend service method to persist availability overrides - Added proper error handling and success feedback - Cache invalidation on save 3. Filters Not Working - Fixed - Fixed PTOManager to use shared selectedMemberId - Filters now react to team member selection Test Results: - Backend: 76 passed ✅ - Frontend Unit: 10 passed ✅ - E2E: 130 passed, 24 skipped ✅ Refs: openspec/changes/headroom-foundation
This commit is contained in:
@@ -6,10 +6,12 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CapacityResource;
|
||||
use App\Http\Resources\RevenueResource;
|
||||
use App\Http\Resources\TeamCapacityResource;
|
||||
use App\Http\Resources\TeamMemberAvailabilityResource;
|
||||
use App\Models\TeamMember;
|
||||
use App\Services\CapacityService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CapacityController extends Controller
|
||||
{
|
||||
@@ -157,4 +159,40 @@ class CapacityController extends Controller
|
||||
'member_revenues' => $memberRevenues,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Team Member Availability
|
||||
*
|
||||
* Persist a daily availability override and refresh cached capacity totals.
|
||||
*
|
||||
* @group Capacity Planning
|
||||
*
|
||||
* @bodyParam team_member_id string required The team member UUID.
|
||||
* @bodyParam date string required The date for the availability override (YYYY-MM-DD).
|
||||
* @bodyParam availability numeric required The availability value (0, 0.5, 1.0).
|
||||
*
|
||||
* @response 201 {
|
||||
* "data": {
|
||||
* "team_member_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
* "date": "2026-02-03",
|
||||
* "availability": 0.5
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function saveAvailability(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'team_member_id' => 'required|exists:team_members,id',
|
||||
'date' => 'required|date_format:Y-m-d',
|
||||
'availability' => ['required', 'numeric', Rule::in([0, 0.5, 1])],
|
||||
]);
|
||||
|
||||
$entry = $this->capacityService->upsertTeamMemberAvailability(
|
||||
$data['team_member_id'],
|
||||
$data['date'],
|
||||
(float) $data['availability']
|
||||
);
|
||||
|
||||
return $this->wrapResource(new TeamMemberAvailabilityResource($entry), 201);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\TeamMemberAvailability;
|
||||
|
||||
class TeamMemberAvailabilityResource extends BaseResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
/** @var TeamMemberAvailability $availability */
|
||||
$availability = $this->resource;
|
||||
|
||||
return [
|
||||
'team_member_id' => $availability->team_member_id,
|
||||
'date' => $availability->date?->toDateString(),
|
||||
'availability' => (float) $availability->availability,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -273,6 +273,21 @@ class CapacityService
|
||||
->mapWithKeys(fn (TeamMemberAvailability $entry) => [$entry->date->toDateString() => (float) $entry->availability]);
|
||||
}
|
||||
|
||||
public function upsertTeamMemberAvailability(string $teamMemberId, string $date, float $availability): TeamMemberAvailability
|
||||
{
|
||||
$entry = TeamMemberAvailability::updateOrCreate(
|
||||
['team_member_id' => $teamMemberId, 'date' => $date],
|
||||
['availability' => $availability]
|
||||
);
|
||||
|
||||
$month = Carbon::createFromFormat('Y-m-d', $date)->format('Y-m');
|
||||
|
||||
$this->forgetCapacityCacheForTeamMember($teamMemberId, [$month]);
|
||||
$this->forgetCapacityCacheForMonth($month);
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CarbonPeriod for the given month.
|
||||
*/
|
||||
|
||||
@@ -45,6 +45,7 @@ Route::middleware(JwtAuth::class)->group(function () {
|
||||
Route::get('/capacity', [CapacityController::class, 'individual']);
|
||||
Route::get('/capacity/team', [CapacityController::class, 'team']);
|
||||
Route::get('/capacity/revenue', [CapacityController::class, 'revenue']);
|
||||
Route::post('/capacity/availability', [CapacityController::class, 'saveAvailability']);
|
||||
|
||||
// Holidays
|
||||
Route::get('/holidays', [HolidayController::class, 'index']);
|
||||
|
||||
@@ -149,6 +149,31 @@ test('4.1.16 GET /api/capacity/revenue calculates possible revenue', function ()
|
||||
expect(round($response->json('data.possible_revenue'), 2))->toBe(round($expectedRevenue, 2));
|
||||
});
|
||||
|
||||
test('4.1.25 POST /api/capacity/availability saves entry', function () {
|
||||
$token = loginAsManager($this);
|
||||
$role = Role::factory()->create();
|
||||
$member = TeamMember::factory()->create(['role_id' => $role->id]);
|
||||
|
||||
$payload = [
|
||||
'team_member_id' => $member->id,
|
||||
'date' => '2026-02-03',
|
||||
'availability' => 0.5,
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/capacity/availability', $payload, [
|
||||
'Authorization' => "Bearer {$token}",
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJsonPath('data.date', '2026-02-03');
|
||||
|
||||
assertDatabaseHas('team_member_daily_availabilities', [
|
||||
'team_member_id' => $member->id,
|
||||
'date' => '2026-02-03 00:00:00',
|
||||
'availability' => 0.5,
|
||||
]);
|
||||
});
|
||||
|
||||
test('4.1.17 POST /api/holidays creates holiday', function () {
|
||||
$token = loginAsManager($this);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user