> $agreements * @return list> */ public function rows(array $agreements, string $audience = ReportAudience::INTERNAL): array { ReportAudience::validate($audience); $filters = $this->filters ?? new ReportFilters(); $mapper = $this->mapper ?? new ReportDataMapper(); $rows = []; foreach ($agreements as $agreement) { if (!$filters->matches($agreement)) continue; $allocated = max(0.0, (float)($agreement['allocated_hours'] ?? 0)); $used = 0.0; foreach ((array)($agreement['hours'] ?? []) as $hours) $used += max(0.0, (float)$hours); $used = round($used, 2); $remaining = round(max(0.0, $allocated - $used), 2); $percentage = $allocated > 0 ? round(($used / $allocated) * 100, 2) : ($used > 0 ? 100.0 : 0.0); $status = $used > $allocated ? 'exceeded' : ($percentage >= 90 ? 'critical' : ($percentage >= 75 ? 'warning' : 'within_limit')); $record = [ 'client_id' => (int)($agreement['client_id'] ?? 0), 'client_name' => (string)($agreement['client_name'] ?? ''), 'allocated_hours' => $allocated, 'used_hours' => $used, 'remaining_hours' => $remaining, 'usage_percentage' => $percentage, 'status' => $status, ]; $rows[] = $audience === ReportAudience::CLIENT ? $mapper->clientSla($record) : $mapper->internalSla($record); } usort($rows, static fn(array $a, array $b): int => strcmp((string)$a['client_name'], (string)$b['client_name']) ?: ((int)$a['client_id'] <=> (int)$b['client_id'])); return $rows; } public function build(array $agreements, string $audience = ReportAudience::INTERNAL): array { return $this->rows($agreements, $audience); } public function query(array $agreements, string $audience = ReportAudience::INTERNAL): array { return $this->rows($agreements, $audience); } }