@php use Illuminate\Support\Carbon; /** @var \Illuminate\Support\Collection $rows */ /** @var \Illuminate\Support\Collection $upcoming */ /** @var array $totals */ $rows = $rows ?? collect(); $upcoming = $upcoming ?? collect(); $totals = $totals ?? [ 'total' => 0, 'active' => 0, 'paused' => 0, 'completed' => 0, 'failed' => 0, 'cancelled' => 0, 'total_sent' => 0, 'total_delivered' => 0, 'avg_delivery' => 0, ]; /** Map a recipient_type + counts blob to a one-line label like "847 / Group". */ $describeRecipients = function ($r) { $count = (int) ($r->total_recipients ?? 0); $tag = match ($r->recipient_type) { 'group' => 'Group', 'queue' => 'Queue', 'number' => 'Numbers', default => '—', }; return number_format($count) . ' / ' . $tag; }; /** Pretty status pill class + dot color. */ $statusPill = function (string $status) { return match ($status) { 'scheduled' => ['cls' => 'bg-wa-mint text-wa-deep', 'dot' => 'bg-wa-green', 'label' => 'Queued'], 'running' => ['cls' => 'bg-[#D9E5F2] text-[#13478A]', 'dot' => 'bg-[#13478A]', 'label' => 'Running'], 'paused' => ['cls' => 'bg-accent-amber/20 text-[#7B5A14]', 'dot' => 'bg-accent-amber', 'label' => 'Paused'], 'completed' => ['cls' => 'bg-paper-100 text-ink-700', 'dot' => 'bg-ink-500', 'label' => 'Done'], 'failed' => [ 'cls' => 'bg-accent-coral/15 text-accent-coral', 'dot' => 'bg-accent-coral', 'label' => 'Failed', ], 'cancelled' => ['cls' => 'bg-paper-100 text-ink-500', 'dot' => 'bg-ink-500/40', 'label' => 'Cancelled'], default => ['cls' => 'bg-paper-100 text-ink-700', 'dot' => 'bg-ink-500/40', 'label' => $status], }; }; /** Pretty template-type pill — colors match the original demo. */ $typePill = function (string $type) { return match ($type) { 'template' => ['cls' => 'bg-wa-deep/10 text-wa-deep', 'icon' => 'M2.5 3.5h11v9h-11zM5 6h6M5 9h6'], 'media' => ['cls' => 'bg-accent-amber/20 text-[#7B5A14]', 'icon' => 'M2 3h12v10H2zM5 7l3 3 5-5'], 'location' => [ 'cls' => 'bg-[#F3E9FF] text-[#5B3D8A]', 'icon' => 'M8 1.5C5.5 1.5 3.5 3.4 3.5 5.7c0 3 4.5 8.8 4.5 8.8s4.5-5.8 4.5-8.8C12.5 3.4 10.5 1.5 8 1.5z', ], default => ['cls' => 'bg-paper-100 text-ink-700', 'icon' => 'M3 4h10M3 8h10M3 12h7'], }; }; @endphp
{{ __('More / Scheduled Messages') }}
{{ __('Scheduled') }} {{ __('messages') }}
{{ $totals['active'] }} {{ __('queued') }} Schedule message
{{ __('Total scheduled') }}
{{ number_format($totals['total']) }} {{ __('across all states') }}
{{ __('Active queue') }}
{{ number_format($totals['active']) }} {{ __('waiting to fire') }}
{{ __('Total sent') }}
{{ number_format($totals['total_sent']) }} {{ __('messages out') }}
{{ __('Delivery rate') }}
{{ $totals['avg_delivery'] }}% {{ __('across completed') }}
{{ __('Up next') }}

{{ __('Firing in the next 24 hours') }}

{{ $upcoming->count() }} {{ __('pending') }}
@if ($upcoming->isEmpty())
Nothing scheduled yet. {{ __('Create one →') }}
@else
@foreach ($upcoming as $row) @php $tz = $row->timezone ?: 'UTC'; $when = Carbon::parse($row->next_run_at); $whenLocal = $when->copy()->setTimezone($tz); // Carbon 3 returns floats from diffInMinutes — cast to int // or the label renders as "in 3.8237903166667m". $diff = (int) round(now()->diffInMinutes($when, false)); $when_lbl = $diff < 0 ? 'overdue' : ($diff < 1 ? 'now' : ($diff < 60 ? "in {$diff}m" : ($diff < 60 * 24 ? 'in ' . (int) round($diff / 60) . 'h' : ($diff < 60 * 48 ? 'tomorrow' : $whenLocal->format('M j'))))); @endphp
{{ $when_lbl }}
{{ $whenLocal->format('H:i') }}
{{ $row->schedule_name }}
{{ $describeRecipients($row) }} · {{ $row->template_type }}{{ $row->is_recurring ? ' · ' . $row->repeat_interval : '' }}
{{ $row->is_recurring ? 'recurring' : 'queued' }}
@endforeach
@endif
{{ __('Tip') }}
{{ __('Send when they read') }}

{{ __("Open rates peak between 9–11 AM and 6–8 PM in your contacts' local timezones. Pick a send time that matches the recipient cohort — recurring schedules let you set this once and forget.") }}

Schedule one
@php $tabs = [ 'all' => $totals['total'], 'scheduled' => $rows->where('status', 'scheduled')->count(), 'running' => $rows->where('status', 'running')->count(), 'paused' => $totals['paused'], 'completed' => $totals['completed'], 'failed' => $totals['failed'], 'cancelled' => $totals['cancelled'], ]; @endphp @foreach ($tabs as $key => $count) @endforeach
{{-- Rows live in their own partial so the JSON `?partial=1` path on the controller can re-render them after silent AJAX actions (pause / resume / cancel / run-now / destroy) without forcing a full page reload. Same shape /broadcasts uses — see resources/js/charts/user-broadcasts-index.js. --}} @include('user.scheduled._rows', [ 'rows' => $rows, 'statusPill' => $statusPill, 'typePill' => $typePill, 'describeRecipients' => $describeRecipients, ])
{{ __('Schedule') }} {{ __('Type') }} {{ __('Recipients') }} {{ __('Send time') }} {{ __('Repeat') }} {{ __('Status') }} {{ __('Actions') }}
{{-- Retry modal — opens when operator clicks retry on a failed row. Operator picks a fresh send date + time + timezone; JS POSTs to /scheduled/{id}/retry. The endpoint resets the row + pivot and re-registers the Node cron with the new ISO time. --}} {{-- Confirmation modal shared with the detail page. JS toggles `hidden`/`flex` and rewrites the title/body for each action. --}}