@php use Illuminate\Support\Carbon; // Labels come from Job::STATUS_LABELS so the board can never disagree with // the model about what a status is called. $statusMeta = [ 'pending' => ['label' => App\Models\Job::statusLabel('pending'), 'color' => 'slate', 'dot' => 'bg-slate-400'], 'in_production' => ['label' => App\Models\Job::statusLabel('in_production'), 'color' => 'sky', 'dot' => 'bg-sky-500'], 'on_hold' => ['label' => App\Models\Job::statusLabel('on_hold'), 'color' => 'amber', 'dot' => 'bg-amber-500'], 'completed' => ['label' => App\Models\Job::statusLabel('completed'), 'color' => 'emerald', 'dot' => 'bg-emerald-500'], 'ready_for_collection' => ['label' => App\Models\Job::statusLabel('ready_for_collection'), 'color' => 'amber', 'dot' => 'bg-amber-400'], 'delivered' => ['label' => App\Models\Job::statusLabel('delivered'), 'color' => 'violet', 'dot' => 'bg-violet-500'], 'closed' => ['label' => App\Models\Job::statusLabel('closed'), 'color' => 'slate', 'dot' => 'bg-slate-500'], 'cancelled' => ['label' => App\Models\Job::statusLabel('cancelled'), 'color' => 'rose', 'dot' => 'bg-rose-400'], ]; // The counter-facing state: a job sitting here is finished work waiting on // the customer, and the board has to say so at a glance. $isReady = fn ($job) => $job->status === 'ready_for_collection'; // Commercial state (spec §2), the funding dimension, separate from status. $commercialMeta = [ 'not_funded' => ['label' => __('Not Funded'), 'color' => 'slate'], 'deposit_paid' => ['label' => __('Deposit Paid'), 'color' => 'amber'], 'fully_paid' => ['label' => __('Fully Paid'), 'color' => 'emerald'], ]; // Categories carry no colour of their own, derive a stable accent so the // board reads like the reference (each category keeps one consistent hue). $categoryPalette = ['indigo', 'sky', 'violet', 'emerald', 'amber', 'rose']; $categoryColor = function ($category) use ($categoryPalette) { if (! $category) { return 'slate'; } return $categoryPalette[$category->id % count($categoryPalette)]; }; // Kanban progress bar takes the column's status colour (solid). $kanbanBar = fn ($job) => [ 'pending' => 'bg-slate-300', 'in_production' => 'bg-sky-500', 'on_hold' => 'bg-amber-500', 'completed' => 'bg-emerald-500', 'ready_for_collection' => 'bg-amber-400', 'delivered' => 'bg-emerald-500', 'closed' => 'bg-slate-500', ][$job->status] ?? 'bg-slate-300'; // Relative due-date pill text + colour. Once the work is made the due date // stops being a countdown and the cell just states where the job is — // except at the counter, where "waiting for the customer" is the point. $dueMeta = function ($job) use ($statusMeta) { if ($job->status === 'ready_for_collection') { return ['label' => $statusMeta[$job->status]['label'], 'cls' => 'text-amber-600 font-medium']; } if (in_array($job->status, ['completed', 'delivered', 'closed'], true)) { return ['label' => $statusMeta[$job->status]['label'], 'cls' => 'text-emerald-600']; } if (! $job->due_date) { return ['label' => '—', 'cls' => 'text-slate-400']; } $days = (int) Carbon::today()->diffInDays($job->due_date->copy()->startOfDay(), false); if ($days < 0) { return ['label' => __(':n days overdue', ['n' => abs($days)]), 'cls' => 'text-rose-600 font-medium']; } if ($days === 0) { return ['label' => __('Due today'), 'cls' => 'text-rose-600 font-medium']; } if ($days === 1) { return ['label' => __('1 day left'), 'cls' => 'text-rose-500']; } if ($days <= 3) { return ['label' => __(':n days left', ['n' => $days]), 'cls' => 'text-amber-600']; } return ['label' => __(':n days left', ['n' => $days]), 'cls' => 'text-slate-400']; }; $progressBar = fn ($job) => in_array($job->status, ['completed', 'delivered', 'closed'], true) ? 'bg-emerald-500' : ($job->status === 'ready_for_collection' ? 'bg-amber-400' : 'bg-gradient-to-r from-indigo-500 to-violet-500'); $sparkStroke = [ 'indigo' => '#124a61', 'sky' => '#0ea5e9', 'amber' => '#f59e0b', 'emerald' => '#10b981', 'violet' => '#8b5cf6', 'slate' => '#94a3b8', ]; @endphp
{{-- On a phone the toggle keeps only its icons and "New Job" takes the rest of the row, so the toolbar never widens the page. --}}
@if ($canManage) {{ __('Invoice') }} {{ __('New Job') }} @endif
@foreach ($stats as $stat) @php $pts = $stat['spark']; $max = max($pts) ?: 1; $w = 88; $h = 30; $n = count($pts); $step = $n > 1 ? $w / ($n - 1) : 0; $coords = []; foreach ($pts as $i => $v) { $x = round($i * $step, 1); $y = round($h - ($v / $max) * ($h - 4) - 2, 1); $coords[] = "{$x},{$y}"; } $line = implode(' ', $coords); $area = "0,{$h} " . $line . " {$w},{$h}"; $stroke = $sparkStroke[$stat['color']] ?? $sparkStroke['slate']; $iconBg = [ 'indigo' => 'bg-indigo-50 text-indigo-600', 'sky' => 'bg-sky-50 text-sky-600', 'amber' => 'bg-amber-50 text-amber-600', 'emerald' => 'bg-emerald-50 text-emerald-600', 'violet' => 'bg-violet-50 text-violet-600', ][$stat['color']] ?? 'bg-slate-100 text-slate-600'; $gid = 'spark-' . $loop->index; @endphp

