@php /** @var \App\Models\Device $device */ $isConnected = (bool) $device->active; $statusPill = match ($device->status) { 'connected' => ['bg' => 'bg-wa-mint', 'text' => 'text-wa-deep', 'dot' => 'bg-wa-green', 'label' => 'Connected'], 'needs_pair' => [ 'bg' => 'bg-accent-amber/15', 'text' => 'text-[#7B5A14]', 'dot' => 'bg-accent-amber', 'label' => 'Needs re-pair', ], 'failed' => [ 'bg' => 'bg-accent-coral/15', 'text' => 'text-[#A1431F]', 'dot' => 'bg-accent-coral', 'label' => 'Failed', ], default => [ 'bg' => 'bg-paper-50', 'text' => 'text-ink-700', 'dot' => 'bg-paper-300', 'label' => 'Disconnected', ], }; // Real metrics pulled from the messages table by DevicesController::show(). // The blade only falls back to defaults if the controller didn't // pass them (e.g. when this view is included from an admin page). $sentSeries = $sentSeries ?? array_fill(0, 7, 0); $failSeries = $failSeries ?? array_fill(0, 7, 0); $sent7d = $sent7d ?? 0; $failed7d = $failed7d ?? 0; $delivered7d = $delivered7d ?? 0; $deliveryPct = $deliveryPct ?? 0; $sent24 = $sent24 ?? 0; $failed24 = $failed24 ?? 0; @endphp
Devices / {{ $device->display_phone }}
{{ $device->device_name }} {{ __('analytics') }}
@if ($isConnected)
@csrf
@else {{-- Re-pair returns to the index page where the connect modal lives. --}} Re-pair @endif
@csrf
{{-- Edit device — name / number / region. Posts to DevicesController::update(). This is the working target of the "Edit" pencil on the device list. --}}

{{ __('Edit device') }}

@csrf @method('PUT')

{{ __('Updates this device only. Connection status is managed by pairing, not here.') }}

@if ($errors->any())
@foreach ($errors->all() as $e)
{{ $e }}
@endforeach
@endif
{{-- Device header card --}}
{{ $isConnected ? 'Connected device' : 'Saved device' }}

{{ $device->device_name }}

