@php /** * Profit by product across the whole invoice. * * A three-product sale reports one blended margin, and that average is where * a loss-maker hides. This breaks the sale back down: revenue is each * product's own share of the invoice, cost is what was tagged to it plus its * share of the job's shared spend, and the winner and the loser are named. */ $rows = $productProfit['rows']; $totals = $productProfit['totals']; $unlinkedJobs = $productProfit['unlinked_jobs']; $priced = collect($rows)->filter(fn ($r) => $r['revenue'] > 0 && $r['job'] !== null); $best = $priced->sortByDesc('margin')->first(); $worst = $priced->sortBy('margin')->first(); $spread = $priced->count() > 1 && $best && $worst ? round($best['margin'] - $worst['margin'], 1) : null; // One invoice is one job, so naming the producing job is normally noise. The // column appears only when something needs explaining: a product no job is // making, or older data where the sale really was spread over several jobs. $showJobColumn = collect($rows)->contains(fn (array $r): bool => $r['job'] === null) || collect($rows)->pluck('job.id')->filter()->unique()->count() > 1; $toneFor = fn (array $r) => $r['revenue'] <= 0 ? 'slate' : ($r['margin'] >= 30 ? 'emerald' : ($r['margin'] >= 10 ? 'amber' : 'rose')); $toneText = ['emerald' => 'text-emerald-600', 'amber' => 'text-amber-600', 'rose' => 'text-rose-600', 'slate' => 'text-slate-400']; $toneBar = ['emerald' => 'bg-emerald-500', 'amber' => 'bg-amber-500', 'rose' => 'bg-rose-500', 'slate' => 'bg-slate-300']; $tonePill = ['emerald' => 'bg-emerald-50 text-emerald-700 ring-emerald-200', 'amber' => 'bg-amber-50 text-amber-700 ring-amber-200', 'rose' => 'bg-rose-50 text-rose-700 ring-rose-200', 'slate' => 'bg-slate-100 text-slate-500 ring-slate-200']; @endphp

{{ __('Profit by product') }}

{{ __('Every product on :invoice, whichever job is producing it.', ['invoice' => $job->invoice?->invoice_number ?: __('this invoice')]) }}

@if ($canUpdate && $job->lineItems->count() > 1) @endif
{{-- Headline: how far apart the products actually are --}} @if ($spread !== null && $spread >= 5)
{{ __('Best') }} {{ $best['name'] }} {{ number_format($best['margin'], 1) }}% {{ __('Weakest') }} {{ $worst['name'] }} {{ number_format($worst['margin'], 1) }}% {{ __(':n points apart on the same sale', ['n' => number_format($spread, 1)]) }}
@endif
@if ($showJobColumn) @endif @foreach ($rows as $row) @php $tone = $toneFor($row); $isMine = $row['job'] && $row['job']->id === $job->id; $bar = max(0, min(100, (float) $row['margin'])); @endphp @if ($showJobColumn) @endif @endforeach
{{ __('Product') }}{{ __('Produced by') }}{{ __('Revenue') }} {{ __('Cost') }} {{ __('Gross Profit') }} {{ __('Margin') }}

{{ $row['name'] }}

{{ rtrim(rtrim(number_format($row['qty'], 2), '0'), '.') }} {{ __('units') }} @if ($row['shared_cost'] > 0) · {{ __('incl. shared costs') }} @endif

@if ($isMine) {{ __('This job') }} @elseif ($row['job']) {{ $row['job']->job_number }} @else {{ __('Not on this job') }} @endif {{ number_format($row['revenue'], 2) }} {{ $row['has_cost_data'] ? number_format($row['cost'], 2) : __('none logged') }} {{ number_format($row['profit'], 2) }}
{{ $row['revenue'] > 0 ? number_format($row['margin'], 1).'%' : '—' }}
{{ __('Whole sale') }} ({{ $baseCurrency }}) {{ number_format($totals['revenue'], 2) }} {{ number_format($totals['cost'], 2) }} {{ number_format($totals['profit'], 2) }} {{ number_format($totals['margin'], 1) }}%
@if ($unlinkedJobs->isNotEmpty())

{{ trans_choice( '{1} :jobs is on this invoice but is not linked to any product, so its costs sit outside the split above. Edit it and tick the products it produces.|[2,*] :jobs are on this invoice but are not linked to any product, so their costs sit outside the split above.', $unlinkedJobs->count(), ['jobs' => $unlinkedJobs->pluck('job_number')->implode(', ')] ) }}

@endif

{{ __('Tag a cost to a product when you record it and it lands here exactly. Untagged costs and logged time are shared across the job’s products by revenue.') }}