{{-- Shared sync panel for Shopify integrations. Rendered by both Integrations\Index (per row) and Integrations\Edit (full page). Required variables in scope: - $integration (App\Models\Integration) - $snapshots (array{customers: ?array, orders: ?array}) - $canManage (bool) - $resetConfirm (?string) staged "{integrationId}:{type}" - $isLocal (bool) - $queueDriver (string) - $shopifyQueueName (string) e.g. "shopify-sync" - $pendingShopifyJobsCount (?int) - $nextShopifyJobAt (?\Illuminate\Support\Carbon) - $autoProcess (bool) - $lastAutoProcessAt (?string) ISO8601 - $lastAutoProcessResult (string) - $autoProcessPollSeconds (int) Methods on the parent Livewire component: - startSync(int, string), cancelSync(int, string), resumeSync(int, string), confirmResetProgress(int, string), cancelResetConfirmation(), resetProgress(int, string), processQueueNow(), toggleAutoProcess(), autoProcessTick() --}} @php $stateLabel = function (?string $state): string { return match ($state) { 'idle' => __('Synced'), 'syncing' => __('Syncing…'), 'queued' => __('Queued'), 'failed' => __('Failed'), 'cancelled' => __('Cancelled'), null => __('Not started'), default => (string) $state, }; }; $stateColor = function (?string $state): string { return match ($state) { 'idle' => 'lime', 'syncing' => 'sky', 'queued' => 'zinc', 'failed' => 'red', 'cancelled' => 'zinc', default => 'zinc', }; }; $relativeTime = function (?string $iso): ?string { if (! is_string($iso) || $iso === '') { return null; } try { return \Illuminate\Support\Carbon::parse($iso)->diffForHumans(); } catch (\Throwable) { return null; } }; $resources = [ ['key' => 'customers', 'label' => __('Customers'), 'snapshot' => $snapshots['customers'] ?? null], ['key' => 'orders', 'label' => __('Orders'), 'snapshot' => $snapshots['orders'] ?? null], ]; @endphp
{{ __('Shopify sync') }}
{{ __('Customers and Orders sync independently.') }}
@foreach ($resources as $resource) @php $key = $resource['key']; $snap = $resource['snapshot']; $state = $snap['state'] ?? null; $isRunning = in_array($state, ['queued', 'syncing'], true); $isResting = ! $isRunning; $confirmKey = $integration->id.':'.$key; $isConfirmingReset = ($resetConfirm ?? null) === $confirmKey; @endphp
{{ $resource['label'] }} {{ $stateLabel($state) }}
@if ($snap !== null)
{{ __('Records imported') }}
{{ number_format($snap['records_imported']) }}
{{ __('Pages processed') }}
{{ number_format($snap['page_count']) }}
@if ($snap['started_at'])
{{ __('Started') }}
{{ $relativeTime($snap['started_at']) ?? '—' }}
@endif @if ($snap['last_synced_at'])
{{ __('Last synced') }}
{{ $relativeTime($snap['last_synced_at']) ?? '—' }}
@endif @if ($snap['completed_at'])
{{ __('Completed') }}
{{ $relativeTime($snap['completed_at']) ?? '—' }}
@endif @if ($snap['cancelled_at'])
{{ __('Cancelled') }}
{{ $relativeTime($snap['cancelled_at']) ?? '—' }}
@endif
{{ __('Resume cursor saved') }}
{{ $snap['has_resume_cursor'] ? __('Yes') : __('No') }}
@if ($state === 'failed' && ! empty($snap['last_error']))
{{ $snap['last_error'] }}
@endif @else

{{ __('Not started yet.') }}

@endif @php // Stale = running state with a saved cursor but // no continuation job on the queue. Computed by // the parent Livewire component and folded into // the snapshot so the view can present a clear // recovery affordance. $isStale = (bool) ($snap['is_stale'] ?? false); @endphp @if ($isStale)
{{ __('Sync is paused:') }} {{ __('resume cursor exists but no queued job was found.') }} {{ __('Click "Resume sync" to dispatch a continuation from the saved cursor.') }}
@endif @if ($canManage)
@if ($isStale) {{ __('Resume sync') }} @endif @if ($isResting) {{ __('Start sync') }} @endif @if ($isRunning) {{ __('Cancel sync') }} @endif @if ($isResting && $snap !== null) @if ($isConfirmingReset) {{ __('Confirm reset progress') }} {{ __('Keep progress') }} @else {{ __('Reset progress') }} @endif @endif

@if ($isRunning) {{ __('Cancel stops future continuation jobs but does not delete imported data.') }} @elseif ($isConfirmingReset) {{ __('Reset clears sync progress and cursor but does not delete imported data.') }} @else {{ __('Start sync resumes from saved cursor where one exists, otherwise starts fresh.') }} @endif

@endif
@endforeach
@if ($isLocal)
{{ __('Queue diagnostic') }}: {{ __('Connection') }}: {{ $queueDriver }} · {{ __('Queue') }}: {{ $shopifyQueueName }} @if ($pendingShopifyJobsCount !== null) · {{ __('Pending') }}: {{ $pendingShopifyJobsCount }} @endif @if ($nextShopifyJobAt !== null) · @if ($nextShopifyJobAt->isPast()) {{ __('Next job ready now') }} @else {{ __('Next job ready in') }} {{ $nextShopifyJobAt->diffForHumans(null, true) }} @endif @endif

{{ __('Queued jobs require a queue worker to be running.') }} {{ __('In production this should be run by Supervisor / Forge.') }} {{ __('In local development you can use the "Process queue now" button below, or run:') }} php artisan shopify-sync:work --limit=20 --timeout=60

@if ($canManage)
{{ __('Process queue now') }} {{ __('Processing…') }} {{ __('Drains up to 5 jobs or 10 seconds, whichever first (kept under Herd\'s PHP 30s limit). Local dev only.') }}
{{-- Auto-process toggle. Local/dev + owner/admin only. When ON the wrapper carries wire:poll so the browser fires a bounded `autoProcessTick` every AUTO_PROCESS_POLL_SECONDS. The poll directive is not attached when the toggle is OFF, so no background traffic happens by default. --}}
@if ($autoProcess) {{ __('Stop auto-process') }} @else {{ __('Start auto-process') }} @endif @if ($autoProcess) {{ __('Auto-processing every :sec seconds', ['sec' => $autoProcessPollSeconds]) }} @else {{ __('Off. Browser will tick the bounded drain when enabled.') }} @endif
@if ($autoProcess && $lastAutoProcessAt)
{{ __('Last tick') }}: {{ \Illuminate\Support\Carbon::parse($lastAutoProcessAt)->diffForHumans() }} @if ($lastAutoProcessResult) — {{ $lastAutoProcessResult }} @endif
@endif

{{ __('Local development only. In production a persistent Supervisor / Forge worker must run:') }} php artisan queue:work database --queue={{ $shopifyQueueName }}

@endif
@endif