@php use Illuminate\Support\Carbon; use Illuminate\Support\Str; // Labels come from Job::STATUS_LABELS; only the colours live here. $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'], ]; // 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'], ]; $priorityColor = ['low' => 'slate', 'normal' => 'sky', 'high' => 'amber', 'urgent' => 'rose']; $fulfilmentMeta = [ 'internal' => ['label' => __('Internal'), 'color' => 'slate'], 'outsourced' => ['label' => __('Outsourced'), 'color' => 'rose'], 'hybrid' => ['label' => __('Hybrid'), 'color' => 'violet'], ]; $initials = function (?string $name): string { $words = preg_split('/\s+/', trim((string) $name)) ?: []; return strtoupper(Str::substr($words[0] ?? '?', 0, 1) . (isset($words[1]) ? Str::substr($words[1], 0, 1) : '')) ?: '?'; }; $dueLabel = null; // Once the work is made the countdown stops being the story. if ($job->due_date && ! in_array($job->status, ['completed', 'ready_for_collection', 'delivered', 'closed'], true)) { $d = (int) Carbon::today()->diffInDays($job->due_date->copy()->startOfDay(), false); if ($d < 0) $dueLabel = ['t' => __(':n days overdue', ['n' => abs($d)]), 'c' => 'text-rose-500']; elseif ($d === 0) $dueLabel = ['t' => __('Due today'), 'c' => 'text-rose-500']; elseif ($d <= 3) $dueLabel = ['t' => __(':n days left', ['n' => $d]), 'c' => 'text-amber-600']; else $dueLabel = ['t' => __(':n days left', ['n' => $d]), 'c' => 'text-slate-400']; } $tabs = [ 'overview' => ['label' => __('Overview'), 'icon' => 'squares-2x2'], 'details' => ['label' => __('Details'), 'icon' => 'identification'], 'products' => ['label' => __('Products'), 'icon' => 'cube'], 'financials' => ['label' => __('Costs & Materials'), 'icon' => 'currency-dollar'], 'production' => ['label' => __('Workflow'), 'icon' => 'cog-6-tooth'], 'checklist' => ['label' => __('Quality'), 'icon' => 'shield-check'], 'time' => ['label' => __('Time Tracking'), 'icon' => 'clock'], 'files' => ['label' => __('Files'), 'icon' => 'document-text'], 'activity' => ['label' => __('Activity'), 'icon' => 'chart-bar'], 'notes' => ['label' => __('Notes'), 'icon' => 'pencil-square'], ]; // Literal class strings (Tailwind scans these in source, never interpolate fragments). $tlChip = [ 'indigo' => 'bg-indigo-50 text-indigo-600', 'sky' => 'bg-sky-50 text-sky-600', 'violet' => 'bg-violet-50 text-violet-600', 'emerald' => 'bg-emerald-50 text-emerald-600', 'amber' => 'bg-amber-50 text-amber-600', 'rose' => 'bg-rose-50 text-rose-600', 'slate' => 'bg-slate-100 text-slate-500', ]; $ratingText = [ 'emerald' => 'text-emerald-600', 'sky' => 'text-sky-600', 'amber' => 'text-amber-600', 'rose' => 'text-rose-600', 'slate' => 'text-slate-500', ]; @endphp
{{-- Counter banner: the job is made and waiting for the customer. The one question the front desk is asked all day, answered at the top of the page with the hand-over action right beside it. --}} @if ($job->isReadyForCollection())

{{ __('Ready for collection') }}

@if ($awaitingCollectionSince) {{ __('Finished on :date and waiting at the counter for :customer.', ['date' => $awaitingCollectionSince->format('d M Y'), 'customer' => $job->customer?->name ?: __('the customer')]) }} @else {{ __('Waiting at the counter for :customer.', ['customer' => $job->customer?->name ?: __('the customer')]) }} @endif

@if ($canUpdate)
{{ __('Delivery note') }}
@endif
@endif {{-- Safety net: finished but no materials were ever recorded. --}} @if ($needsMaterialsAttention)

{{ __('This job is finished but no materials were recorded') }}

{{ __('No stock was deducted and no material cost was logged. Did you use any stock? Add it under Production, or confirm none were needed.') }}

@if ($canUpdate)
@endif
@endif

{{ $job->title }}@if ($job->customer) — {{ $job->customer->name }}@endif

