Deploy Ladill Meet / deploy (push) Failing after 7s
Phases 0–18: core meetings, webinar, breakouts, team chat, live streaming, town hall, billing, and Ladill Mail calendar wiring. Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
9.4 KiB
PHP
248 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Meet;
|
|
|
|
use App\Models\Invitation;
|
|
use App\Models\Member;
|
|
use App\Models\Participant;
|
|
use App\Models\Recording;
|
|
use App\Models\Room;
|
|
use App\Models\Session;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class ReportService
|
|
{
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
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<string, mixed>
|
|
*/
|
|
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<string, mixed>
|
|
*/
|
|
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<string, mixed>
|
|
*/
|
|
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<string, mixed>
|
|
*/
|
|
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<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $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<int, array<string, mixed>> $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<int, array<int, string>> $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));
|
|
}
|
|
}
|