Skip to main content

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>