{{ $stat['label'] }}

{{ $stat['count'] }}

{{ $stat['caption'] }}

@endforeach
@php $prodChip = [ 'indigo' => 'bg-indigo-50 text-indigo-600', 'sky' => 'bg-sky-50 text-sky-600', 'amber' => 'bg-amber-50 text-amber-600', 'emerald' => 'bg-emerald-50 text-emerald-600', 'rose' => 'bg-rose-50 text-rose-600', 'violet' => 'bg-violet-50 text-violet-600', 'slate' => 'bg-slate-100 text-slate-600', ]; $onTime = (float) $production['on_time_pct']; $onTimeOk = $onTime >= (float) $production['on_time_target_pct']; $reworkHigh = (bool) $production['high_rework']; $prodTiles = [ ['label' => __('On-Time Delivery'), 'value' => number_format($onTime, 1) . '%', 'icon' => 'check-circle', 'color' => $onTimeOk ? 'emerald' : 'amber', 'note' => __('target :t%', ['t' => number_format((float) $production['on_time_target_pct'], 0)])], ['label' => __('Rework Rate'), 'value' => number_format((float) $production['rework_pct'], 1) . '%', 'icon' => 'arrow-path', 'color' => $reworkHigh ? 'rose' : ((float) $production['rework_pct'] > 0 ? 'amber' : 'emerald'), 'note' => __(':n reworked', ['n' => $production['rework_jobs']])], ['label' => __('Avg Turnaround'), 'value' => number_format((float) $production['avg_turnaround_days'], 1) . ' ' . __('days'), 'icon' => 'clock', 'color' => 'sky', 'note' => __('start → done')], ['label' => __('Overdue'), 'value' => (string) $production['overdue'], 'icon' => 'exclamation-triangle', 'color' => $production['overdue'] > 0 ? 'rose' : 'emerald', 'note' => __('open & past due')], ['label' => __('Billable Hours'), 'value' => number_format((float) $production['billable_hours'], 1), 'icon' => 'clock', 'color' => 'amber', 'note' => __(':p% of :t hrs', ['p' => number_format((float) $production['billable_pct'], 0), 't' => number_format((float) $production['job_hours'], 1)])], ]; // Cost-vs-estimate is a money variance, hidden from Production users. if ($canViewCostVariance) { $prodTiles[] = ['label' => __('Cost vs Estimate'), 'value' => ($production['cost_variance'] > 0 ? '+' : '') . $production['base_currency'] . ' ' . number_format((float) $production['cost_variance'], 2), 'icon' => 'calculator', 'color' => $production['cost_variance'] > 0 ? 'rose' : 'emerald', 'note' => __(':n with estimate', ['n' => $production['jobs_with_estimate']])]; } @endphp

{{ __('Production Performance') }}

{{ $productionPeriodLabel }} · {{ $production['jobs_completed'] }} {{ __('completed') }}

@if ($reworkHigh) {{ __('High rework') }} @endif
@foreach ($prodTiles as $tile)

{{ $tile['label'] }}

{{ $tile['value'] }}

{{ $tile['note'] }}

