*/ public function getAll(?bool $active = null): Collection { /** @var Collection $teamMembers */ $teamMembers = $this->rememberTeamMembers( $this->buildTeamMembersCacheKey($active), now()->addHour(), function () use ($active): Collection { $query = TeamMember::with('role'); if ($active !== null) { $query->where('active', $active); } return $query->get(); } ); return $teamMembers; } /** * Find a team member by ID. */ public function findById(string $id): ?TeamMember { return TeamMember::with('role')->find($id); } /** * Create a new team member. * * @throws ValidationException */ public function create(array $data): TeamMember { $validator = Validator::make($data, [ 'name' => 'required|string|max:255', 'role_id' => 'required|integer|exists:roles,id', 'hourly_rate' => 'required|numeric|gt:0', 'active' => 'boolean', ], [ 'hourly_rate.gt' => 'Hourly rate must be greater than 0', ]); if ($validator->fails()) { throw new ValidationException($validator); } $teamMember = TeamMember::create([ 'name' => $data['name'], 'role_id' => $data['role_id'], 'hourly_rate' => $data['hourly_rate'], 'active' => $data['active'] ?? true, ]); $teamMember->load('role'); $this->forgetTeamMembersCache(); return $teamMember; } /** * Update an existing team member. * * @throws ValidationException */ public function update(TeamMember $teamMember, array $data): TeamMember { $validator = Validator::make($data, [ 'name' => 'sometimes|string|max:255', 'role_id' => 'sometimes|integer|exists:roles,id', 'hourly_rate' => 'sometimes|numeric|gt:0', 'active' => 'sometimes|boolean', ], [ 'hourly_rate.gt' => 'Hourly rate must be greater than 0', ]); if ($validator->fails()) { throw new ValidationException($validator); } $teamMember->update($data); $teamMember->load('role'); $this->forgetTeamMembersCache(); return $teamMember; } /** * Delete a team member. * * @throws \RuntimeException */ public function delete(TeamMember $teamMember): void { // Check if team member has allocations if ($teamMember->allocations()->exists()) { throw new \RuntimeException( 'Cannot delete team member with active allocations', 422 ); } // Check if team member has actuals if ($teamMember->actuals()->exists()) { throw new \RuntimeException( 'Cannot delete team member with historical data', 422 ); } $teamMember->delete(); $this->forgetTeamMembersCache(); } /** * Check if a team member can be deleted. * * @return array{canDelete: bool, reason?: string} */ public function canDelete(TeamMember $teamMember): array { if ($teamMember->allocations()->exists()) { return [ 'canDelete' => false, 'reason' => 'Team member has active allocations', ]; } if ($teamMember->actuals()->exists()) { return [ 'canDelete' => false, 'reason' => 'Team member has historical data', ]; } return ['canDelete' => true]; } private function buildTeamMembersCacheKey(?bool $active): string { if ($active === null) { return 'team-members:all'; } return $active ? 'team-members:active' : 'team-members:inactive'; } /** * @param Closure(): Collection $callback * @return Collection */ private function rememberTeamMembers(string $key, DateTimeInterface|int $ttl, Closure $callback): Collection { if (! $this->redisAvailable()) { /** @var Collection $payload */ $payload = Cache::store('array')->remember($key, $ttl, $callback); return $payload; } try { /** @var Collection $payload */ $payload = Cache::store('redis')->remember($key, $ttl, $callback); return $payload; } catch (Throwable) { /** @var Collection $payload */ $payload = Cache::store('array')->remember($key, $ttl, $callback); return $payload; } } private function forgetTeamMembersCache(): void { Cache::store('array')->forget($this->buildTeamMembersCacheKey(null)); Cache::store('array')->forget($this->buildTeamMembersCacheKey(true)); Cache::store('array')->forget($this->buildTeamMembersCacheKey(false)); if (! $this->redisAvailable()) { return; } try { Cache::store('redis')->forget($this->buildTeamMembersCacheKey(null)); Cache::store('redis')->forget($this->buildTeamMembersCacheKey(true)); Cache::store('redis')->forget($this->buildTeamMembersCacheKey(false)); } catch (Throwable) { // Ignore cache failures when Redis is unavailable. } } private function redisAvailable(): bool { if ($this->redisAvailable !== null) { return $this->redisAvailable; } if (! config('cache.stores.redis')) { return $this->redisAvailable = false; } $client = config('database.redis.client', 'phpredis'); if ($client === 'predis') { return $this->redisAvailable = class_exists('Predis\\Client'); } return $this->redisAvailable = extension_loaded('redis'); } }