@extends('setting.nav') @section('title', 'Product Master') @section('content')

Product Master

What you sell, and for how much. A mail classified Pricing Enquiry says somebody wants a price; these rows are where that price comes from, and a quotation line will point at one of them. The catalogue belongs to the account — everyone in your team sees the same list, and only an administrator can change it.

@if ($isAdmin) @endif
@if (session('success')) @endif @if (session('error')) @endif @if ($errors->any())
Please fix the following:
@endif {{-- A catalogue that is entirely switched off looks fine on this screen — every row is present, just grey — and the consequence only appears on the reply screen as "nothing can be quoted". Said here instead, where the switches are. --}} @php $activeCount = $products->where('is_active', true)->count(); @endphp @if ($products->isNotEmpty() && $activeCount === 0 && $search === '')
None of your {{ $products->count() }} {{ $products->count() === 1 ? 'product is' : 'products are' }} active. Inactive products are not offered to the AI and cannot be quoted, so Prepare quotation on a pricing enquiry will have nothing to work from. Switch at least one on with the tick in the Active column.
@endif {{-- Units offered as suggestions rather than a closed list: a business with its own unit should not have to pick the wrong one. --}} @foreach ($units as $unit) @endforeach
{{-- ============================ CATALOGUE ============================ --}}
Products ({{ number_format($products->count()) }})
@if ($search !== '') Clear @endif
@forelse ($products as $product) @php // Priced from the loaded relation so the whole // catalogue costs one pricing query, not one per row. $current = $product->priceOn($today, 1); $bands = $product->pricing ->where('is_active', true) ->sortBy([['minimum_quantity', 'asc'], ['valid_from', 'desc']]); @endphp {{-- The price book for one product: every band and every window, current and historic. This is the evidence behind a quotation, so withdrawn rows stay visible. --}} @empty @endforelse
Product Code Price today Tax Unit Active Actions
{{ $product->name }}
@if ($product->description)
{{ \Illuminate\Support\Str::limit($product->description, 110) }}
@endif @if ($product->ai_aliases)
also matches: {{ $product->ai_aliases }}
@endif
{{ $product->product_code }} @if ($current) {{ number_format((float) $current->price, 2) }} {{ $current->currency }} @if ($bands->count() > 1)
{{ $bands->count() }} price bands
@endif @else {{-- No applicable price is a real state, not a zero: nothing can be quoted from this product today. --}} No price today @endif
{{ rtrim(rtrim(number_format((float) $product->tax_percentage, 2), '0'), '.') }}% {{ $product->unit }} @if ($isAdmin)
@csrf
@else {{ $product->is_active ? 'Active' : 'Inactive' }} @endif
@if ($isAdmin)
@csrf @method('DELETE')
@endif
Price book — {{ $product->product_code }} @if ($isAdmin) @endif
@if ($isAdmin) @endif @forelse ($product->pricing as $price) @if ($isAdmin) @endif @empty @endforelse
Price Quantity Applies Source State
{{ number_format((float) $price->price, 2) }} {{ $price->currency }} {{ $price->bandLabel() }} {{ $price->windowLabel() }} @if ($price->source === \App\Models\ProductPricing::SOURCE_IMPORT) Import @if ($price->import_id) #{{ $price->import_id }} @endif @else Entered @endif @if (!$price->is_active) Withdrawn @elseif ($current && $current->id === $price->id) Quoted today @elseif ($price->isCurrent()) In force @elseif ($price->valid_from && $price->validFromDate() > $today) Future @else Expired @endif @if ($price->is_active)
@csrf @method('DELETE')
@endif
No prices on file — nothing can be quoted from this product.
@if ($search !== '') Nothing matches "{{ $search }}". @else No products yet. Add one, or import your price list from a spreadsheet on the right. @endif
{{-- ============================== IMPORT ============================== --}}
@if ($isAdmin)
Import a price list

Rows are matched on product code: a code you already have is updated, a new one is created. Nothing is ever deleted by an import — a partial price list is normal, and an omission is not a retirement.

@csrf

@csrf
The file has to be shared as Anyone with the link — otherwise Google serves a sign-in page instead of the data.

Download the sample workbook
Column names are matched loosely — "Product Code", "SKU" and "Item Code" all work, and the header row does not have to be the first row.
@endif
Import history
@forelse ($imports as $run)
{{ \Illuminate\Support\Str::limit($run->file_name, 40) }}
{{ $run->created_at?->format('d M Y H:i') }} · {{ $run->source === \App\Models\PricingImport::SOURCE_DRIVE ? 'Google Drive' : 'Upload' }}
{{ $run->summaryLine() }}
{{ ucfirst($run->status) }}
@empty
No imports yet. Every run is recorded here with its counts and any rejected rows, so a price that looks wrong can be traced to the file that set it.
@endforelse
{{-- ============================== MODALS ============================== --}} @if ($isAdmin) @foreach ($products as $product) @endforeach @endif @endsection