@php use App\Support\InvoiceDisplayFormatter as F; // ── Resolve the company logo as a base64 data URI so Dompdf, browser // print and email attachments all render the same image. Prefer the // canonical circular emblem shipped with the app over any wordmark // saved in Settings (which is the sidebar asset). --} $logoSrc = null; $tryEncode = function ($path) { if (!$path || !is_file($path) || !is_readable($path)) return null; $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $mime = $ext === 'jpg' || $ext === 'jpeg' ? 'image/jpeg' : ($ext === 'gif' ? 'image/gif' : ($ext === 'svg' ? 'image/svg+xml' : 'image/png')); return 'data:' . $mime . ';base64,' . base64_encode(file_get_contents($path)); }; foreach ([ public_path('images/stepup-circular-logo.png'), public_path('images/step-up-logo.png'), public_path('images/logo-round.png'), ] as $candidate) { if ($encoded = $tryEncode($candidate)) { $logoSrc = $encoded; break; } } $s = null; try { $s = \App\Models\Setting::find(1); } catch (\Throwable $e) { $s = null; } // White-label: use Company Settings + config('company.*'), no whitelisting. $rawCompanyName = trim((string) ($s->companyName ?? '')); $companyName = $rawCompanyName !== '' ? F::titleCase($rawCompanyName) : trim((string) (config('company.legal') ?: config('company.name', ''))); $companyAddress = $s->companyAddress ?? trim((string) (config('company.address.line1', '') . ' ' . config('company.address.line2', ''))); $companyPhone = $s->companyPhone ?? (string) config('company.phone', ''); $companyEmail = trim((string) ($s->companyEmail ?? config('company.email', ''))); $companyWebsite = $s->companyWebsite ?? (string) config('company.website', ''); // ── Money & date helpers (Dompdf: no closures in view scope). --} $fmtMoney = fn ($v) => '$' . number_format((float) $v, 2); $fmtDate = fn ($d) => $d ? \Carbon\Carbon::parse($d)->format('M j, Y') : '—'; // ── Student / parent bill-to block. --} $studentFirst = trim((string) ($invoice->student->first_name ?? '')); $studentLast = trim((string) ($invoice->student->last_name ?? '')); $studentName = F::titleCase(trim("$studentFirst $studentLast")) ?: '—'; $parentName = F::titleCase(trim((string) ($parent['name'] ?? ''))) ?: null; $parentEmail = $parent['email'] ?? null; $addressParts = array_filter([ $invoice->student->unit_number ?? null, $invoice->student->street ?? null, $invoice->student->city ?? null, $invoice->student->province ?? null, $invoice->student->postal_code ?? null, ]); $addressLine = $addressParts ? F::titleCase(implode(', ', $addressParts)) : '—'; $phone = $parent['phone'] ?? ($invoice->student->phone ?? null); $studentEmail = $invoice->student->email ?? null; $locationName = F::titleCase((string) ($invoice->student->location->location_name ?? '')) ?: null; // ── Billing period — prefer the invoice's own billing_period (YYYY-MM // added by the 2026_07_23 migration) so it reflects the CANONICAL // invoice month, not the accidental issueDate month. Fall back to // the first line's period, then to the issueDate month. --} $rawPeriod = $invoice->billing_period ?? null; $firstItem = ($items instanceof \Illuminate\Support\Collection) ? $items->first() : ($items[0] ?? null); if (!$rawPeriod && $firstItem && !empty($firstItem->billing_period)) { $rawPeriod = $firstItem->billing_period; } if (!$rawPeriod && $invoice->issueDate) { $rawPeriod = \Carbon\Carbon::parse($invoice->issueDate)->format('Y-m'); } $periodLabel = F::billingPeriodLabel($rawPeriod, $firstItem->billing_period_label ?? null); $statusRaw = strtolower((string) ($effective_status ?? $invoice->status ?? 'draft')); $statusLabel = F::statusLabel($statusRaw); @endphp @php // Watermark lives OUTSIDE .invoice-page so its `position: fixed` // containing block is the page/viewport itself — Dompdf then // repeats it on every generated PDF page. Placing it inside a // positioned ancestor would render it only once. The Print List // wrapper emits its own top-level watermark and passes // $skipWatermark=true so we don't double-stack them. $suppressWatermark = !empty($skipWatermark); @endphp @if($logoSrc && !$suppressWatermark) @endif
{{-- ─── Shared Step Up header (synced with invoice list PDF) ───── Everything except the right-column content (status badge here; report title + date on the list) is identical to invoice_list.blade.php. Keep them in lock-step. --}} @php // Map the invoice's effective status to soft-pill colours that // match the previous badge treatment. Kept inline here so the // shared header stays self-contained. $badgeMap = [ 'paid' => ['bg' => '#d4f0e2', 'fg' => '#0f6b35'], 'partial' => ['bg' => '#fef3c7', 'fg' => '#92400e'], 'overdue' => ['bg' => '#fee2e2', 'fg' => '#991b1b'], 'sent' => ['bg' => '#dbeafe', 'fg' => '#1d4ed8'], 'draft' => ['bg' => '#f1f5f9', 'fg' => '#475569'], 'pending' => ['bg' => '#f1f5f9', 'fg' => '#475569'], 'voided' => ['bg' => '#e5e7eb', 'fg' => '#4b5563'], 'cancelled' => ['bg' => '#e5e7eb', 'fg' => '#4b5563'], ]; $badge = $badgeMap[$statusRaw] ?? $badgeMap['draft']; @endphp
@if($logoSrc) @endif
{{ $companyName }}
{{ $companyAddress }}
Tel: {{ $companyPhone }}  ·  Email: {{ $companyEmail }}
{{-- Single-invoice variant: status badge (list report uses title + date + prepared-by here instead). --}}
Status
{{ $statusLabel }}
{{-- ─── Info grid: Bill To + Invoice Details (spec §5) ────────── --}}
Bill To
Student{{ $studentName }}
@if($parentName)
Parent / Guardian{{ $parentName }}
@endif
Address{{ $addressLine }}
@if($phone)
Phone{{ $phone }}
@endif @if($parentEmail || $studentEmail)
Email{{ $parentEmail ?: $studentEmail }}
@endif @if($locationName)
Location{{ $locationName }}
@endif
Invoice Details
Invoice Number{{ $invoice->invoiceNumber }}
Billing Period{{ $periodLabel }}
Issue Date{{ $fmtDate($invoice->issueDate) }}
Due Date{{ $fmtDate($invoice->dueDate) }}
Status{{ $statusLabel }}
{{-- ─── Line items (spec §6) ─────────────────────────────────── --}} @foreach($items as $i => $it) @php // Structured backend fields (spec §3) — never parse the description. $subject = F::titleCase((string) ($it->subject_name ?? '')); $grade = F::titleCase((string) ($it->grade_name ?? '')); $staff = F::titleCase((string) ($it->staff_name ?? '')); $loc = F::titleCase((string) ($it->location_name ?? '')); $modeLbl = F::teachingMode($it->teaching_mode ?? null); $classCode = trim((string) ($it->class_code ?? '')); $btToken = strtolower(preg_replace('/[\s_-]/', '', (string) ($it->billing_type ?? ''))); $isPerClass = str_contains($btToken, 'perclass'); $feeTypeLabel = $isPerClass ? 'Per Class' : 'Monthly Fee'; $qtyInt = (int) ($it->quantity ?? 0); $itemPeriod = F::billingPeriodLabel($it->billing_period ?? null, $it->billing_period_label ?? null); $chipParts = array_values(array_filter([ $classCode !== '' ? $classCode : null, $loc, $modeLbl, ])); $chip = implode(' • ', $chipParts); $head = $subject !== '' ? $subject : (trim((string) $it->description) ?: 'Line item'); @endphp @endforeach @if(count($items) === 0) @endif
Class Details Fee Type Rate Amount
{{-- Subject (bold) + grade on the same line. Subject already passes through F::titleCase() which preserves canonical acronyms (ICT, ABA, STEM, ESL) and title-cases the rest — so "SCIENCE / ICT" becomes "Science / ICT". --}}
{{ $head }}@if($grade !== '') — {{ $grade }}@endif
@if($staff !== '')
Staff: {{ $staff }}
@endif @if($chip !== '')
{{ $chip }}
@endif @if($itemPeriod !== '—')
{{ $itemPeriod }} {{ $isPerClass ? 'Per Class Fee' : 'Monthly Fee' }}
@endif @if($subject === '' && $staff === '' && !empty($it->description)) {{-- Legacy / manual line — no structured data, keep description. --}}
{{ $it->description }}
@endif
{{ $feeTypeLabel }}
@if($isPerClass && $qtyInt > 0)
{{ $qtyInt }} {{ $qtyInt === 1 ? 'class' : 'classes' }}
@endif
{{ $fmtMoney($it->unitPrice) }} {{ $fmtMoney($it->totalPrice) }}
No line items.
{{-- ─── Totals (spec §8) ────────────────────────────────────── Single full-width table so its box matches the width and alignment of the header, info, items, notes and footer boxes above and below — no empty spacer column, no floating card that reads narrower than everything else. --}} @if((float) $invoice->discountAmount > 0) @endif @if((float) $invoice->taxAmount > 0) @endif
Subtotal {{ $fmtMoney($invoice->subtotal) }}
Discount −{{ $fmtMoney($invoice->discountAmount) }}
Tax {{ $fmtMoney($invoice->taxAmount) }}
Total {{ $fmtMoney($invoice->totalAmount) }}
Balance Due {{ $fmtMoney($balance_due) }}
{{-- ─── Payment history ──────────────────────────────────────── Detailed per-payment table so the printed invoice reads as a complete transaction record — matches the Payment History section on the on-screen Invoice View. Row is compact but professional: Date · Receipt · Method · Reference · Amount · Status. Total Paid footer mirrors the Financial Summary. --}} @if(!empty($payments) && count($payments) > 0)
Payment History
Recorded payments applied to this invoice
@foreach($payments as $p) @php $pStatus = strtolower((string) ($p->status ?? 'completed')); $pStatusLbl = ucfirst($pStatus === '' ? 'completed' : $pStatus); $pillClass = in_array($pStatus, ['completed','pending','failed'], true) ? 'pill pill-' . $pStatus : 'pill pill-completed'; @endphp @endforeach
Date Receipt # Method Reference Amount Status
{{ $p->paymentDate ? \Carbon\Carbon::parse($p->paymentDate)->format('M j, Y') : '—' }} {{ $p->receipt_number ?? ('REC-' . $p->id) }} {{ $p->paymentMethod ?: '—' }} {{ $p->transactionId ?? '—' }} {{ $fmtMoney($p->amount) }} {{ $pStatusLbl }}
TOTAL PAID {{ $fmtMoney($paid_amount) }}
@else
No payments recorded.
@endif {{-- ─── Notes (only when the user actually typed one) ──────────── Skip the system-generated cover text (AssignmentBillingService writes "Auto-generated from class assignment (July 2026)." on every auto-invoice). Only render when a real human note is on the invoice — anything that isn't the auto-generated stub. --}} @php $noteText = trim((string) ($invoice->notes ?? '')); $isAutoNote = $noteText !== '' && preg_match('/^Auto-generated from class assignment(\s*\(.*\))?\.?\s*$/i', $noteText) === 1; @endphp @if($noteText !== '' && !$isAutoNote)
Notes {{ $noteText }}
@endif {{-- ─── Shared Step Up footer (synced with invoice list PDF) ──── --}}
Thank you for choosing {{ $companyName }}.
For questions regarding this invoice, contact {{ $companyEmail }} or {{ $companyPhone }}.