@extends('layouts.app') @php $pageTitle = 'Devices'; $pageHeading = 'Device Management'; $pageDescription = 'Perangkat baru wajib di-approve lebih dulu sebelum dianggap aman untuk sinkronisasi.'; $tableThemeClass = 'ops-datatable-theme ops-datatable-theme--devices'; $sortBy = $filters['sort_by'] ?? 'last_activity'; $sortDir = $filters['sort_dir'] ?? 'desc'; $sortIcon = function (string $column) use ($sortBy, $sortDir): string { if ($sortBy !== $column) { return '↕'; } return $sortDir === 'asc' ? '↑' : '↓'; }; $sortUrl = function (string $column) use ($filters, $sortBy, $sortDir): string { $query = array_filter($filters, fn ($value) => $value !== null && $value !== '' && $value !== []); $query['sort_by'] = $column; $query['sort_dir'] = $sortBy === $column && $sortDir === 'asc' ? 'desc' : 'asc'; return route('devices.index', $query); }; $now = \Illuminate\Support\Carbon::now(); $formatTimeHealth = function ($value, string $emptyLabel, int $warnMinutes = 15, int $dangerMinutes = 60) use ($now) { if (empty($value)) { return [ 'display' => '-', 'relative' => $emptyLabel, 'style' => 'color: var(--muted);', 'warning' => null, ]; } $time = \Illuminate\Support\Carbon::parse($value); $diffMinutes = max(0, $time->diffInMinutes($now)); $display = $time->format('Y-m-d H:i:s'); if ($time->isSameDay($now) && $diffMinutes <= $warnMinutes) { return [ 'display' => $display, 'relative' => $time->diffForHumans($now, [ 'parts' => 2, 'short' => false, ]), 'style' => 'color: #17643d; font-weight: 800;', 'warning' => null, ]; } if ($time->isSameDay($now) && $diffMinutes <= $dangerMinutes) { return [ 'display' => $display, 'relative' => $time->diffForHumans($now, [ 'parts' => 2, 'short' => false, ]), 'style' => 'color: #9a6700; font-weight: 700;', 'warning' => 'Mulai telat, masih hari ini.', ]; } $warning = $time->isSameDay($now) ? 'Terlalu lama dari jam sekarang.' : 'Tanggal sudah tidak sama dengan hari ini.'; return [ 'display' => $display, 'relative' => $time->diffForHumans($now, [ 'parts' => 2, 'short' => false, ]), 'style' => 'color: #b42318; font-weight: 800;', 'warning' => $warning, ]; }; @endphp @section('content') @if (session('status'))
Status
{{ session('status') }}
@endif
Total {{ number_format($summary['total']) }} Approved {{ number_format($summary['approved']) }} Pending {{ number_format($summary['pending']) }} Online {{ number_format($summary['online']) }} Stale {{ number_format($summary['stale']) }} Offline {{ number_format($summary['offline']) }} Polling {{ number_format($summary['polling']) }} Data Sync {{ number_format($summary['data_sync']) }}
@if (!empty($bulkQuickMeta['active'])) Bulk paste aktif {{ (int) ($bulkQuickMeta['matched_tokens'] ?? 0) }} data cocok / {{ (int) ($bulkQuickMeta['unmatched_tokens'] ?? 0) }} tidak cocok @if (!empty($bulkQuickMeta['unmatched_list'])) Tidak cocok: {{ implode(', ', array_slice($bulkQuickMeta['unmatched_list'], 0, 12)) }}@if (count($bulkQuickMeta['unmatched_list']) > 12) ... @endif @endif @endif
Reset
@csrf

Device Registry