{{ $statusMeta[$job->status]['label'] ?? $job->status }} {{ $commercialMeta[$job->commercial_state]['label'] ?? __('Not Funded') }} {{ ucfirst($job->priority) }} {{ __('Priority') }} @php // A hybrid job says which of its products go out, so the // badge says how many rather than just "hybrid". $outsourcedProducts = $job->fulfilment === 'hybrid' ? $job->lineItems->filter(fn ($line) => $line->pivot->fulfilment === 'outsourced')->count() : 0; @endphp {{ $fulfilmentMeta[$job->fulfilment]['label'] ?? ucfirst($job->fulfilment ?? 'internal') }} @if ($job->fulfilment === 'hybrid') · {{ __(':n of :total outsourced', ['n' => $outsourcedProducts, 'total' => $job->lineItems->count()]) }} @elseif ($job->partner) · {{ $job->partner->name }} @endif @if ($job->category) {{ $job->category->name }} @endif {{-- A multi-product sale is still one job; the Products tab is where its products are managed apart. --}} @if ($job->lineItems->count() > 1) @endif
{{-- Header actions. On a phone the status picker takes the width it needs and the buttons wrap under it; nothing is allowed to hold the row wider than the screen. --}}
@if ($canUpdate) {{-- Status changer (relocated from the old info strip). --}}
{{ __('Saving…') }}
@endif @if ($canUpdate) @if ($job->lineItems->count() > 1) @endif @endif @if ($job->invoice) {{ __('View linked invoice') }} @endif @if ($job->quote) {{ __('View linked quote') }} @endif {{ __('Download QC certificate') }} {{ __('Create delivery note') }} @if ($portalUrl) {{ __('Open client portal') }} @endif @if ($canDelete)
@endif
@php $timeEntryCount = $job->timeEntries->count(); $reworkCount = (int) $job->rework_count; $pillTone = [ 'emerald' => 'bg-emerald-50 text-emerald-700', 'rose' => 'bg-rose-50 text-rose-700', 'sky' => 'bg-sky-50 text-sky-700', 'amber' => 'bg-amber-50 text-amber-700', 'violet' => 'bg-violet-50 text-violet-700', 'slate' => 'bg-slate-100 text-slate-600', ]; $statTiles = [ ['label' => __('Reworks'), 'value' => $reworkCount.' '.trans_choice('{1}reprint|[0,2,*]reprints', $reworkCount), 'fa' => 'fa-arrows-rotate', 'color' => $reworkCount > 0 ? 'amber' : 'slate', 'valueColor' => $reworkCount > 0 ? 'text-amber-600' : 'text-slate-900', 'action' => $canUpdate ? ['method' => 'logRework', 'confirm' => __('Log a rework/reprint for this job?'), 'label' => __('Log rework')] : null], ['label' => __('Linked Invoice'), 'value' => $job->invoice?->invoice_number ?: __('None'), 'fa' => 'fa-file-invoice-dollar', 'color' => 'indigo', 'href' => $job->invoice ? route('invoices.preview', $job->invoice_id) : null, 'caption' => $job->invoice ? __('View invoice') : __('Not linked to a sale')], ['label' => __('Rating'), 'value' => $rating['label'], 'fa' => 'fa-scale-balanced', 'color' => $rating['color'], 'valueColor' => $ratingText[$rating['color']] ?? 'text-slate-500', 'pill' => $revenue > 0 ? number_format($margin, 1).'% '.__('margin') : __('No revenue'), 'pillColor' => $rating['color']], ['label' => __('Progress'), 'value' => $job->progress.'%', 'fa' => 'fa-chart-column', 'color' => 'indigo', 'bar' => (int) $job->progress, 'caption' => $statusMeta[$job->status]['label'] ?? $job->status], ['label' => __('Time Logged'), 'value' => number_format($hours, 2).' h', 'fa' => 'fa-clock', 'color' => 'violet', 'caption' => trans_choice('{0} No entries|{1} :count entry|[2,*] :count entries', $timeEntryCount, ['count' => $timeEntryCount])], ]; @endphp
@foreach ($statTiles as $tile) @php $chip = $tlChip[$tile['color']] ?? $tlChip['slate']; @endphp

{{ $tile['label'] }}

@if (! empty($tile['href'])) {{ $tile['value'] }} @else {{ $tile['value'] }} @endif

@if (! empty($tile['pill'])) {{ $tile['pill'] }} @endif @if (isset($tile['bar']))
@endif @if (! empty($tile['caption']))

{{ $tile['caption'] }}

@endif @if (! empty($tile['action'])) @endif
@endforeach

{{ __('Financial Overview') }}

{{ __('All values in :code', ['code' => $baseCurrency]) }}

