@extends('layouts.merchant') @section('title', $feed->name) @section('content') @php($isActive = $feed->status === \App\Enums\FeedStatus::Active) @php($marketplace = $feed->isMarketplace()) @php($type = $feed->source_type)

{{ $feed->name }}

{{ $feed->status->label() }} · обновление {{ $feed->update_interval_minutes == 720 ? '2 раза в день' : '1 раз в день' }}

{{-- Ручной импорт нужен только до первой успешной выгрузки — дальше работает автоматика. --}} @unless ($feed->last_success_at)
@csrf
@endunless @unless ($feed->isBlocked()) @if ($isActive) @else
@csrf
@endif @endunless
@unless ($isActive)
Фид {{ $feed->status->label() }} — его товары не показываются в каталоге. Нажмите «Включить», чтобы возобновить.
@endunless {{-- Две выгрузки одного каталога — товары задваиваются на витрине. Предупреждаем, но ничего не выключаем: второй фид бывает нужен, например на время переезда, и решать это продавцу. Метку ставит проверка после импорта. --}} @if ($feed->duplicateOf)
Похоже, эта выгрузка повторяет «{{ $feed->duplicateOf->name }}» Совпало {{ $feed->duplicate_ratio }}% артикулов, значит одни и те же товары попадают в каталог дважды. Если каталог один и тот же, оставьте одну выгрузку — вторую можно открыть и сравнить.
@endif {{-- Товар без цены на площадку не загружается. В истории импортов продавец видит только «Пропущено», а чинится этот пропуск не в сопоставлении категорий, а в самом файле выгрузки — поэтому говорим прямо. --}} @if ($noPriceCount > 0)
Товаров без цены в последней выгрузке: {{ number_format($noPriceCount, 0, '', ' ') }} На teeu товары без цены не загружаются — купить такой товар нельзя. {{ $marketplace ? 'Укажите цену товара в кабинете '.$type->label() : 'Укажите цену в файле выгрузки' }}, и при следующем обновлении эти позиции появятся в каталоге.
@endif {{-- Цены Wildberries отдаёт по тысяче товаров раз в 15 минут, и у большого кабинета они собираются часами. Карточка без собранной цены в каталог не попадает — продавец должен видеть, что это «ещё загружается», а не «пропало». --}} @if ($pricesPending && ($pricesPending['skipped'] > 0 || $pricesPending['waiting'] > 0))
@if ($pricesPending['waiting'] > 0) @php($hours = max(1, (int) ceil($pricesPending['waiting'] / 1000 * \App\Services\Yml\Wildberries\WildberriesPrices::INTERVAL_MINUTES / 60))) Цены ещё загружаются: {{ number_format($pricesPending['waiting'], 0, '', ' ') }} {{ \App\Support\Ru::plural($pricesPending['waiting'], ['товар ждёт', 'товара ждут', 'товаров ждут']) }} цену Wildberries отдаёт цены по 1000 товаров раз в 15 минут, поэтому большой каталог появляется постепенно — примерно {{ $hours }} {{ \App\Support\Ru::plural($hours, ['час', 'часа', 'часов']) }}. Товары добавятся в каталог сами. {{-- Окно цен у WB одно на весь кабинет: если его раз за разом занимает другая интеграция продавца, цены не придут никогда — это надо сказать. --}} @if ($pricesPending['limited_since']?->lt(now()->subHour())) Но с {{ $pricesPending['limited_since']->timezone(config('app.timezone'))->format('d.m в H:i') }} Wildberries не отдаёт цены: лимит 4 запроса в час общий на весь кабинет, и его, похоже, расходует другая ваша интеграция. Проверьте, как часто учётная система или сервис аналитики запрашивают цены. @endif @else Цены загружены {{ number_format($pricesPending['skipped'], 0, '', ' ') }} {{ \App\Support\Ru::plural($pricesPending['skipped'], ['товар появится', 'товара появятся', 'товаров появятся']) }} в каталоге при ближайшем обновлении выгрузки. @endif
@endif {{-- У ключа маркетплейса есть срок: три месяца у Ozon, полгода у Wildberries. Истёк — выгрузка перестаёт обновляться, а после нескольких неудач подряд отключается и прячет товары. Узнать об этом продавец должен здесь и заранее, а не по опустевшей витрине. --}} @if ($marketplace && ($feed->credentialsExpired() || $feed->credentialsExpireSoon()))
@if ($feed->credentialsExpired()) {{ $type->keyName() }} {{ $type->label() }} истёк {{ $feed->credentials_expires_at->format('d.m.Y') }} @else {{ $type->keyName() }} {{ $type->label() }} истекает {{ $feed->credentials_expires_at->format('d.m.Y') }} @endif Выпустите новый {{ mb_strtolower($type->keyName()) }} в кабинете {{ $type->label() }} ({{ $type->keyPlace() }}) и вставьте его в блоке «Кабинет {{ $type->label() }}» ниже — иначе цены и наличие перестанут обновляться.
@endif {{-- Feed settings the seller controls. --}}