Centang mesin lalu gunakan Command To Machine.
Menampilkan {{ $devices->firstItem() ?? 0 }}-{{ $devices->lastItem() ?? 0 }} dari {{ $devices->total() }} device
@forelse ($devices as $device) @empty @endforelse
Alias {{ $sortIcon('alias') }} Serial Number {{ $sortIcon('sn') }} IP {{ $sortIcon('ip_address') }} Approval {{ $sortIcon('approval') }} Sync Rule State {{ $sortIcon('state') }} User Qty {{ $sortIcon('user_count') }} Finger Qty Transaction Qty {{ $sortIcon('transaction_count') }} Last Poll {{ $sortIcon('push_time') }} Last Data Sync {{ $sortIcon('upload_time') }} Last Activity {{ $sortIcon('last_activity') }} Action
{{ $device->alias }}
@if ((int) $device->status !== 1 && $device->create_time && \Illuminate\Support\Carbon::parse($device->create_time)->isToday())
New Device
@endif
{{ $device->sn }} {{ $device->ip_address }} @if ((int) $device->status === 1) Approved @else Pending Approval @endif @if ((int) $device->status === 1) Sync allowed @else Sync blocked until approved @endif
@if ((int) $device->status !== 1) Ping Blocked @elseif (empty($device->first_sync_at)) Waiting First Sync @else {{ $device->presence_label }}
@php $activityStyle = match ($device->presence_activity_type ?? null) { 'polling' => 'background: rgba(28, 100, 242, 0.12); color: #1c64f2;', 'data_sync' => 'background: rgba(45, 106, 79, 0.12); color: #2d6a4f;', default => 'background: rgba(201, 162, 39, 0.14); color: #8b6b14;', }; @endphp {{ $device->presence_activity_label }}
@endif
@if ((int) $device->status !== 1) Device boleh tercatat, tapi heartbeat dan sync data ditahan sampai approval. @elseif (empty($device->first_sync_at)) Device sudah approved, tapi belum ada feedback pertama dari mesin ke database. @else {{ $device->presence_meta }} @endif
{{ $device->user_count ?? 0 }}
DB: {{ $device->db_user_count ?? 0 }}
{{ $device->fp_count ?? 0 }}
DB: {{ $device->db_fp_count ?? 0 }}
{{ $device->transaction_count ?? 0 }}
DB: {{ $device->db_transaction_count ?? 0 }}
@php($pushHealth = $formatTimeHealth($device->push_time, 'Belum ada poll', 1, 10))
{{ $pushHealth['display'] }}
{{ $pushHealth['relative'] }}
@if ($pushHealth['warning'])
{{ $pushHealth['warning'] }}
@endif
@php($uploadHealth = $formatTimeHealth($device->upload_time, 'Belum ada sync data', 5, 30))
{{ $uploadHealth['display'] }}
{{ $uploadHealth['relative'] }}
@if ($uploadHealth['warning'])
{{ $uploadHealth['warning'] }}
@endif
@php($activityHealth = $formatTimeHealth($device->last_activity, 'Belum ada heartbeat', 1, 10))
{{ $activityHealth['display'] }}
{{ $activityHealth['relative'] }}
@if ($activityHealth['warning'])
{{ $activityHealth['warning'] }}
@endif
{{ $device->presence_meta ?? 'Belum ada heartbeat' }}
First sync: {{ $device->first_sync_at ?: '-' }}
Edit Users XLSX @if ((int) $device->status !== 1) @csrf @endif
@csrf
Belum ada data device.
Menampilkan {{ $devices->firstItem() ?? 0 }}-{{ $devices->lastItem() ?? 0 }} dari {{ $devices->total() }} device
{{ $devices->onEachSide(1)->links() }}

Approval Audit Trail

Riwayat siapa yang approve device dan kapan perubahan approval dilakukan.
@forelse ($recentApprovalAudits as $audit) @empty @endforelse
When Device Action Old Status New Status By Summary
{{ $audit->created_at?->format('Y-m-d H:i:s') ?: '-' }} {{ $audit->terminal_alias ?: $audit->terminal_sn }} {{ strtoupper($audit->action_type) }} {{ $audit->old_status ?? '-' }} {{ $audit->new_status ?? '-' }} {{ $audit->performed_name ?: ($audit->performed_by ?: 'system') }} {{ $audit->summary ?: '-' }}
Belum ada audit approval device.

Re-log Audit Trail

Riwayat soft re-sync, hard re-log, dan backfill re-log berdasarkan tanggal agar tim bisa review tindakan recovery per mesin.
@forelse ($recentRelogAudits as $audit) @empty @endforelse
When Device Action Range Rows By Summary
{{ $audit->created_at?->format('Y-m-d H:i:s') ?: '-' }} {{ $audit->terminal_alias ?: $audit->terminal_sn }} {{ strtoupper(str_replace('_', ' ', $audit->action_type)) }} @if ($audit->requested_from || $audit->requested_to) {{ $audit->requested_from ?: '?' }} s/d {{ $audit->requested_to ?: '?' }} @else - @endif {{ $audit->row_count }} {{ $audit->performed_name ?: ($audit->performed_by ?: 'system') }} {{ $audit->summary ?: '-' }}
Belum ada audit re-log device.
@endsection @push('scripts') @endpush