@endforeach
@php // How many of the non-search filters are actually set. On a phone the // filter bank is folded away, so this count is the only signal that // the list you are looking at is a filtered one. $activeFilterCount = collect([$statusFilter, $categoryFilter, $customerFilter, $startDate, $endDate]) ->filter(fn ($v) => filled($v))->count(); @endphp {{-- Phone: search stays out, the filter bank folds behind a button so the list starts near the top of the screen instead of five full-width selects down. From lg the bank is always open and the whole thing is the old single row. --}}
{{-- One filter bank, not two: `hidden`/`flex` is bound so the phone can fold it, and `lg:flex` (a later rule than `hidden`) forces it back open on a laptop whatever Alpine put there. Duplicating the markup instead would ship the whole customer list twice. --}}
@if ($viewMode === 'kanban')
@foreach ($statuses as $col) @php $cards = $board[$col] ?? collect(); @endphp
{{ $statusMeta[$col]['label'] }} {{ $cards->count() }} @if ($canManage) @endif
@forelse ($cards as $job) @php $p = (int) $job->progress; $showCurrency = $job->currency_code && $job->currency_code !== $baseCurrency; @endphp
{{ $job->title }} @if ($canManage) {{ __('View') }}
{{-- Only the moves the FRS §3 workflow allows from here, so the menu never offers a jump the gate would just refuse. --}}

{{ __('Move to') }}

@forelse ($job->allowedTransitions() as $target) @empty

{{ __('This job is finished.') }}

@endforelse @if ($canDelete)
@endif
@endif
{{ $job->job_number }}

{{ $job->customer?->name ?: __('No customer') }}

@if ($job->due_date)

{{ $job->due_date->format('Y-m-d') }}

@endif @if ($job->category || $showCurrency)
@if ($showCurrency) {{ $job->currency_code }} @endif @if ($job->category) {{ $job->category->name }} @endif
@endif @if ($isReady($job))
{{ $job->completed_at ? __('Waiting since :date', ['date' => $job->completed_at->format('d M')]) : __('Waiting for the customer') }}
@endif @if ($col === 'on_hold' && $job->description)
{{ $job->description }}
@endif
@if ($job->value !== null) {{ $job->value_currency }} {{ number_format($job->value, 2) }} @if ((float) ($job->invoice?->refunded_total ?? 0) > 0) − {{ $job->value_currency }} {{ number_format($job->invoice->refunded_total, 2) }} {{ __('refunded') }} @endif @else — @endif {{ $p }}%
@empty

{{ __('No jobs') }}

@endforelse
@if ($canManage) @endif
@endforeach
@foreach ($statuses as $s) {{ $statusMeta[$s]['label'] }} @endforeach
@else @php $sortIcon = fn ($field) => $sortField === $field ? ($sortDirection === 'asc' ? '▲' : '▼') : ''; @endphp @if (count($selected) > 0)

{{ __(':count selected', ['count' => count($selected)]) }}

@if ($canDelete) @endif
@endif
@forelse ($jobs as $job) @php $due = $dueMeta($job); $p = (int) $job->progress; @endphp
{{ $job->title }} {{ $job->job_number }}

{{ $job->customer?->name ?: '—' }}

{{ $statusMeta[$job->status]['label'] ?? $job->status }} @if ($job->category) {{ $job->category->name }} @endif

{{ $p }}%

{{ $due['label'] }}

@if ($job->value !== null)
{{ $job->value_currency }} {{ number_format($job->value, 2) }} @if ((float) ($job->invoice?->refunded_total ?? 0) > 0) − {{ $job->value_currency }} {{ number_format($job->invoice->refunded_total, 2) }} {{ __('refunded') }} @endif
@endif
@if ($canManage) @endif @if ($canDelete) @endif
@empty
@if ($canManage) {{ __('New Job') }} @endif
@endforelse
@if ($jobs && $jobs->total() > 0)
{{ __('Showing :from to :to of :total jobs', ['from' => $jobs->firstItem(), 'to' => $jobs->lastItem(), 'total' => $jobs->total()]) }}
{{ $jobs->onEachSide(1)->links() }}
@endif
@endif
{{ $editingId ? __('Edit Job') : __('New Job') }}
{{-- A job must originate from a sale: choose the invoice, then the product(s) it fulfils. --}}

{{ __('A job is created from a sale, pick the invoice it fulfils.') }}

@if ($invoice_id)
@if ($invoiceLineItems->isEmpty())

{{ __('This invoice has no line items to build a job from.') }}

@else
@foreach ($invoiceLineItems as $item) @endforeach
@endif

{{ __('Tick which product(s) this job is for.') }}

@endif
{{ $selectedInvoice?->customer?->name ?? __('— set from the invoice —') }}
{{ $progress }}%

{{ __('Completed and delivered jobs are always shown at 100%.') }}

{{ __('Record the outsourced portion as a cost line linked to a supplier.') }}

{{-- A hybrid job does not name one partner: each product says whether it is made in-house or sent out, and to whom. --}} @if ($fulfilment === 'hybrid')

{{ __('Once the job is saved, open its Products tab to mark which products go out and to which partner.') }}

@endif @if ($fulfilment === 'outsourced')
@if ($partners->isEmpty())

{{ __('No partners yet.') }} {{ __('Add one') }}.

@else

{{ __('The company this job is outsourced to.') }}

@endif
@endif
{{ __('Cancel') }} {{ __('Save') }}