Настройки

@if ($marketplace) {{-- Кабинет маркетплейса вместо адреса выгрузки. Сменить кабинет нельзя — другой кабинет это другой каталог; а ключ заменить можно и нужно: у него есть срок. Сам ключ не печатается — им управляют магазином. --}} @php($lastRun = $feed->runs->sortByDesc('id')->first()) @php($keyWord = $type === \App\Enums\YmlSourceType::Wildberries ? 'токен' : 'ключ')
Кабинет {{ $type->label() }} {{-- Client ID продавец Ozon узнаёт в лицо; ID продавца WB — UUID из токена, его не показываем. --}} @if ($type === \App\Enums\YmlSourceType::Ozon) Client ID {{ $feed->external_account_id }} @endif
@if (filled($feed->settings['company'] ?? null)) {{ $feed->settings['company'] }} · @endif {{ $type === \App\Enums\YmlSourceType::Wildberries ? 'Токен' : 'API-ключ' }} {{ $feed->maskedApiKey() }} @if ($feed->credentials_expires_at) · действует до {{ $feed->credentials_expires_at->format('d.m.Y') }} @endif
@if ($feed->last_success_at)
Последняя удачная синхронизация: {{ $feed->last_success_at->format('d.m.Y в H:i') }}.
@endif @if ($lastRun && $lastRun->status === \App\Enums\ImportRunStatus::Failed && $lastRun->error_message)
Последняя синхронизация не удалась: {{ $lastRun->error_message }}
@endif
@csrf @error('api_key')

{{ $message }}

@enderror
@else {{-- Адрес выгрузки: продавец вводит его один раз при подключении и потом нигде не видит — а свериться с ним нужно как раз тогда, когда товары перестали приходить. Поля для правки нет: смена адреса означает другой каталог, и делается она через поддержку. --}}
{{ $feed->source_type === \App\Enums\YmlSourceType::File ? 'Загруженный файл' : 'Адрес выгрузки' }} @if ($feed->url)
Открыть
@endif
{{ $feed->url ?: basename((string) $feed->file_path) }}
@if ($feed->last_success_at)
Последняя удачная загрузка: {{ $feed->last_success_at->format('d.m.Y в H:i') }}.
@elseif ($feed->failed_sync_count > 0)
Файл по этому адресу не загружается — неудачных попыток подряд: {{ $feed->failed_sync_count }}.
@endif
@endif
@csrf {{-- Два поля в ряд, дальше — переключатели, и только потом кнопка. Раньше всё лежало одной строкой с переносом: длинные подписи чекбоксов рвали ряд, и «Сохранить» оказывалась посреди текста. --}}
{{-- Тематика решает спорные названия: «Прокладка» бывает автомобильной и гигиенической, «Подшипник» — велосипедным и автомобильным. Мы её определяем сами, но последнее слово за продавцом: он знает свой ассортимент, а ошибка здесь уводит весь каталог целиком. --}}
{{-- У кабинета маркетплейса «своей страницы товара» нет: ссылка вела бы покупателя с teeu на сам маркетплейс. --}} @unless ($marketplace) @endunless
@if ($marketplace) {{-- Склады для наличия. Товар на своём складе продавец отгрузит покупателю teeu сам, со склада маркетплейса — нет, поэтому что считать остатком, решает он. Смена отметок пересчитывает наличие сразу, из остатков последнего прогона. --}} @php($selectedWarehouses = $feed->stockWarehouses())

