@extends('layouts.app') @section('header')
Monitoring

People Screenshots

@if ($summary)
{{ $summary['shots'] }} screenshots
@endif
@endsection @section('content') @php $fmtDur = fn ($sec) => \App\Support\Duration::compact((int) $sec); @endphp {{-- ── Filter bar: ‹ date › · employee · timezone · group by ─────────── --}}
{{-- Display only. The real value rides in the hidden input below, so the arrows, the picker and a page whose JS never ran all submit the same ?date=. --}}
{{-- Narrows the employee list below. Changing it clears a selected user who no longer qualifies — the controller drops any id that isn't in the filtered set. --}}
{{-- Upgraded to a searchable Tom Select below; the inline onchange stays so the plain @foreach ($employees as $id => $name) @endforeach
@if ($window && ! $window['start']->isStartOfDay()) {{-- Only worth saying when the day is NOT midnight-to-midnight: otherwise it just restates the date already in the picker. --}}
{{ \App\Support\ReportingDay::label($dayMode) }} — {{ $window['start']->format('D d M, H:i') }} to {{ $window['end']->format('D d M, H:i') }} @if ($shift) (24h, from this employee's {{ \Illuminate\Support\Str::of($shift['start'] ?? '')->substr(0, 5) }}–{{ \Illuminate\Support\Str::of($shift['end'])->substr(0, 5) }} shift) @endif
@endif
@if ($summary) {{-- ── Day summary strip (from time_intervals) ───────────────────── --}}
@php $cards = [ ['icon' => 'ti-login-2', 'color' => 'purple', 'value' => $summary['checkIn'] ?? '—', 'label' => 'Check In'], ['icon' => 'ti-logout-2', 'color' => 'red', 'value' => $summary['checkOut'] ?? '—', 'label' => 'Check Out'], ['icon' => 'ti-clock-play', 'color' => 'green', 'value' => $fmtDur($summary['activeSec']), 'label' => 'Active Time'], ['icon' => 'ti-clock-pause', 'color' => 'gray', 'value' => $fmtDur($summary['idleSec']), 'label' => 'Idle / Breaks'], ['icon' => 'ti-photo', 'color' => 'blue', 'value' => $summary['shots'], 'label' => 'Screenshots'], ]; // Only when something was actually measured. A day recorded before // 1.15.0 has no input figure at all, and showing "0%" there would // assert the employee sat still when the truth is nobody looked. if (($summary['inputPercent'] ?? null) !== null) { $cards[] = [ 'icon' => 'ti-activity', 'color' => 'azure', 'value' => $summary['inputPercent'] . '%', 'label' => 'Avg. Input Activity', ]; } @endphp @foreach ($cards as $card)
{{ $card['value'] }}
{{ $card['label'] }}
@endforeach
@if ($capped)
Showing the first 1000 screenshots of this day.
@endif @php /** * The 5-minute window a capture belongs to — "07:30 – 07:35" rather than * "07:33:31". The agent takes one shot per 5-minute window at a random * offset inside it, so the exact second is an artefact of that jitter and * reads as false precision. Two shots in one window share a label, which * is correct: they are from the same window. */ $slot = function ($at) use ($tz) { if (! $at) return '—'; $t = $at->copy()->tz($tz); $from = $t->copy()->setTime($t->hour, intdiv($t->minute, 5) * 5, 0); return $from->format('H:i') . ' – ' . $from->copy()->addMinutes(5)->format('H:i'); }; /** * Every capture on the page, in the order it is rendered — the index * the lightbox carousel steps through, so one modal serves the whole * day rather than a modal per thumbnail. */ $ordered = $groupBy === 'task' ? $byGroup->flatMap(fn ($g) => $g)->values() : $segments->flatMap(fn ($seg) => $seg['shots'])->values(); $slideOf = []; foreach ($ordered as $i => $o) { $slideOf[$o->id] = $i; } $dur = fn ($from, $to) => \App\Support\Duration::compact((int) $from->diffInSeconds($to)); @endphp {{-- ── Screenshots: a timeline in hour mode, plain cards by task ────── --}} @if ($groupBy !== 'task') @forelse ($segments as $seg) @if ($loop->first)
@endif @empty

No screenshots this day

Nothing was captured for this employee on {{ $filters['date'] }}.

@endforelse @else {{-- Task mode stays a plain card per task: tasks aren't a timeline, and the same task recurs all day, so steps would imply an order that isn't there. --}} @forelse ($byGroup as $groupKey => $shots)

{{ $tasks[$groupKey] ?? 'Task #'.$groupKey }}

{{ $shots->count() }} shots
@foreach ($shots as $shot)
{{ $slot($shot->captured_at) }}
{{ $tasks[$shot->task_id] ?? 'Task #'.$shot->task_id }}
@endforeach
@empty

No screenshots this day

Nothing was captured for this employee on {{ $filters['date'] }}.

@endforelse @endif {{-- ── Lightbox ───────────────────────────────────────────────────── One modal for the whole day, not one per thumbnail. The grid has already downloaded every full-size file — the thumbnails ARE those files, scaled by CSS — so with the immutable cache header on screenshots.file the carousel re-renders them from cache and issues no second request. Slides carry data-src rather than src: a thousand elements with live sources would be a thousand decoded bitmaps in memory even though the bytes are cached. Only the visible slide and its neighbours are materialised. --}} @if ($ordered->isNotEmpty()) @endif @else {{-- Nothing loads until an employee is picked — that's the point. --}}

Please select a user to see screenshots

Pick an employee (and a date) above — screenshots load one person, one day at a time.

@endif @endsection @push('styles') {{-- Tabler themes both of these (litepicker + .ts-* live in this vendors bundle, which the base layout doesn't load), but it ships overrides only — Tom Select's base stylesheet still has to come from the CDN. Same pair of sources as Reports → Timesheet and Obvizio. --}} @include('partials.litepicker-theme') @endpush @push('scripts') @endpush