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:
2026-02-19 18:09:16 -05:00
parent c3ba83d101
commit d6b7215f93
9 changed files with 211 additions and 19 deletions

View File

@@ -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);
}
}

View File

@@ -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,
];
}
}

View File

@@ -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.
*/