Склады для наличия

@if ($type === \App\Enums\YmlSourceType::Wildberries) Товар «в наличии», если на отмеченных складах есть остаток. Складов Wildberries (FBW) в списке нет: оттуда заказ с teeu не отправить. Список складов обновляется при каждой синхронизации. @else Товар «в наличии», если на отмеченных складах есть свободный остаток. Склады Ozon (FBO) отмечайте, только если держите такой же запас у себя: оттуда заказ с teeu не отправить. Список складов обновляется при каждой синхронизации. @endif

@if ($feed->marketplaceWarehouses() === [])

Склады появятся после первой синхронизации.

@else
@csrf
@foreach ($feed->marketplaceWarehouses() as $warehouse) @endforeach
@error('stock_warehouses')

{{ $message }}

@enderror
@endif
@endif {{-- Наценка: ступени «от цены — до цены — процент». Накидывается на цены из выгрузки (округление вверх до рубля). Пусто = цены как есть. --}}

Наценка на цены

Задайте ступени: цена товара из выгрузки попадает в подходящий диапазон, и к ней прибавляется наценка. Итог округляется вверх до рубля. Оставьте «до» пустым у последней ступени — она сработает «и выше». Без строк наценка не применяется — цены отдаются как в выгрузке. Изменения пересчитывают уже загруженные товары сразу.

@csrf
@error('rules')

{{ $message }}

@enderror
{{-- Адрес склада: точка, ОТ которой считается срок доставки до города покупателя. Правится в любой момент — оценка доставки пересчитывается автоматически. --}}

Адрес склада (откуда доставка)

Точка отгрузки товаров этого фида. От неё считается срок доставки до города покупателя. У каждого фида адрес свой — можно менять когда угодно.

@csrf
{{-- Delivery zone: seller outlines where they deliver; buyers outside the zone see a badge. --}}

Зона доставки

Обведите на карте область, куда доставляете — города внутри добавятся автоматически (кликайте по карте, чтобы поставить углы; перетаскивайте угол, чтобы подвинуть; клик по углу — удалить его). Список ниже можно поправить вручную. Пусто = показываем и доставляем во все города. Покупатель из города вне зоны увидит товар с пометкой «доставка в ваш город недоступна».

Ctrl + колесо — масштаб
Определяю города…
{{-- Manual city add --}}
{{-- Selected cities: first 10, then «показать все» --}}
@csrf
{{-- Category mapping: seller confirms/fixes how feed categories map to teeu (ТЗ §11). --}}

{{ $marketplace ? 'Категории '.$type->label().' → категории teeu' : 'Категории фида → категории teeu' }}

@if ($unmappedCount > 0) Не сопоставлено: {{ $unmappedCount }} — их товары не показываются @endif
{{-- Категорию маркетплейса часть строк находит у нас по именам, без угадывания, — это видно в колонке «Кто». --}} @if ($type === \App\Enums\YmlSourceType::Ozon)

Каталог teeu вырос из категорий Ozon, поэтому многие типы товаров находятся у нас по названию — такие строки отмечены «Ozon». Остальные подобраны автоматически по смыслу, их стоит проверить.

@elseif ($type === \App\Enums\YmlSourceType::Wildberries)

Предметы Wildberries, которые нашлись в каталоге teeu по названию, отмечены «Wildberries». Остальные подобраны автоматически по смыслу, их стоит проверить.

@endif

Товары без категории teeu не публикуются. Проверьте сопоставление: «— не показывать —» скрывает товары этой папки, а «сборная категория» подбирает категорию каждому товару по его названию — для папок вроде «Товары» или «Разное», где лежит всё подряд. Строки «подобрать не удалось» ждут вашего выбора: по ним ответ был слишком неуверенным, и мы предпочли не гадать.

Когда несколько папок идут в одну категорию, выберите её один раз и нажмите значок копирования — в остальных строках появится значок вставки.