{{ __('Job Timeline') }}

    @forelse ($timeline->reverse()->take(5)->reverse() as $event)
  1. {{ $event['title'] }}

    @if ($event['by'])

    {{ __('by :name', ['name' => $event['by']]) }}

    @endif
    {{ $event['at']?->format('Y-m-d H:i') }}
  2. @empty
  3. {{ __('No activity yet.') }}
  4. @endforelse
{{-- Client feedback, management only --}} @if ($canViewFeedback)

{{ __('Client Feedback') }}

{{ __('Visible to management only.') }}

@if ($portalUrl) {{ __('Client portal') }} @endif
@if ($feedback && $feedback->submitted_at) @php $dims = ['rating_quality' => __('Quality'), 'rating_turnaround' => __('Turnaround'), 'rating_service' => __('Service')]; @endphp
@foreach ($dims as $field => $label)

{{ $label }}

@for ($n = 1; $n <= 5; $n++) ★ @endfor {{ $feedback->$field ? $feedback->$field.'/5' : '—' }}
@endforeach
@if ($feedback->comment)
“{{ $feedback->comment }}”
@endif
{{ __('Submitted :date', ['date' => $feedback->submitted_at->format('Y-m-d H:i')]) }} @if ($feedback->allow_testimonial) {{ __('Testimonial consent given') }} @endif
@else

@if ($job->client_portal_sent_at) {{ __('Review invite sent :date, awaiting the client’s feedback.', ['date' => $job->client_portal_sent_at->format('Y-m-d')]) }} @else {{ __('No feedback yet. The client is invited automatically when the job is marked Delivered.') }} @endif

{{-- The review chase applies from the moment the customer has the goods, whether that was a delivery or a counter collection. --}} @if (in_array($job->status, App\Models\Job::HANDED_OVER_STATUSES, true)) @php $waMessage = __(':company, your order :job is ready! View it and leave us a quick review here: :url', [ 'company' => $job->team?->name, 'job' => $job->job_number, 'url' => $portalUrl, ]); $waLink = \App\Support\WhatsApp::link($job->customer?->phone, $waMessage); @endphp
@if ($waLink) {{ __('Send via WhatsApp') }} @else {{ __('Add a client phone number to enable WhatsApp.') }} @endif
@endif @endif
@endif @include('livewire.jobs.partials.stepper')
@php $productCount = $job->lineItems->count(); $qtyTotal = $job->lineItems->sum(fn ($l) => (float) $l->qty); $reworkCount = (int) $job->rework_count; // Tinted icon-chip themes (literal classes so Tailwind scans them). $chipTone = [ 'indigo' => 'bg-indigo-50 text-indigo-600', 'sky' => 'bg-sky-50 text-sky-600', 'violet' => 'bg-violet-50 text-violet-600', 'emerald' => 'bg-emerald-50 text-emerald-600', 'amber' => 'bg-amber-50 text-amber-600', 'rose' => 'bg-rose-50 text-rose-600', 'slate' => 'bg-slate-100 text-slate-500', ]; // Grouped into sections for a clean, scannable layout. $detailSections = [ [ 'title' => __('Overview'), 'rows' => [ ['fa' => 'fa-hashtag', 'color' => 'indigo', 'label' => __('Job Number'), 'value' => $job->job_number], ['fa' => 'fa-user', 'color' => 'sky', 'label' => __('Customer'), 'value' => $job->customer?->name ?: '—'], ['fa' => 'fa-layer-group', 'color' => 'violet', 'label' => __('Category'), 'value' => $job->category?->name ?: '—'], ['fa' => 'fa-box', 'color' => 'sky', 'label' => __('Products'), 'value' => trans_choice('{0} None|{1} :count item|[2,*] :count items', $productCount, ['count' => $productCount])], ['fa' => 'fa-list-ol', 'color' => 'slate', 'label' => __('Quantity'), 'value' => rtrim(rtrim(number_format($qtyTotal, 2), '0'), '.') . ' ' . __('units')], ['fa' => 'fa-user-pen', 'color' => 'violet', 'label' => __('Created By'), 'value' => $job->creator?->name ?: '—'], ], ], [ 'title' => __('Commercial'), 'rows' => [ ['fa' => 'fa-circle-check', 'color' => 'emerald', 'label' => __('Status'), 'value' => $statusMeta[$job->status]['label'] ?? $job->status], ['fa' => 'fa-money-bill-wave', 'color' => 'emerald', 'label' => __('Commercial State'), 'value' => $commercialMeta[$job->commercial_state]['label'] ?? __('Not Funded')], ['fa' => 'fa-flag', 'color' => 'amber', 'label' => __('Priority'), 'value' => ucfirst($job->priority) . ' ' . __('Priority')], ['fa' => 'fa-truck-fast', 'color' => 'violet', 'label' => __('Fulfilment'), 'value' => ($fulfilmentMeta[$job->fulfilment]['label'] ?? ucfirst($job->fulfilment ?? 'internal')) . ($job->fulfilment === 'hybrid' ? ' · '.__(':n of :total products outsourced', ['n' => $outsourcedProducts, 'total' => $job->lineItems->count()]) : ($job->partner ? ' · '.$job->partner->name : ''))], ['fa' => 'fa-coins', 'color' => 'amber', 'label' => __('Currency'), 'value' => $job->currency_code ?: '—'], ['fa' => 'fa-rotate', 'color' => $reworkCount > 0 ? 'rose' : 'slate', 'label' => __('Reworks'), 'value' => $reworkCount . ' ' . trans_choice('{1}reprint|[0,2,*]reprints', $reworkCount)], ['fa' => 'fa-file-invoice-dollar','color' => 'indigo', 'label' => __('Linked Invoice'), 'value' => $job->invoice?->invoice_number ?: '—', 'href' => $job->invoice ? route('invoices.preview', $job->invoice_id) : null], ['fa' => 'fa-file-lines', 'color' => 'sky', 'label' => __('Linked Quote'), 'value' => $job->quote?->quote_number ?: '—', 'href' => $job->quote ? route('quotes.preview', $job->quote_id) : null], ], ], [ 'title' => __('Timeline'), 'rows' => [ ['fa' => 'fa-calendar-day', 'color' => 'rose', 'label' => __('Due Date'), 'value' => $job->due_date?->format('M j, Y') ?: '—'], ['fa' => 'fa-play', 'color' => 'emerald', 'label' => __('Started'), 'value' => optional($job->started_at)->format('M j, Y') ?: __('Not started')], ['fa' => 'fa-flag-checkered', 'color' => 'emerald', 'label' => __('Completed'), 'value' => optional($job->completed_at)->format('M j, Y') ?: '—'], ['fa' => 'fa-calendar-plus', 'color' => 'slate', 'label' => __('Created'), 'value' => $job->created_at?->format('M j, Y · H:i')], ['fa' => 'fa-clock-rotate-left', 'color' => 'slate', 'label' => __('Last Updated'), 'value' => $job->updated_at?->format('M j, Y · H:i')], ], ], ]; @endphp

