Data display
Invoice code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/invoice.blade.php
@props([
'number' => null,
'status' => 'due',
'from' => null,
'to' => null,
'issuedOn' => null,
'dueOn' => null,
'items' => [],
'currency' => '$',
'subtotal' => null,
'discount' => null,
'tax' => null,
'total' => null,
'amountDue' => null,
'notes' => null,
'title' => 'Invoice',
])
@php
/*
| Printable invoice / receipt. Renders a branded header with the invoice
| number and a status badge, billed-to / billed-from blocks, a line-item
| table and an aligned totals summary. Numeric amounts are formatted with the
| `currency` symbol; pre-formatted strings are passed through untouched.
|
| from / to: ['name' => ?, 'email' => ?, 'lines' => ['Street', 'City']]
| items: ['description' => ?, 'detail' => ?, 'qty' => ?, 'price' => ?, 'amount' => ?]
| tax: a number/string, or ['label' => 'Tax (8%)', 'amount' => 12.40]
|
| Use the `actions` slot for print / download buttons and `logo` for a mark.
|
| Usage:
| <x-ui.invoice
| number="INV-2026-014" status="paid" issued-on="Jun 1, 2026" due-on="Jun 15, 2026"
| :from="['name' => 'Harbour, Inc.', 'lines' => ['548 Market St', 'San Francisco, CA']]"
| :to="['name' => 'Ada Lovelace', 'email' => 'ada@example.com']"
| :items="[['description' => 'Pro plan', 'qty' => 1, 'price' => 29, 'amount' => 29]]"
| :subtotal="29" :tax="['label' => 'Tax (8%)', 'amount' => 2.32]" :total="31.32"
| />
*/
$money = function ($value) use ($currency): ?string {
if ($value === null || $value === '') {
return null;
}
return is_numeric($value)
? $currency.number_format((float) $value, 2)
: (string) $value;
};
$statuses = [
'paid' => ['variant' => 'green', 'icon' => 'check-circle', 'label' => 'Paid'],
'due' => ['variant' => 'amber', 'icon' => 'clock', 'label' => 'Due'],
'overdue' => ['variant' => 'red', 'icon' => 'warning-circle', 'label' => 'Overdue'],
'draft' => ['variant' => 'gray', 'icon' => 'pencil-simple', 'label' => 'Draft'],
'refunded' => ['variant' => 'blue', 'icon' => 'arrow-counter-clockwise', 'label' => 'Refunded'],
'void' => ['variant' => 'gray', 'icon' => 'prohibit', 'label' => 'Void'],
];
$statusMeta = $statuses[$status] ?? $statuses['due'];
$taxLabel = is_array($tax) ? ($tax['label'] ?? 'Tax') : 'Tax';
$taxAmount = is_array($tax) ? ($tax['amount'] ?? null) : $tax;
$resolvedDue = $amountDue ?? $total;
@endphp
<div {{ $attributes->merge(['class' => 'overflow-hidden rounded-2xl border border-gray-200 bg-white text-gray-900 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100']) }}>
{{-- Header --}}
<div class="flex flex-col gap-4 border-b border-gray-200 p-6 dark:border-gray-800 sm:flex-row sm:items-start sm:justify-between">
<div class="flex items-center gap-3">
@isset($logo)
{{ $logo }}
@else
<x-ui.brand />
@endisset
</div>
<div class="sm:text-right">
<div class="flex items-center gap-2 sm:justify-end">
<h2 class="text-lg font-semibold tracking-tight">{{ $title }}</h2>
<x-ui.badge :variant="$statusMeta['variant']" :icon="$statusMeta['icon']">{{ $statusMeta['label'] }}</x-ui.badge>
</div>
@if ($number)
<p class="mt-1 font-mono text-sm text-gray-500 dark:text-gray-400">{{ $number }}</p>
@endif
</div>
</div>
{{-- Meta: parties + dates --}}
<div class="grid gap-6 border-b border-gray-200 p-6 dark:border-gray-800 sm:grid-cols-2 lg:grid-cols-4">
@foreach (['from' => 'From', 'to' => 'Billed to'] as $key => $heading)
@php $details = $key === 'from' ? $from : $to; @endphp
@if ($details)
<div class="min-w-0">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">{{ $heading }}</p>
@if (! empty($details['name']))
<p class="mt-1.5 text-sm font-medium text-gray-900 dark:text-white">{{ $details['name'] }}</p>
@endif
@foreach (($details['lines'] ?? []) as $line)
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $line }}</p>
@endforeach
@if (! empty($details['email']))
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ $details['email'] }}</p>
@endif
</div>
@endif
@endforeach
@if ($issuedOn)
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Issued</p>
<p class="mt-1.5 text-sm text-gray-700 dark:text-gray-300">{{ $issuedOn }}</p>
</div>
@endif
@if ($dueOn)
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Due</p>
<p class="mt-1.5 text-sm text-gray-700 dark:text-gray-300">{{ $dueOn }}</p>
</div>
@endif
</div>
{{-- Line items --}}
<div class="max-w-full overflow-x-auto">
<table class="w-full min-w-[34rem] text-sm">
<thead>
<tr class="border-b border-gray-200 text-left text-xs font-semibold uppercase tracking-wide text-gray-400 dark:border-gray-800 dark:text-gray-500">
<th class="px-6 py-3 font-semibold">Description</th>
<th class="px-6 py-3 text-right font-semibold">Qty</th>
<th class="px-6 py-3 text-right font-semibold">Unit price</th>
<th class="px-6 py-3 text-right font-semibold">Amount</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
@forelse ($items as $item)
@php
$amount = $item['amount'] ?? (
isset($item['qty'], $item['price']) && is_numeric($item['qty']) && is_numeric($item['price'])
? $item['qty'] * $item['price']
: null
);
@endphp
<tr>
<td class="px-6 py-3.5">
<p class="font-medium text-gray-900 dark:text-white">{{ $item['description'] ?? '' }}</p>
@if (! empty($item['detail']))
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{{ $item['detail'] }}</p>
@endif
</td>
<td class="px-6 py-3.5 text-right tabular-nums text-gray-600 dark:text-gray-300">{{ $item['qty'] ?? '—' }}</td>
<td class="px-6 py-3.5 text-right tabular-nums text-gray-600 dark:text-gray-300">{{ $money($item['price'] ?? null) ?? '—' }}</td>
<td class="px-6 py-3.5 text-right font-medium tabular-nums text-gray-900 dark:text-white">{{ $money($amount) ?? '—' }}</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">No line items.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Totals --}}
<div class="flex justify-end border-t border-gray-200 p-6 dark:border-gray-800">
<dl class="w-full max-w-xs space-y-2 text-sm">
@if ($money($subtotal))
<div class="flex items-center justify-between">
<dt class="text-gray-500 dark:text-gray-400">Subtotal</dt>
<dd class="tabular-nums text-gray-700 dark:text-gray-300">{{ $money($subtotal) }}</dd>
</div>
@endif
@if ($money($discount))
<div class="flex items-center justify-between">
<dt class="text-gray-500 dark:text-gray-400">Discount</dt>
<dd class="tabular-nums text-green-600 dark:text-green-400">−{{ $money($discount) }}</dd>
</div>
@endif
@if ($money($taxAmount))
<div class="flex items-center justify-between">
<dt class="text-gray-500 dark:text-gray-400">{{ $taxLabel }}</dt>
<dd class="tabular-nums text-gray-700 dark:text-gray-300">{{ $money($taxAmount) }}</dd>
</div>
@endif
@if ($money($total))
<div class="flex items-center justify-between border-t border-gray-200 pt-2 text-base font-semibold dark:border-gray-800">
<dt class="text-gray-900 dark:text-white">Total</dt>
<dd class="tabular-nums text-gray-900 dark:text-white">{{ $money($total) }}</dd>
</div>
@endif
@if ($money($resolvedDue) && $status !== 'paid')
<div class="flex items-center justify-between rounded-lg bg-brand-50 px-3 py-2 text-base font-semibold text-brand-700 dark:bg-brand-950/50 dark:text-brand-300">
<dt>Amount due</dt>
<dd class="tabular-nums">{{ $money($resolvedDue) }}</dd>
</div>
@endif
</dl>
</div>
@if ($notes || isset($actions))
<div class="flex flex-col gap-4 border-t border-gray-200 bg-gray-50/60 p-6 dark:border-gray-800 dark:bg-gray-900/40 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
@if ($notes)
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Notes</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ $notes }}</p>
@endif
</div>
@isset($actions)
<div class="flex shrink-0 items-center gap-2">{{ $actions }}</div>
@endisset
</div>
@endif
</div>