@php($savedRows = array_map('intval', (array) session('changed_maps', []))) @if ($totalMaps === 0)
Категории появятся после первого импорта фида.
@else {{-- Отбор строк. Тысяча папок в одной таблице — это не «длинно», это неработающая страница: браузер вешался на выгрузке с 3 316 строками. По умолчанию открываем то, что ждёт решения, остальное — по запросу. --}}
@foreach ([['todo', 'Ждут решения', $unmappedCount], ['all', 'Все папки', $totalMaps]] as [$key, $title, $count]) {{ $title }} — {{ $count }} @endforeach
@if ($search !== '') сбросить @endif
{{-- Shared category list (id + breadcrumb name) for the searchable pickers. --}} @php($catsJs = collect($teeuCategories)->map(fn ($label, $id) => ['id' => (string) $id, 'label' => $label])->values())
Скопировано:
@csrf
@foreach ($categoryMaps as $m) @php($curLabel = $m->category_id ? ($teeuCategories[$m->category_id] ?? '') : '') @php($justSaved = in_array($m->id, $savedRows, true)) {{-- x-data на строке, а не в ячейке: подсветить надо всю строку, а выбор живёт внутри — вложенное видит родительский scope, обратное неверно. --}} {{-- Пустая строка бывает двух совершенно разных видов, и до сих пор они выглядели одинаково — «— не показывать —». Первый: человек решил папку скрыть. Второй: подобрать не удалось — модель ответила слишком неуверенно (порог 0.75) или сказала, что подходящей категории нет. Второе — не решение, а незаконченная работа, и звать его «не показывать» значит врать: продавец видит «скрыто» там, где на самом деле ждут его выбора. --}} @php($unresolved = ! $m->category_id && ! $m->classify_per_item && $m->source === 'ai') @endforeach
{{ $marketplace ? 'Категория на '.$type->label() : 'Категория в фиде' }}Категория teeuКто
{{ $m->category_path ?? $m->external_category_id }}
{{-- Копировать/вставить: у фида бывают десятки папок, и половина уезжает в одну и ту же категорию. Буфер общий на страницу. Только иконки: путь категории в подписи кнопки занимал две строки и спорил за внимание с самим списком. Куда именно вставится — во всплывающей подсказке. --}}

Категорию подберём каждому товару по его названию. Это дольше и не всегда точно — если можно проставить категории в самой выгрузке, лучше так.

{{-- x-if, а не x-show: скрытый x-show всё равно держит в DOM все шестьдесят строк списка, и на тысяче папок браузер умирал ещё до первого клика. --}}
@switch($m->source) @case('seller') вы @break @case('admin') админ @break @case('ozon') Ozon @break @case('wildberries') Wildberries @break @default ИИ @endswitch
@if ($categoryMaps->isEmpty())
@if ($search !== '') По запросу «{{ $search }}» ничего не найдено. @else Все папки этой выгрузки разобраны. @endif
@endif @if ($categoryMaps->hasPages())
{{ $categoryMaps->links() }}
@endif
Сохраняются строки этой страницы — остальные остаются как были. Товары переедут в выбранные категории за несколько минут. Если из-за категории какие-то позиции не создались — запустится и повторная выгрузка, они появятся после неё.
{{-- Ответ по имени папки запоминается навсегда — в том числе ответ «не знаю». Значит строка, не поддавшаяся однажды, сама уже не разберётся, даже когда подбор стал точнее или в каталоге появилась нужная категория. Кнопка — единственный способ сказать «попробуй ещё раз», и нажимает её человек: подбор платный. --}} @if ($unresolvedCount > 0)
@csrf Не удалось подобрать: {{ $unresolvedCount }}. По этим папкам ответ был слишком неуверенным — выберите категории сами или попробуйте подобрать заново.
@endif @if ($savedRows !== []) {{-- Сохранение возвращает страницу к началу, а правят её сверху вниз и подолгу: без этого после каждого сохранения приходится заново искать глазами, где остановился. Ведём к последней сохранённой строке. --}} @endif @endif

История импортов

@forelse ($feed->runs as $run) @empty @endforelse
СтатусНайденоСозданоОбновленоБез изм.ПропущеноДеактив.ОшибкиНачало
@php($st = $run->status) {{ $st->label() }} {{ $run->offers_found }} {{ $run->created_count }} {{ $run->updated_count }} {{ $run->unchanged_count }} {{ $run->skipped_count }} {{ $run->deactivated_count }} {{ $run->failed_count }} {{ $run->started_at?->format('d.m H:i') }}
Импортов ещё не было.
@endsection