{{ $device->display_phone }}
{{ $statusPill['label'] }} @if ($device->region) {{ $device->region }} @endif @if ($device->country_code) Dial code {{ $device->country_code }} @endif {{ $device->last_seen_at ? 'Last sync ' . $device->last_seen_at->diffForHumans() : 'Never connected' }} Added {{ $device->created_at?->diffForHumans() ?? '—' }}
{{-- KPI strip — pulls from the device's stored metrics --}}
{{ __('Sent (7d)') }}
{{ number_format($sent7d) }}{{ __('outgoing') }}
{{ __('Delivered') }}{{ $deliveryPct }}%
{{ number_format($deliveryPct, 1) }}%{{ number_format($delivered7d) }} ok
{{ __('Sent (24h)') }}
{{ number_format($sent24) }}{{ __('today') }}
{{ __('Failed') }}{{ $sent24 ? round(($failed24 / max($sent24, 1)) * 100, 1) : 0 }}%
{{ number_format($failed24) }}{{ __('retry queue') }}
{{ __('Status') }}
{{ $statusPill['label'] }}
{{-- Charts: send volume + status mix --}}
{{ __('Activity') }}

{{ __('Sent vs failed (7d)') }}

{{ __('Status mix') }}

{{ __('Delivery breakdown') }}

Delivered{{ number_format($delivered7d) }}
Failed{{ number_format($failed7d) }}
Total sent{{ number_format($sent7d) }}
{{-- Top recipients + recent failures. Note: messages.from_number is encrypted-at-rest so we can't SQL-WHERE on it. Instead we walk the conversation chain (conversations.device_id is a plain FK) to fetch only the rows that came from this device. --}} @php $deviceMessages = \App\Models\Message::query() ->whereHas('conversation', fn($q) => $q->where('device_id', $device->id)) ->orderByDesc('created_at') ->limit(200) ->get(); $topRecipients = $deviceMessages ->where('direction', 'out') ->groupBy('to_number') ->map(fn($g) => ['to_number' => $g->first()->to_number, 'count' => $g->count()]) ->sortByDesc('count') ->take(5) ->values(); $recentFailures = $deviceMessages->where('direction', 'out')->where('status', 'failed')->take(8); $recentSends = $deviceMessages->where('direction', 'out')->take(20); @endphp
{{ __('Top recipients') }}

{{ __('Most-messaged') }}

@if ($topRecipients->isNotEmpty()) @else
@include('user.partials.empty-state', [ 'message' => 'No outbound messages found. Once this device starts sending, top recipients will appear here.', ])
@endif
{{ __('Failures') }}

{{ __('Recent failed sends') }}

{{ __('All →') }}
@if ($recentFailures->count())
@foreach ($recentFailures as $m) @endforeach
{{ __('Time') }} {{ __('Recipient') }} {{ __('Reason') }} {{ __('Action') }}
{{ $m->created_at?->format('H:i') ?? '—' }}
{{ $m->to_number ?? '—' }}
{{ $m->failure_reason ?: 'unknown error' }} {{ $m->media_path ? 'media' : 'text' }}
@else
@include('user.partials.empty-state', [ 'message' => 'No failed sends found. Failed sends from this device will appear here.', ])
@endif
{{-- Live send feed for this device. Pulled from real Messages joined to conversations.device_id, so it stays fresh as queues fire and scheduled sends complete. Refreshes via the page-level meta-refresh below (the lightweight /chat polling lives in JS only). --}}
{{ __('Live feed') }}

{{ __('Recent sends from this device') }}

{{ __('auto-refresh 5s') }} {{ __('Open chat →') }}
{{-- Per-source totals strip — chat / campaign / scheduled counts show at a glance what's flowing through this device. --}}
{{ __('Chat sends') }}
{{ number_format($kindCounts['chat'] ?? 0) }}
{{ __('via /chat composer') }}
{{ __('Campaign sends') }}
{{ number_format($kindCounts['campaign'] ?? 0) }}
{{ __('via /wa-campaigns') }}
{{ __('Scheduled sends') }}
{{ number_format($kindCounts['scheduled'] ?? 0) }}
{{ __('via /scheduled') }}
@if ($recentRows->count())
@foreach ($recentRows as $m) @php $statusBadge = match ($m->status) { 'sent', 'delivered' => 'bg-wa-mint text-wa-deep border border-wa-deep/30', 'read' => 'bg-[#E5F5FE] text-[#13478A] border border-[#53BDEB]/40', 'failed' => 'bg-accent-coral/15 text-[#A1431F] border border-accent-coral/40', 'scheduled', 'queued' => 'bg-[#FFF7E2] text-[#8A5F0E] border border-[#E0B445]/40', default => 'bg-paper-100 text-ink-700 border border-paper-300', }; $kindBadge = match ($m->kind) { 'campaign' => 'bg-[#E5F5FE] text-[#13478A] border border-[#53BDEB]/30', 'scheduled' => 'bg-[#FFF7E2] text-[#8A5F0E] border border-[#E0B445]/40', default => 'bg-wa-mint text-wa-deep border border-wa-deep/20', }; @endphp @endforeach
{{ __('Time') }} {{ __('Source') }} {{ __('Recipient') }} {{ __('Body / Media') }} {{ __('Status') }}
{{ $m->ts?->format('M d · H:i') ?? '—' }} {{ $m->kind }} {{ $m->to ?: '—' }} @if ($m->media_type) {{ $m->media_type }} @else
{{ \Illuminate\Support\Str::limit($m->body ?: '—', 80) }}
@endif
{{ $m->status }}
@else
@include('user.partials.empty-state', [ 'message' => 'No sends yet. Once you send messages from this device they will appear here.', ])
@endif
{{-- ─────────────────────────────────────────────────────────────────── All messages for this device (full paginated stream) Mirrors /message-history but scoped to this device's id. Pulls from 5 source tables: inbox, auto-reply, campaign, scheduled, legacy direct. Broadcast is excluded — broadcasts fan-out across every connected number so attributing them to one device lies. ─────────────────────────────────────────────────────────────── --}} @php $sourceMeta = [ 'inbox' => ['label' => 'Team inbox', 'pill' => 'bg-wa-mint text-wa-deep'], 'auto_reply' => ['label' => 'Auto-reply', 'pill' => 'bg-[#F3E9FF] text-[#5B3D8A]'], 'campaign' => ['label' => 'Campaign', 'pill' => 'bg-accent-amber/20 text-[#7B5A14]'], 'scheduled' => ['label' => 'Scheduled', 'pill' => 'bg-[#FFF7E2] text-[#8A5F0E]'], 'legacy' => ['label' => 'Direct', 'pill' => 'bg-paper-100 text-ink-700'], ]; $activeSources = $msgSources ?: array_keys($sourceMeta); @endphp
{{ __('Message history') }}

{{ __('All messages for this device') }}

{{ number_format($msgPaginator->total()) }} total · across {{ count(array_filter($msgSourceCounts, fn($v, $k) => $k !== 'total' && $v > 0, ARRAY_FILTER_USE_BOTH)) }} {{ __('sources') }}
@foreach (['dir' => $msgDirection !== 'all' ? $msgDirection : null] as $k => $v) @if ($v) @endif @endforeach @foreach ($msgSources as $s) @endforeach @if ($msgQ !== '' || $msgDirection !== 'all' || count($msgSources) < 5) reset @endif
{{-- Source filter pills + direction filter --}}
@php $allPicked = count($msgSources) === 5; $allUrl = url()->current() . '?' . http_build_query( array_filter([ 'q' => $msgQ ?: null, 'dir' => $msgDirection !== 'all' ? $msgDirection : null, ]), ); @endphp All {{ number_format($msgSourceCounts['total']) }} @foreach ($sourceMeta as $key => $meta) @php $isOnly = count($msgSources) === 1 && $msgSources[0] === $key; $u = url()->current() . '?' . http_build_query( array_filter([ 'q' => $msgQ ?: null, 'dir' => $msgDirection !== 'all' ? $msgDirection : null, 'sources' => [$key], ]), ); @endphp {{ $meta['label'] }} {{ number_format($msgSourceCounts[$key] ?? 0) }} @endforeach
@foreach ([['all', 'All'], ['out', 'Sent'], ['in', 'Received'], ['fail', 'Failed']] as [$k, $label]) @php $u = url()->current() . '?' . http_build_query( array_filter([ 'q' => $msgQ ?: null, 'dir' => $k !== 'all' ? $k : null, 'sources' => count($msgSources) < 5 ? $msgSources : null, ]), ); @endphp {{ $label }} @endforeach
{{-- The actual table --}} @if ($msgPaginator->total() === 0)
@include('user.partials.empty-state', [ 'message' => 'No messages found for this device with the current filters. Try resetting filters or expanding the source set.', ])
@else
@foreach ($msgPaginator as $r) @php $dir = $r['direction'] ?? 'in'; $status = $r['status'] ?? ''; $statusBadge = match (true) { in_array($status, ['read'], true) => 'bg-[#E5F5FE] text-[#13478A] border border-[#53BDEB]/40', in_array($status, ['delivered', 'sent', 'fired', 'paid'], true) => 'bg-wa-mint text-wa-deep border border-wa-deep/30', in_array($status, ['failed', 'error'], true) => 'bg-accent-coral/15 text-[#A1431F] border border-accent-coral/40', in_array($status, ['scheduled', 'queued', 'pending'], true) => 'bg-[#FFF7E2] text-[#8A5F0E] border border-[#E0B445]/40', default => 'bg-paper-100 text-ink-700 border border-paper-300', }; $sm = $sourceMeta[$r['source']] ?? [ 'label' => $r['source_label'] ?? '—', 'pill' => 'bg-paper-100 text-ink-700', ]; $contact = $r['contact_name'] ?: ($r['phone'] ?: '—'); @endphp @endforeach
{{ __('When') }} {{ __('Source') }} {{ __('Direction') }} {{ __('Contact / Phone') }} {{ __('Body') }} {{ __('Status') }}
{{ $r['when']?->format('M d · H:i') ?? '—' }} {{ $sm['label'] }} @if ($dir === 'out') {{ __('out') }} @else in @endif
{{ $contact }}
@if ($r['phone'] && $r['contact_name'])
{{ $r['phone'] }}
@endif
{{ \Illuminate\Support\Str::limit((string) ($r['body'] ?? ''), 90) ?: '—' }}
{{ $status ?: '—' }}
@if ($msgPaginator->hasPages())
Showing {{ $msgPaginator->firstItem() }}–{{ $msgPaginator->lastItem() }} of {{ number_format($msgPaginator->total()) }} · Page {{ $msgPaginator->currentPage() }} of {{ $msgPaginator->lastPage() }}
@if ($msgPaginator->onFirstPage()) {{ __('Prev') }} @else Prev @endif @php $pages = []; for ( $i = max(1, $msgPaginator->currentPage() - 2); $i <= min($msgPaginator->lastPage(), $msgPaginator->currentPage() + 2); $i++ ) { $pages[] = $i; } if (!in_array(1, $pages, true)) { array_unshift($pages, 1); } if (!in_array($msgPaginator->lastPage(), $pages, true)) { $pages[] = $msgPaginator->lastPage(); } @endphp @foreach ($pages as $p) @if ($p === $msgPaginator->currentPage()) {{ $p }} @else {{ $p }} @endif @endforeach @if ($msgPaginator->hasMorePages()) Next @else {{ __('Next') }} @endif
@endif @endif
{{-- Health sidebar --}}
{{ __('Health checks') }}

{{ __('Things to watch') }}

@if ($isConnected)
{{ __('Bridge online') }}
Last sync {{ $device->last_seen_at?->diffForHumans() ?? 'never' }}.
@else
{{ __('Not connected') }}
{{ __('Re-pair from the device list to start sending again.') }}
@endif @if ($deliveryPct >= 95 && $sent7d > 0)
{{ __('Delivery rate is healthy') }}
{{ $deliveryPct }}% over the last 7 days.
@elseif ($sent7d > 0)
{{ __('Delivery rate slipping') }}
{{ $deliveryPct }}% — review failure reasons in the table above.
@endif
{{ __('Linked sessions') }}
{{ $device->device_name }}
  • {{ __('Region') }}{{ $device->region ?: '—' }}
  • {{ __('Dial code') }}{{ $device->country_code ?: '—' }}
  • {{ __('Status') }}{{ $statusPill['label'] }}
  • {{ __('Added') }}{{ $device->created_at?->format('M d, Y') ?? '—' }}
{{ __('Manage devices') }}
{{-- Auto-refresh the page in-place every 5s so the live feed reflects scheduled sends + status flips without forcing a manual reload. Pauses when the tab is hidden. --}}