@extends('layouts.app') @section('title', 'Product AI Import') @section('content') @php $shopifyListings = $shopifyListings ?? []; $availableChannels = $availableChannels ?? []; $deferRepairBar = (bool) ($deferRepairBar ?? false); @endphp

AVI THE JEWELER · Catalog

Product AI Import

Build Shopify drafts from photos and notes, or Improve existing live products (media only). Manual walks the wizard; Auto runs the pipeline and creates a Shopify draft only when everything checks out.

@if (session('success'))
{{ session('success') }}
@endif @if (session('warning'))
{{ session('warning') }}
@endif @if (session('error'))
{{ session('error') }}
@endif
@php $indexSearchQ = $indexSearchQ ?? ''; @endphp @if($imports->isEmpty() && $indexSearchQ === '')

No imports yet. Start with New import to upload images and an optional video.

@else @php $pendingImports = $imports->filter(function ($imp) { if (! filled($imp->shopify_product_gid) || $imp->isRepairAttentionSkipped()) { return false; } return $imp->hasPendingShopifySync() || $imp->shopifyPayloadNeedsPrepare(); }); $pendingCount = $pendingImports->count(); $pendingListRows = $pendingImports->map(function ($imp) { $needsPrepare = $imp->shopifyPayloadNeedsPrepare(); $needsSync = $imp->hasPendingShopifySync(); $reason = $imp->improvePendingWorkReason(); if ($reason === null) { if ($needsPrepare && $needsSync) { $reason = 'Prepare media, then push to Shopify.'; } elseif ($needsPrepare) { $reason = 'Prepare pending media (crop / compress).'; } else { $reason = 'Push pending gallery changes to Shopify.'; } } $preview = $imp->listPreviewImage(); return [ 'import_id' => (int) $imp->id, 'title' => (string) ($imp->title ?: ('Import #'.$imp->id)), 'open_url' => route('product-ai-import.show', $imp), 'dismiss_url' => route('product-ai-import.dismiss-repair', $imp), 'image_url' => $preview?->displayUrl() ?: $preview?->originalUrl(), 'sku' => (string) ($imp->sku ?: ''), 'workflow' => $imp->isImproveExisting() ? 'improve' : 'import', 'mode' => $imp->is_auto_mode ? 'auto' : 'manual', 'needs_prepare' => $needsPrepare, 'needs_sync' => $needsSync, 'summary' => $reason, ]; })->values()->all(); $runningAutos = $imports->filter(function ($imp) { return $imp->isImproveExisting() && $imp->is_auto_mode && in_array((string) $imp->auto_status, [ \App\Models\ProductAiImport::AUTO_STATUS_QUEUED, \App\Models\ProductAiImport::AUTO_STATUS_RUNNING, ], true); }); $runningAutoCount = $runningAutos->count(); $repairStats = $repairStats ?? null; $repairTotal = 0; $repairRedo = 0; $repairSync = 0; $repairVideo = 0; $repairVideoMissing = 0; $repairBusy = 0; $repairQueued = 0; $repairCapped = false; $repairMode = 'mixed'; $syncOnly = false; $videoOnly = false; $omniPaused = false; $omniResumeAt = null; $repairVideoDeferred = 0; $repairVideoRunnable = 0; $repairListRedo = []; $repairListSync = []; $repairListVideo = []; if (is_array($repairStats)) { $repairTotal = (int) ($repairStats['total'] ?? 0); $repairRedo = (int) ($repairStats['redo'] ?? 0); $repairSync = (int) ($repairStats['sync'] ?? 0); $repairVideo = (int) ($repairStats['video'] ?? 0); $repairVideoMissing = (int) ($repairStats['video_missing'] ?? $repairVideo); $repairBusy = (int) ($repairStats['busy'] ?? 0); $repairQueued = (int) ($repairStats['queued'] ?? $repairTotal); $repairCapped = (bool) ($repairStats['capped'] ?? false); $repairMode = (string) ($repairStats['mode'] ?? 'mixed'); $syncOnly = $repairMode === 'sync_only'; $videoOnly = $repairMode === 'video_only'; $omniPaused = (bool) ($repairStats['omni_paused'] ?? false); $omniResumeAt = $repairStats['omni_resume_at'] ?? null; $repairVideoDeferred = (int) ($repairStats['video_deferred'] ?? 0); $repairVideoRunnable = max(0, $repairVideo - $repairVideoDeferred); $repairLists = is_array($repairStats['lists'] ?? null) ? $repairStats['lists'] : []; $repairListRedo = is_array($repairLists['redo'] ?? null) ? $repairLists['redo'] : []; $repairListSync = is_array($repairLists['sync'] ?? null) ? $repairLists['sync'] : []; $repairListVideo = is_array($repairLists['video'] ?? null) ? $repairLists['video'] : []; } $batchLite = $activeBatchLite ?? null; $batchLiteActive = (bool) ($batchLite['is_active'] ?? false); // Orange “now working” row only while the batch is live — never after it finishes. $batchCurrentId = $batchLiteActive ? (int) ($batchLite['current_import_id'] ?? 0) : 0; $batchCurrentOnPage = $batchCurrentId > 0 && $imports->contains(fn ($imp) => (int) $imp->id === $batchCurrentId); $isSearching = $indexSearchQ !== ''; @endphp @if(! $isSearching && $runningAutoCount > 0 && ! $batchLiteActive)
{{ $runningAutoCount }} Improve Auto job(s) running on this page @foreach($runningAutos->take(3) as $ra) #{{ $ra->id }} {{ \Illuminate\Support\Str::limit($ra->title ?: 'Product', 40) }}@if(! $loop->last) · @endif @endforeach @if($runningAutoCount > 3) · +{{ $runningAutoCount - 3 }} more @endif
@csrf
@endif @if(! $isSearching && $batchLiteActive)