{{ __('Job Details') }}

{{ __('Everything on record for this job.') }}

@foreach ($detailSections as $section)

{{ $section['title'] }}

@foreach ($section['rows'] as $i => $row) {{-- Zebra by visual row (two per row on desktop) for a clean even effect. --}} @php $tint = intdiv($i, 2) % 2 === 0 ? 'bg-white' : 'bg-slate-50'; @endphp
{{ $row['label'] }}
@if (! empty($row['href'])) {{ $row['value'] }} @else {{ $row['value'] }} @endif
@endforeach
@endforeach @if ($job->description)

{{ __('Description') }}

{{ $job->description }}

@endif
{{-- One invoice is one job, so a multi-product sale is not several jobs. This is where its products are arranged into units of work and managed apart: own state, dates, owner, notes and margin. --}}
{{-- Profitability totals already live in the job header cards above, so this tab shows no duplicate strip, just the working ledger, split into three mini sub-tabs (Production Costs / Production History / Job Funding). --}}
@if ($invoiceNative && $invoiceNative['code'] !== $baseCurrency)

{{ __('Invoice billed in :code :total', ['code' => $invoiceNative['code'], 'total' => number_format($invoiceNative['total'], 2)]) }}

@endif {{-- Per-product P&L. Shown whenever the sale has more than one product, because that is exactly when one blended margin hides a loss-maker. --}} @if ($productProfit && count($productProfit['rows']) > 1) @include('livewire.jobs.partials.product-profit') @endif {{-- Sub-tab nav --}}
@if ($canManageFinance) @endif
{{-- Reopen the post-completion review to log costs that landed late and optionally recharge the client. --}} @if ($canUpdate && $canCostJob && in_array($job->status, ['completed', 'ready_for_collection', 'delivered', 'closed'], true)) @endif
@if ($canManageFinance)
@endif
@include('livewire.jobs.partials.stepper')

{{ __('Production Details') }}

