*/ public function orgSummary(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array { $rooms = $this->roomQuery($ownerRef, $organizationId, $branchId) ->whereBetween('scheduled_at', [$from, $to]); $sessions = Session::owned($ownerRef) ->whereHas('room', fn ($q) => $q->where('organization_id', $organizationId) ->when($branchId, fn ($q2) => $q2->where('branch_id', $branchId))) ->whereBetween('started_at', [$from, $to]); $recordings = Recording::owned($ownerRef) ->whereHas('session.room', fn ($q) => $q->where('organization_id', $organizationId) ->when($branchId, fn ($q2) => $q2->where('branch_id', $branchId))) ->whereBetween('created_at', [$from, $to]); $totalMinutes = (clone $sessions)->whereNotNull('ended_at')->get()->sum(function (Session $s) { return $s->started_at && $s->ended_at ? (int) $s->started_at->diffInMinutes($s->ended_at) : 0; }); return [ 'meetings_scheduled' => (clone $rooms)->count(), 'meetings_completed' => (clone $rooms)->where('status', 'ended')->count(), 'meetings_cancelled' => (clone $rooms)->where('status', 'cancelled')->count(), 'sessions_held' => (clone $sessions)->count(), 'total_minutes' => $totalMinutes, 'peak_participants' => (clone $sessions)->max('peak_participants') ?? 0, 'recordings_count' => (clone $recordings)->count(), 'recording_storage_bytes' => (int) (clone $recordings)->where('status', 'ready')->sum('file_size'), 'active_members' => Member::owned($ownerRef)->where('organization_id', $organizationId)->count(), ]; } /** * @return array */ public function meetingsReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array { $rooms = $this->roomQuery($ownerRef, $organizationId, $branchId) ->whereBetween('scheduled_at', [$from, $to]) ->with(['sessions']) ->orderBy('scheduled_at') ->get(); $rows = $rooms->map(fn (Room $room) => [ 'title' => $room->title, 'type' => $room->type, 'status' => $room->status, 'scheduled_at' => $room->scheduled_at?->toIso8601String(), 'duration_minutes' => $room->duration_minutes, 'sessions' => $room->sessions->count(), 'peak_participants' => $room->sessions->max('peak_participants') ?? 0, 'invitations' => $room->invitations()->count(), 'accepted' => $room->invitations()->where('status', 'accepted')->count(), ]); return [ 'total' => $rows->count(), 'by_type' => $rows->groupBy('type')->map->count(), 'rows' => $rows, ]; } /** * @return array */ public function usersReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array { $participants = Participant::owned($ownerRef) ->whereHas('session.room', fn ($q) => $q->where('organization_id', $organizationId) ->when($branchId, fn ($q2) => $q2->where('branch_id', $branchId))) ->whereBetween('joined_at', [$from, $to]) ->get(); $hosts = Room::owned($ownerRef) ->where('organization_id', $organizationId) ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) ->whereBetween('scheduled_at', [$from, $to]) ->select('host_user_ref') ->distinct() ->count('host_user_ref'); $byUser = $participants->groupBy('user_ref')->map(function (Collection $group, ?string $userRef) { $minutes = $group->sum(fn (Participant $p) => $p->joined_at && $p->left_at ? (int) $p->joined_at->diffInMinutes($p->left_at) : ($p->joined_at ? (int) $p->joined_at->diffInMinutes(now()) : 0)); return [ 'user_ref' => $userRef, 'display_name' => $group->first()->display_name, 'meetings_joined' => $group->count(), 'minutes' => $minutes, 'hosted' => $group->where('role', 'host')->count(), ]; })->sortByDesc('minutes')->values(); return [ 'unique_participants' => $participants->pluck('user_ref')->filter()->unique()->count(), 'unique_hosts' => $hosts, 'top_users' => $byUser->take(20), ]; } /** * @return array */ public function invitationsReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array { $invites = Invitation::owned($ownerRef) ->whereHas('room', fn ($q) => $q->where('organization_id', $organizationId) ->when($branchId, fn ($q2) => $q2->where('branch_id', $branchId))) ->whereBetween('created_at', [$from, $to]) ->get(); $total = $invites->count(); return [ 'total' => $total, 'pending' => $invites->where('status', 'pending')->count(), 'accepted' => $invites->where('status', 'accepted')->count(), 'declined' => $invites->where('status', 'declined')->count(), 'acceptance_rate' => $total > 0 ? round($invites->where('status', 'accepted')->count() / $total * 100, 1) : 0, ]; } /** * @return array */ public function branchRollup(string $ownerRef, int $organizationId, Carbon $from, Carbon $to): array { return \App\Models\Branch::owned($ownerRef) ->where('organization_id', $organizationId) ->where('is_active', true) ->orderBy('name') ->get() ->map(fn ($branch) => array_merge( ['branch_id' => $branch->id, 'branch_name' => $branch->name], $this->orgSummary($ownerRef, $organizationId, $from, $to, $branch->id), )) ->all(); } /** * @param array $data */ public function toCsv(string $type, array $data): string { return match ($type) { 'meetings' => $this->meetingsCsv($data), 'users' => $this->usersCsv($data), 'summary' => $this->summaryCsv($data), 'branches' => $this->branchesCsv($data), default => '', }; } /** @param array $data */ protected function meetingsCsv(array $data): string { $rows = [['Title', 'Type', 'Status', 'Scheduled', 'Duration', 'Sessions', 'Peak', 'Invites', 'Accepted']]; foreach ($data['rows'] ?? [] as $row) { $rows[] = [ $row['title'], $row['type'], $row['status'], $row['scheduled_at'] ?? '', (string) ($row['duration_minutes'] ?? ''), (string) $row['sessions'], (string) $row['peak_participants'], (string) $row['invitations'], (string) $row['accepted'], ]; } return $this->encodeCsv($rows); } /** @param array $data */ protected function usersCsv(array $data): string { $rows = [['Name', 'User ref', 'Meetings joined', 'Minutes', 'Hosted']]; foreach ($data['top_users'] ?? [] as $row) { $rows[] = [ $row['display_name'], $row['user_ref'] ?? '', (string) $row['meetings_joined'], (string) $row['minutes'], (string) $row['hosted'], ]; } return $this->encodeCsv($rows); } /** @param array $data */ protected function summaryCsv(array $data): string { $rows = [['Metric', 'Value']]; foreach ($data as $key => $value) { if (is_array($value)) { continue; } $rows[] = [$key, (string) $value]; } return $this->encodeCsv($rows); } /** @param array> $data */ protected function branchesCsv(array $data): string { $rows = [['Branch', 'Scheduled', 'Completed', 'Sessions', 'Minutes', 'Recordings']]; foreach ($data as $row) { $rows[] = [ $row['branch_name'], (string) $row['meetings_scheduled'], (string) $row['meetings_completed'], (string) $row['sessions_held'], (string) $row['total_minutes'], (string) $row['recordings_count'], ]; } return $this->encodeCsv($rows); } /** @param array> $rows */ protected function encodeCsv(array $rows): string { return collect($rows)->map(fn ($r) => implode(',', array_map( fn ($c) => '"'.str_replace('"', '""', (string) $c).'"', $r, )))->implode("\n"); } protected function roomQuery(string $ownerRef, int $organizationId, ?int $branchId) { return Room::owned($ownerRef) ->where('organization_id', $organizationId) ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)); } }