*/ public function summary(string $ownerRef, Organization $organization, Carbon $from, Carbon $to, ?int $branchId = null): array { $visits = $this->visitQuery($ownerRef, $organization->id, $from, $to, $branchId); $checkedIn = (clone $visits)->whereNotNull('checked_in_at'); $durations = (clone $checkedIn) ->whereNotNull('checked_out_at') ->get(['checked_in_at', 'checked_out_at']) ->map(fn (Visit $v) => $v->checked_in_at->diffInMinutes($v->checked_out_at)); return [ 'total_visits' => (clone $visits)->count(), 'checked_in' => (clone $checkedIn)->count(), 'checked_out' => (clone $visits)->where('status', Visit::STATUS_CHECKED_OUT)->count(), 'cancelled' => (clone $visits)->where('status', Visit::STATUS_CANCELLED)->count(), 'contractors' => (clone $checkedIn)->where('visitor_type', 'contractor')->count(), 'deliveries' => (clone $checkedIn)->where('visitor_type', 'delivery')->count(), 'avg_duration_minutes' => $durations->isEmpty() ? 0 : (int) round($durations->avg()), 'unique_visitors' => (clone $checkedIn)->distinct('visitor_id')->count('visitor_id'), ]; } /** @return array */ public function peakHours(string $ownerRef, Organization $organization, Carbon $from, Carbon $to, ?int $branchId = null): array { $counts = array_fill(0, 24, 0); $this->visitQuery($ownerRef, $organization->id, $from, $to, $branchId) ->whereNotNull('checked_in_at') ->pluck('checked_in_at') ->each(function ($checkedInAt) use (&$counts) { $counts[(int) Carbon::parse($checkedInAt)->format('G')]++; }); $hours = []; for ($h = 0; $h < 24; $h++) { $hours[] = ['hour' => $h, 'count' => $counts[$h]]; } return $hours; } /** @return Collection */ public function visitsByDepartment(string $ownerRef, Organization $organization, Carbon $from, Carbon $to, ?int $branchId = null): Collection { return $this->visitQuery($ownerRef, $organization->id, $from, $to, $branchId) ->whereNotNull('frontdesk_visits.checked_in_at') ->join('frontdesk_hosts', 'frontdesk_visits.host_id', '=', 'frontdesk_hosts.id') ->selectRaw("coalesce(frontdesk_hosts.department, 'Unassigned') as department, count(*) as total") ->groupBy('department') ->orderByDesc('total') ->get() ->map(fn ($row) => (object) ['department' => $row->department, 'count' => (int) $row->total]); } /** @return Collection */ public function frequentVisitors(string $ownerRef, Organization $organization, int $limit = 10): Collection { return Visitor::owned($ownerRef) ->where('organization_id', $organization->id) ->where('is_frequent', true) ->orderByDesc('visit_count') ->limit($limit) ->get(); } /** @return array */ public function securityIncidents(string $ownerRef, Organization $organization, Carbon $from, Carbon $to): array { $query = AuditLog::owned($ownerRef) ->where('organization_id', $organization->id) ->whereBetween('created_at', [$from, $to]) ->whereIn('action', [ 'watchlist.blocked_attempt', 'watchlist.flagged_checkin', 'badge.expired', ]); return [ 'blocked_attempts' => (clone $query)->where('action', 'watchlist.blocked_attempt')->count(), 'flagged_checkins' => (clone $query)->where('action', 'watchlist.flagged_checkin')->count(), 'expired_badges' => (clone $query)->where('action', 'badge.expired')->count(), ]; } /** @return array */ public function dailyCounts(string $ownerRef, Organization $organization, Carbon $from, Carbon $to, ?int $branchId = null): array { $rows = []; $this->visitQuery($ownerRef, $organization->id, $from, $to, $branchId) ->whereNotNull('checked_in_at') ->pluck('checked_in_at') ->each(function ($checkedInAt) use (&$rows) { $key = Carbon::parse($checkedInAt)->toDateString(); $rows[$key] = ($rows[$key] ?? 0) + 1; }); $days = []; for ($date = $from->copy()->startOfDay(); $date->lte($to); $date->addDay()) { $key = $date->toDateString(); $days[] = ['date' => $key, 'count' => (int) ($rows[$key] ?? 0)]; } return $days; } protected function visitQuery(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId): Builder { return Visit::query() ->from('frontdesk_visits') ->where('frontdesk_visits.owner_ref', $ownerRef) ->where('frontdesk_visits.organization_id', $organizationId) ->when($branchId, fn ($q) => $q->where('frontdesk_visits.branch_id', $branchId)) ->where(function ($q) use ($from, $to) { $q->whereBetween('frontdesk_visits.checked_in_at', [$from, $to]) ->orWhereBetween('frontdesk_visits.scheduled_at', [$from, $to]); }); } }