{{ __('Materials & consumption →') }}
{{ __('Status') }}
{{ $statusMeta[$job->status]['label'] ?? $job->status }}
{{ __('Priority') }}
{{ ucfirst($job->priority) }}
{{ __('Fulfilment') }}
{{ $fulfilmentMeta[$job->fulfilment]['label'] ?? ucfirst($job->fulfilment ?? 'internal') }}
{{ __('Category') }}
{{ $job->category?->name ?: '—' }}
{{ __('Sales Rep') }}
{{ $job->salesRep?->name ?: ($job->invoice?->creator?->name ?: ($job->creator?->name ?: '—')) }}
{{ __('Started') }}
{{ optional($job->started_at)->format('Y-m-d') ?: '—' }}
{{ __('Completed') }}
{{ optional($job->completed_at)->format('Y-m-d') ?: '—' }}
{{-- The hand-over moment, which §12 on-time delivery measures to. --}}
{{ __('Handed Over') }}
{{ optional($job->delivered_at)->format('Y-m-d') ?: '—' }}
{{ __('Due Date') }}
{{ $job->due_date?->format('Y-m-d') ?: '—' }}
{{ __('Progress') }}
{{ $job->progress }}%
{{-- Delivery-note interplay: the paper trail behind the hand-over. Notes are raised against the job, so a delivered job with none is a gap somebody should see. --}}
{{ __('Delivery Notes') }}
@if ($deliveryNotes->isNotEmpty())
@foreach ($deliveryNotes as $note) {{ $note->delivery_number }} {{ ucfirst($note->status) }} @endforeach
@else
{{ in_array($job->status, App\Models\Job::HANDED_OVER_STATUSES, true) ? __('Handed over with no delivery note on file.') : __('None raised yet.') }}
@endif
@if ($job->description)
{{ __('Description') }}
{{ $job->description }}
@endif

{{ __('Team Members') }}

    @forelse ($members as $member) @php $u = $member['user']; $mh = round($member['seconds'] / 3600, 2); @endphp
  • @if ($u->profile_photo_url) {{ $u->name }} @else {{ $initials($u->name) }} @endif

    {{ $u->name }}

    {{ $u->email }}

    @if ($member['lead']) {{ __('Lead') }} @endif @if ($mh > 0)

    {{ number_format($mh, 2) }} h

    @endif
  • @empty
  • {{ __('No one has logged time yet.') }}
  • @endforelse
@include('livewire.jobs.partials.files')

{{ __('Activity Feed') }}

    @forelse ($timeline->reverse() as $event)
  1. {{ $event['title'] }}

    @if ($event['by'])

    {{ __('by :name', ['name' => $event['by']]) }}

    @endif
    {{ $event['at']?->format('Y-m-d H:i') }}
  2. @empty
  3. {{ __('No activity recorded yet.') }}
  4. @endforelse
@include('livewire.jobs.partials.notes')
{{-- Post-completion prompt: capture extra costs + optionally bill the client for the difference. Persistent (outside the tabs) so it can open on any status change. Management only, the review stays pending (and prompts them) when a production user is the one who finishes the job. --}} @if ($canCostJob) @endif {{ __('Edit Job') }}
{{ $selectedInvoice?->customer?->name ?? $job->customer?->name ?? __('— set from the invoice —') }}

{{ __('Who sold this job. Commission follows the rep, not whoever raised the invoice.') }}

{{ $progress }}%
@if ($fulfilment === 'hybrid')

{{ __('Mark which products go out, and to which partner, on the Products tab.') }}

@endif

{{ __('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
{{ __('Cancel') }} {{ __('Save') }}
@switch ($gateKind) @case('deposit') {{ __('Deposit required before production') }} @break @case('closure') {{ __('This job has not been invoiced') }} @break @default {{ __('Required checks not complete') }} @endswitch @if ($gateKind === 'closure')

{{ __('A job can only be closed once it has been billed. Closing an unbilled job writes off work nobody will ever chase.') }}

{{ __('Link an invoice to this job, then close it. An unpaid invoice is fine — the balance stays on the aged receivables report.') }}
@elseif ($gateKind === 'deposit')

{{ __('Production can\'t start on ":status" because this job\'s invoice has no payment yet. Record a deposit on the invoice, or override to start anyway.', ['status' => $statusMeta[$gateTarget]['label'] ?? $gateTarget]) }}

{{ __('A job is "won" once its invoice receives a deposit or payment.') }}
@else

{{ __('Before this job can move to ":status", these required quality checks must be signed off:', ['status' => $statusMeta[$gateTarget]['label'] ?? $gateTarget]) }}

    @foreach ($gateOutstanding as $label)
  • {{ $label }}
  • @endforeach

{{ __('Open the Quality tab to complete them.') }}

@endif @if ($canOverride)
@else

{{ __('Only an owner or admin can override this gate.') }}

@endif
{{ __('Close') }} @if ($gateKind === 'closure') {{ __('Raise an invoice') }} @elseif ($gateKind === 'deposit') {{ __('Go to invoice') }} @else @endif @if ($canOverride) {{ __('Override & change status') }} @endif