{{ ($batchLite['source'] ?? '') === 'repair' ? 'Repair pass running' : 'Improve Auto batch running' }}

@if((int) ($batchLite['running'] ?? 0) > 0) Working {{ (int) ($batchLite['current'] ?? 0) }} of {{ (int) ($batchLite['total'] ?? 0) }} · {{ (int) ($batchLite['finished'] ?? 0) }} finished @else {{ (int) ($batchLite['finished'] ?? 0) }} of {{ (int) ($batchLite['total'] ?? 0) }} processed @endif

Watch full queue
@csrf

{{ \Illuminate\Support\Str::limit($batchLite['message'] ?? '', 220) }} The queue survives a refresh. You do not need to hunt every page — use Open product above.

@endif @if(! $isSearching && $deferRepairBar && ! $batchLiteActive)

Scanning catalog backlog…

@elseif(! $isSearching && ! $batchLiteActive && ($repairTotal > 0 || $repairVideoMissing > 0)) @include('product_ai_import.partials.index_repair_bar', [ 'repairTotal' => $repairTotal, 'repairRedo' => $repairRedo, 'repairSync' => $repairSync, 'repairVideo' => $repairVideo, 'repairVideoMissing' => $repairVideoMissing, 'repairBusy' => $repairBusy, 'repairQueued' => $repairQueued, 'repairCapped' => $repairCapped, 'repairMode' => $repairMode, 'syncOnly' => $syncOnly, 'videoOnly' => $videoOnly, 'omniPaused' => $omniPaused, 'omniResumeAt' => $omniResumeAt, 'repairVideoDeferred' => $repairVideoDeferred, 'repairVideoRunnable' => $repairVideoRunnable, 'repairListRedo' => $repairListRedo, 'repairListSync' => $repairListSync, 'repairListVideo' => $repairListVideo, ]) @endif @if(! $isSearching && $pendingCount > 0) @include('product_ai_import.partials.index_pending_bar', [ 'pendingCount' => $pendingCount, 'pendingListRows' => $pendingListRows, ]) @endif
@forelse($imports as $import) @php // Shopify set only — never count AI history (original + Enhanced + Omni = inflated). $shopifySet = $import->shopifyMediaItems(); $preview = $import->listPreviewImage(); $previewUrl = $preview?->displayUrl(); $origUrl = $preview?->originalUrl() ?: $previewUrl; $websiteUrl = $import->storefrontUrl(); $imgCount = $shopifySet->filter(fn ($m) => $m->isImage())->count(); $vidCount = $shopifySet->filter(fn ($m) => $m->isVideo())->count(); if ($imgCount < 1 && $vidCount < 1) { $imgCount = $import->uploadedProductImagesCount(); $vidCount = $import->uploadedVideosCount(); } $listing = $shopifyListings[$import->id] ?? ['status' => null, 'channels' => [], 'channels_live' => false]; $shopStatus = strtoupper((string) ($listing['status'] ?? '')); $channels = is_array($listing['channels'] ?? null) ? $listing['channels'] : []; $channelsLive = ! empty($listing['channels_live']); if ($channels === [] && $availableChannels !== [] && ! $channelsLive) { $channels = array_map(static fn ($ch) => [ 'id' => $ch['id'], 'name' => $ch['name'], 'published' => true, ], $availableChannels); } $adminUrl = is_array($import->form_data) ? ($import->form_data['shopify_admin_url'] ?? null) : null; $linked = (bool) $import->shopify_product_gid; $autoRunning = $import->isAutoRunning(); $bgBusy = $import->isBackgroundBusy(); $isBatchCurrent = $batchCurrentId > 0 && (int) $import->id === $batchCurrentId; $workBusy = $autoRunning || $bgBusy || $isBatchCurrent; $needsPrepare = $linked && $import->shopifyPayloadNeedsPrepare(); $pendingSync = $import->hasPendingShopifySync(); $hasPending = $needsPrepare || $pendingSync; $paiVideos = $import->relationLoaded('media') ? $import->media->where('media_type', \App\Models\ProductAiImportMedia::TYPE_VIDEO) : $import->videos; $noOmniVideo = $import->isImproveExisting() && $paiVideos->where('is_ai_generated', false)->isNotEmpty() && ! $import->hasAiEnhancedVideo(); $indexGallery = []; $galleryStart = 0; $galleryZipUrl = route('product-ai-import.download-ai-zip', $import); foreach ($shopifySet as $media) { $gUrl = $media->displayUrl(); if (! $gUrl) { continue; } if ($preview && (int) $media->id === (int) $preview->id) { $galleryStart = count($indexGallery); } $isWorn = $import->isWornOriginalMedia($media); $isMeas = $import->isMeasurementOriginalMedia($media); $gKind = 'Original'; if ($media->isVideo()) { $gKind = $media->is_ai_generated ? 'Omni video' : 'Video'; } elseif ($media->isEnhance()) { $gKind = 'Enhanced'; } elseif ($media->isLifestyleAi()) { $gKind = 'Lifestyle'; } elseif ($isWorn) { $gKind = 'How it looks'; } elseif ($isMeas) { $gKind = 'Measurement'; } elseif ($media->is_ai_generated) { $gKind = 'AI'; } $canDownload = $import->isIndexGalleryDownloadable($media) && $import->indexGalleryDownloadRelativePath($media) !== null; $indexGallery[] = [ 'id' => (int) $media->id, 'src' => $gUrl, 'orig' => $media->originalUrl() ?: $gUrl, 'kind' => $gKind, 'pos' => (int) $media->position, 'is_video' => $media->isVideo(), 'downloadable' => $canDownload, 'download_url' => $canDownload ? route('product-ai-import.media.download-ai', [$import, $media]) : null, 'selected' => $canDownload, ]; } @endphp @empty @endforelse
Product Mode Workflow Import Shopify Pending Sales channels Updated
@if($previewUrl) @else
No photo
@endif
@if($websiteUrl) {{ $import->title ?: 'Untitled' }} ↗ @else
{{ $import->title ?: 'Untitled' }}
@endif
#{{ $import->id }} · SKU {{ $import->sku ?: '—' }}
{{ $imgCount }} photo{{ $imgCount === 1 ? '' : 's' }}@if($vidCount > 0) · {{ $vidCount }} video{{ $vidCount === 1 ? '' : 's' }}@endif
@if($websiteUrl) @endif
@if($import->is_auto_mode) Auto @else Manual @endif @if($import->isImproveExisting()) Improve @else Import @endif @php // “Needs sync” must never wear the green synced badge. $paiPendingSync = $workBusy ? null : $import->improvePendingWorkReason(); $paiBadge = $workBusy ? ' pai-badge--running' : ''; if ($import->is_auto_mode && ! $workBusy) { if ($paiPendingSync !== null) { $paiBadge = ' pai-badge--blocked'; } elseif ($import->auto_status === \App\Models\ProductAiImport::AUTO_STATUS_BLOCKED) { $paiBadge = ' pai-badge--blocked'; } elseif ($import->auto_status === \App\Models\ProductAiImport::AUTO_STATUS_FAILED) { $paiBadge = ' pai-badge--failed'; } elseif ($import->auto_status === \App\Models\ProductAiImport::AUTO_STATUS_SYNCED) { $paiBadge = ' pai-badge--synced'; } } @endphp @if($isBatchCurrent && ! $autoRunning && ! $bgBusy) In repair queue @else {{ $import->statusLabel() }} @endif @if($paiPendingSync !== null)
{{ $paiPendingSync }}
@endif @php $idxActivity = $import->indexActivityPresentation( $isBatchCurrent, is_array($batchLite ?? null) ? ($batchLite['current_step_message'] ?? null) : null ); $idxBlockers = is_array($import->form_data['auto_blockers'] ?? null) ? $import->form_data['auto_blockers'] : []; $idxShowBlockers = $import->is_auto_mode && $import->auto_status === \App\Models\ProductAiImport::AUTO_STATUS_BLOCKED && count($idxBlockers) > 0; $idxShowActivity = $idxActivity['show_line'] || $idxShowBlockers; @endphp
@if(! $linked) Not linked @elseif($shopStatus === 'ACTIVE') Active @elseif($shopStatus === 'DRAFT') Draft @elseif($shopStatus === 'ARCHIVED') Archived @elseif($shopStatus !== '') {{ ucfirst(strtolower($shopStatus)) }} @else Linked @endif @if($needsPrepare)
Prepare pending
@elseif($pendingSync)
Sync pending
@elseif($noOmniVideo)
No Omni video
@else — @endif
@if($channelsLive && $channels === []) Loading channels… @elseif($channels === []) Channels unavailable @else
@foreach($channels as $ch) @php $on = ! empty($ch['published']); @endphp {{ $ch['name'] ?? 'Channel' }} @endforeach
@unless($linked)
Default: all marked on create
@endunless @endif
{{ $import->updated_at?->timezone(config('app.deals_display_timezone', 'America/New_York'))->format('M j, g:i A') }}
Open @if($import->isImproveExisting() && $linked && ! $import->wasRevertedToOriginals())
@csrf
@elseif($import->isImproveExisting() && $linked && $import->wasRevertedToOriginals()) Reverted @endif @if($import->canDeleteFromApp()) @endif
@if($indexSearchQ !== '') No products match “{{ $indexSearchQ }}”. Clear search @else No Product AI imports yet. @endif
{{ $imports->links() }}
@endif
{{-- Delete import: two-step confirm --}} @push('styles') @endpush @push('scripts') @endpush @endsection