@extends('admin.layout') @section('title', 'Kunjungan') @section('subtitle', 'Berapa yang membuka aplikasi. Satu perangkat dihitung sekali per halaman tiap jam, waktu Asia/Jakarta') @section('content') {{-- Filter periode di atas, supaya jelas semua angka di bawah mengikutinya. --}}
@include('admin.partials.period-picker', [ 'start' => $start, 'end' => $end, 'target' => route('admin.analytics'), ]) {{-- Satuan grafik. Periode yang sedang aktif ikut dibawa, jadi menukar satuan tidak mengubah rentangnya. --}}
Show @foreach (['hari' => 'Hari', 'jam' => 'Jam'] as $opt => $text) {{ $text }} @endforeach
Kunjungan pada periode ini
maks. 1 per perangkat/halaman tiap jam
{{ number_format($totalViews) }}
Perangkat
dijumlahkan dari hitungan tiap hari
{{ number_format($totalVisitors) }}
Rentang
{{ $start->translatedFormat('d M Y') }} – {{ $end->translatedFormat('d M Y') }}
{{ $days }} hari
{{-- Kunjungan lama tidak menyimpan jam maupun peran. Disebutkan terang-terangan supaya grafik yang lebih rendah dari total periode tidak dibaca sebagai data yang hilang. --}} @if ($viewsWithoutRole > 0 || $viewsWithoutHour > 0)
@if ($viewsWithoutRole > 0)
{{ number_format($viewsWithoutRole) }} kunjungan pada periode ini dicatat sebelum pencatatan per peran aktif, jadi tidak diketahui pengguna, mitra, atau tamu — dan tidak masuk garis mana pun.
@endif @if ($viewsWithoutHour > 0)
{{ number_format($viewsWithoutHour) }} kunjungan dicatat sebelum pencatatan per jam aktif, jadi jamnya tidak diketahui dan tidak masuk profil jam.
@endif
Kartu “Kunjungan pada periode ini” tetap menghitung semuanya.
@endif {{-- Grafik kunjungan, satu garis per peran. Digambar sebagai SVG langsung dari data — tanpa pustaka chart, supaya CMS tidak menarik JS dari luar. "Perangkat" tidak digambar di sini: enam garis jadi tak terbaca, angkanya ada di tabel di bawah. --}} @php $w = 760; $h = 240; // kanvas $padL = 44; $padR = 16; $padT = 16; $padB = 28; $plotW = $w - $padL - $padR; $plotH = $h - $padT - $padB; // Tamu digambar samar dan putus-putus: jumlahnya biasanya paling besar, // dan yang dicari di grafik ini biasanya pengguna vs mitra. $lines = [ 'customer' => ['color' => '#2563EB', 'dash' => null], 'mitra' => ['color' => '#EA580C', 'dash' => null], 'guest' => ['color' => '#A8A29E', 'dash' => '5 4'], ]; $points = $series->values(); $peak = max(1, $points->max(fn ($p) => max($p['views']))); // Skala dibulatkan ke atas ke angka bulat supaya label sumbunya rapi. $step = max(1, (int) ceil($peak / 4)); $topY = $step * 4; $n = max(1, $points->count() - 1); $xAt = fn ($i) => $padL + ($n === 0 ? $plotW / 2 : $i * $plotW / $n); $yAt = fn ($v) => $padT + $plotH - ($v / $topY * $plotH); $path = fn (string $role) => $points ->map(fn ($p, $i) => ($i ? 'L' : 'M').round($xAt($i), 1).' '.round($yAt($p['views'][$role]), 1)) ->implode(' '); // Label tanggal dibatasi ~7 supaya tidak bertumpuk di rentang panjang. $labelEvery = max(1, (int) ceil($points->count() / 7)); // Titik-titik yang dipakai lapisan sorot di JS, sudah dalam satuan // viewBox supaya JS tidak perlu tahu skalanya. $chartJson = $points->map(fn ($p, $i) => [ 'label' => $p['label'], 'total' => $p['total'], // Alamat rincian titik ini: per tanggal pada satuan hari, per jam // pada profil jam. Keduanya membuka popup yang sama. 'detail' => $p['date'] ? route('admin.analytics.day', ['date' => $p['date']]) : route('admin.analytics.hour', ['hour' => (int) $p['tick'], 'start' => $start->toDateString(), 'end' => $end->toDateString()]), 'views' => $p['views'], 'x' => round($xAt($i), 1), 'y' => collect($lines)->map(fn ($_, $role) => round($yAt($p['views'][$role]), 1))->all(), ])->values()->toJson(); $rolesJson = collect($lines) ->map(fn ($line, $role) => ['name' => $roles[$role], 'color' => $line['color']]) ->toJson(); @endphp
{{ $show === 'jam' ? 'Kunjungan per jam' : 'Tren kunjungan harian' }} · {{ $show === 'jam' ? 'seluruh hari pada periode ini dijumlahkan per jam' : 'satu titik per hari' }} klik grafik untuk rincian
{{-- Legenda: identitas tidak pernah lewat warna saja — namanya ditulis, dan garis tamu juga dibedakan lewat pola putus-putus. --}}
@foreach ($lines as $role => $line) {{ $roles[$role] }} {{ number_format($roleTotals[$role]['views']) }} @endforeach
{{-- Grid & label sumbu Y, sengaja tipis agar tidak bersaing dengan data. --}} @for ($i = 0; $i <= 4; $i++) @php($gv = $topY / 4 * $i) {{ (int) $gv }} @endfor {{-- Label sumbu X: tanggal, atau jam kalau satuannya jam. --}} @foreach ($points as $i => $p) @if ($i % $labelEvery === 0 || $i === $points->count() - 1) {{ $p['tick'] }} @endif @endforeach {{-- Garis tiap peran. Tamu digambar lebih dulu supaya pengguna dan mitra — yang lebih sering dibaca — ada di atasnya. --}} @foreach (array_reverse($lines, true) as $role => $line) @endforeach {{-- Penanda titik terakhir, jadi ujung tiap garis tetap jelas tanpa perlu menunjuk apa pun. --}} @php($last = $points->last()) @php($lx = round($xAt($points->count() - 1), 1)) @foreach ($lines as $role => $line) @endforeach {{-- Lapisan sorot: garis bantu + titik, digerakkan oleh JS. --}} @foreach ($lines as $role => $line) @endforeach {{-- Tooltip mengikuti kursor. --}}
{{-- Rincian mengikuti satuan grafik: per jam kalau "Jam" dipilih, dan ikut dipisah per peran seperti grafiknya. Dibaca dari $series yang sama, jadi tabel dan grafik tidak mungkin berbeda angka. --}}
{{ $show === 'jam' ? 'Per jam' : 'Per hari' }}
@foreach ($roles as $name) @endforeach {{-- Jam urut maju (00→23); hari urut mundur supaya yang terbaru ada di atas, seperti sebelumnya. Ditulis sebagai @php(...) sebaris, bukan blok @php: satu berkas Blade tidak boleh mencampur keduanya kalau yang sebaris muncul lebih dulu — blok raw-nya akan menelan semua yang ada di antaranya. --}} @php($rows = $show === 'jam' ? $series : $series->reverse()) @forelse ($rows as $row) @foreach ($roles as $role => $name) @endforeach @empty @endforelse
{{ $show === 'jam' ? 'Jam' : 'Tanggal' }}{{ $name }}Total Perangkat
@if ($row['total']) {{-- Baris yang ada kunjungannya bisa dibuka untuk melihat riwayat aksesnya. Baris kosong tidak dibuat bisa diklik: tidak ada isinya. --}} @else {{ $row['label'] }} @endif {{ $row['views'][$role] ? number_format($row['views'][$role]) : '–' }} {{ $row['total'] ? number_format($row['total']) : '–' }} {{ $row['visitors'] ? number_format($row['visitors']) : '–' }}
Belum ada data kunjungan.
{{-- Per halaman --}}
Per halaman (periode terpilih)
@php($max = $pages->max('views') ?: 1) @forelse ($pages as $row) @empty @endforelse
Halaman Kunjungan Perangkat
{{ $row->page }}
{{-- Bar proporsional terhadap halaman terpopuler. --}}
{{ number_format($row->views) }} {{ number_format($row->visitors) }}
Belum ada data kunjungan.
{{-- Matriks hari × halaman --}} @if ($pages->isNotEmpty())
Rincian hari × halaman
@foreach ($pages as $p) @endforeach @foreach ($daily as $row) @php($key = $row->date->toDateString()) @foreach ($pages as $p) @php($n = (int) ($matrix[$key][$p->page] ?? 0)) @endforeach @endforeach
Tanggal{{ $p->page }}
{{ $row->date->format('d M') }}{{ $n ?: '–' }}
@endif {{-- Popup rincian tanggal. Isinya diminta ke server saat dibuka, bukan dirender ikut halaman: memuat data pribadi, jadi hanya ditarik kalau admin memang membukanya. --}} @endsection