feat: styled PDF invoice export with download button

This commit is contained in:
2026-03-01 16:29:55 -05:00
parent a30d12063c
commit afdae80ec9
2 changed files with 281 additions and 87 deletions

View File

@@ -3,153 +3,347 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ __('invoices.invoice', ['id' => $invoice->number]) }}</title> <title>{{ __('invoices.invoice', ['id' => $invoice->number]) }}</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
font-family: font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
system-ui, font-size: 14px;
-apple-system, color: #1a1a2e;
/* Firefox supports this but not yet `system-ui` */ line-height: 1.6;
'Segoe UI', padding: 40px;
Roboto,
Helvetica,
Arial,
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji';
font-size: 16px;
} }
table { /* Header */
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 3px solid #1A759F;
}
.header-left h1 {
font-size: 28px;
font-weight: 900;
color: #1a1a2e;
margin-bottom: 4px;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(to right, #1A759F, #52B69A);
border-radius: 2px;
margin-top: 6px;
}
.status-badge {
display: inline-block;
padding: 8px 20px;
border-radius: 8px;
font-weight: 700;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-paid {
color: #16a34a;
border: 2px solid #16a34a;
}
.status-pending {
color: #d97706;
border: 2px solid #d97706;
}
/* Info Cards */
.info-grid {
width: 100%;
margin-bottom: 30px;
border-collapse: separate;
border-spacing: 10px;
}
.info-grid td {
vertical-align: top;
padding: 18px 22px;
border: 1px solid #e5e7eb;
border-radius: 12px;
width: 33.33%;
}
.info-label {
text-transform: uppercase;
font-weight: 900;
font-size: 11px;
letter-spacing: 1.5px;
color: #1A759F;
margin-bottom: 10px;
}
.info-name {
font-size: 16px;
font-weight: 700;
color: #1a1a2e;
margin-bottom: 6px;
}
.info-text {
font-size: 12px;
color: #6b7280;
line-height: 1.7;
}
.info-detail-row {
display: flex;
justify-content: space-between;
padding: 6px 10px;
margin-bottom: 4px;
border-radius: 6px;
}
.info-detail-row:nth-child(odd) {
background-color: #f9fafb;
}
.info-detail-label {
font-size: 12px;
color: #6b7280;
font-weight: 500;
}
.info-detail-value {
font-size: 12px;
font-weight: 700;
color: #1a1a2e;
}
/* Section Headers */
.section-title {
font-size: 18px;
font-weight: 700;
color: #1a1a2e;
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 2px solid #f3f4f6;
}
/* Tables */
.items-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 30px;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e5e7eb;
} }
.items-table thead tr {
table th { background-color: #f8fafc;
border-bottom: 2px solid #e5e7eb;
}
.items-table th {
text-align: left; text-align: left;
color: #999; font-weight: 900;
border-bottom: 2px solid #ddd; font-size: 11px;
padding: 10px 0 15px 0;
font-size: 0.75em;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px;
color: #1a1a2e;
padding: 14px 20px;
} }
.items-table th:last-child {
table td {
padding: 15px 0;
}
table th:last-child {
text-align: right; text-align: right;
} }
.items-table td {
table tr td { padding: 14px 20px;
padding: 0; font-size: 13px;
color: #1a1a2e;
border-bottom: 1px solid #f3f4f6;
} }
.items-table td:last-child {
table tr td:last-child {
text-align: right; text-align: right;
font-weight: 700;
}
.items-table tbody tr:last-child td {
border-bottom: none;
} }
.invoice-items tbody tr:first-child td { /* Totals */
padding-top: 10px; .totals-wrapper {
width: 100%;
margin-bottom: 30px;
}
.totals-table {
width: 320px;
margin-left: auto;
border-collapse: collapse;
}
.totals-table td {
padding: 8px 14px;
font-size: 13px;
}
.totals-table .label {
text-transform: uppercase;
font-weight: 700;
font-size: 11px;
letter-spacing: 1px;
color: #6b7280;
}
.totals-table .value {
text-align: right;
font-weight: 700;
color: #1a1a2e;
}
.total-row td {
padding: 14px;
border-top: 2px solid #1A759F;
}
.total-row .label {
font-size: 14px;
font-weight: 900;
color: #1a1a2e;
}
.total-row .value {
font-size: 20px;
font-weight: 900;
color: #1A759F;
} }
.invoice-info { /* Footer */
font-size: 0.875em; .footer {
} margin-top: 40px;
padding-top: 16px;
.invoice-info td { border-top: 1px solid #e5e7eb;
padding: 2px 0; text-align: center;
font-size: 11px;
color: #9ca3af;
} }
</style> </style>
</head> </head>
<body> <body>
@if(config('settings.logo')) <!-- Header -->
<div style="margin: 20px 0 70px 0;"> <table style="width: 100%; margin-bottom: 30px; border-bottom: 3px solid #1A759F; padding-bottom: 16px;">
<img style="height: 30px" src="{{ public_path('storage/' . config('settings.logo')) }}"
alt="{{ config('app.name') }}">
</div>
@endif
<!-- Invoice status -->
<div style="margin-bottom: 20px;font-size: 20px">
<strong>{{ __('invoices.status') }}:</strong><span
style="@if($invoice->status == 'paid') color: green; @else color: orange; @endif">
{{ ucfirst($invoice->status) }}
</span>
</div>
<table class="invoice-info">
<tr> <tr>
<td rowspan="2" style="font-size: 1em;vertical-align: top;"> <td style="vertical-align: middle;">
<strong>{{ __('invoices.issued_to') }}</strong><br> @if(config('settings.logo'))
{{ $invoice->user->name }} <br /> <img style="height: 36px;" src="{{ public_path('storage/' . config('settings.logo')) }}" alt="{{ config('app.name') }}">
@foreach($invoice->user->properties()->with('parent_property')->whereHas('parent_property', function @endif
($query) { <div style="margin-top: 8px;">
$query->where('show_on_invoice', true); <span style="font-size: 28px; font-weight: 900; color: #1a1a2e;">{{ __('invoices.invoice', ['id' => $invoice->number]) }}</span>
})->get() as $property) </div>
{{ $property->value }} <br /> <div style="width: 80px; height: 4px; background: linear-gradient(to right, #1A759F, #52B69A); border-radius: 2px; margin-top: 6px;"></div>
@endforeach
</td> </td>
<td> <td style="text-align: right; vertical-align: middle;">
<strong>{{ strtoupper(__('invoices.bill_to')) }}</strong> <br /> <span class="status-badge {{ $invoice->status == 'paid' ? 'status-paid' : 'status-pending' }}">
{{ config('settings.company_name') }} <br /> {{ $invoice->status == 'paid' ? __('invoices.paid') : __('invoices.payment_pending') }}
{{ config('settings.company_address') }} {{ config('settings.company_address2') }} <br /> </span>
{{ config('settings.company_zip') }} {{ config('settings.company_city') }} <br />
{{ config('settings.company_state') }} {{ config('settings.company_country') }} <br />
{{ config('settings.company_tax_id') }} <br />
{{ config('settings.company_id') }} <br />
</td> </td>
</tr> </tr>
</table> </table>
<p>{{ __('invoices.invoice_date') }}: <strong>{{ $invoice->created_at->format('d/m/Y') }}</strong></p>
<p>{{ __('invoices.invoice_no') }}: <strong>{{ $invoice->number }}</strong></p>
<table style="margin-top: 40px;" class="invoice-items"> <!-- Info Cards -->
<table class="info-grid">
<tr>
<td>
<div class="info-label">{{ __('invoices.issued_to') }}</div>
<div class="info-name">{{ $invoice->user->properties->where('parent_property.name', 'Company Name (Include Registration Number)')->first()?->value }}</div>
<div class="info-text">
Attention: {{ $invoice->user->first_name }} {{ $invoice->user->last_name }}<br>
{{ $invoice->user->properties->where('parent_property.name', 'Property Number/House Name')->first()?->value }}
{{ $invoice->user->properties->where('parent_property.name', 'Street Name')->first()?->value }}<br>
{{ $invoice->user->properties->where('parent_property.name', 'City')->first()?->value }}
{{ $invoice->user->properties->where('parent_property.name', 'State/Province/Region')->first()?->value }}
{{ $invoice->user->properties->where('parent_property.name', 'Full Postal Code')->first()?->value }}<br>
{{ $invoice->user->properties->where('parent_property.name', 'Country')->first()?->value }}
</div>
</td>
<td>
<div class="info-label">{{ __('invoices.bill_to') }}</div>
<div class="info-name">{{ config('settings.company_name') }}</div>
<div class="info-text">
{{ config('settings.company_address') }} {{ config('settings.company_address2') }}<br>
{{ config('settings.company_city') }} {{ config('settings.company_state') }} {{ config('settings.company_zip') }}<br>
{{ \Locale::getDisplayRegion('-' . config('settings.company_country')) }}
</div>
</td>
<td>
<div class="info-label">Invoice Details</div>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 6px 8px; font-size: 12px; color: #6b7280; background: #f9fafb; border-radius: 6px;">{{ __('invoices.invoice_date') }}</td>
<td style="padding: 6px 8px; font-size: 12px; font-weight: 700; text-align: right; background: #f9fafb; border-radius: 6px;">{{ $invoice->created_at->format('d M Y') }}</td>
</tr>
<tr>
<td style="padding: 6px 8px; font-size: 12px; color: #6b7280;">{{ __('invoices.invoice_no') }}</td>
<td style="padding: 6px 8px; font-size: 12px; font-weight: 700; text-align: right;">#{{ $invoice->number }}</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- Invoice Items -->
<div class="section-title">Invoice Items</div>
<table class="items-table">
<thead> <thead>
<tr> <tr>
<th>{{ __('invoices.item') }}</th> <th>{{ __('invoices.item') }}</th>
<th style="width: 100px">{{ __('invoices.quantity') }}</th> <th>{{ __('invoices.price') }}</th>
<th>{{ __('invoices.unit_price') }}</th> <th>{{ __('invoices.quantity') }}</th>
<th>{{ __('invoices.total') }}</th> <th>{{ __('invoices.total') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($invoice->items as $item) @foreach($invoice->items as $item)
<tr> <tr>
<td>{{ $item->description }}</td> <td style="font-weight: 600;">{{ $item->description }}</td>
<td>{{ $item->quantity }}</td>
<td>{{ $item->formattedPrice }}</td> <td>{{ $item->formattedPrice }}</td>
<td>{{ $item->quantity }}</td>
<td>{{ $item->formattedTotal }}</td> <td>{{ $item->formattedTotal }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
@if($invoice->transactions->count() > 0) <!-- Totals -->
<table style="margin-top: 80px;" class="invoice-items"> <div class="totals-wrapper">
<table class="totals-table">
@if ($invoice->formattedTotal->tax > 0)
<tr>
<td class="label">{{ __('invoices.subtotal') }}</td>
<td class="value">{{ $invoice->formattedTotal->format($invoice->formattedTotal->price - $invoice->formattedTotal->tax) }}</td>
</tr>
<tr>
<td class="label">{{ \App\Classes\Settings::tax()->name }}</td>
<td class="value">{{ $invoice->formattedTotal->formatted->tax }}</td>
</tr>
@endif
<tr class="total-row">
<td class="label">{{ __('invoices.total') }}</td>
<td class="value">{{ $invoice->formattedTotal }}</td>
</tr>
</table>
</div>
<!-- Transactions -->
@if($invoice->transactions->isNotEmpty())
<div class="section-title">{{ __('invoices.transactions') }}</div>
<table class="items-table">
<thead> <thead>
<tr> <tr>
<th>{{ __('invoices.date') }}</th>
<th>{{ __('invoices.transaction_id') }}</th> <th>{{ __('invoices.transaction_id') }}</th>
<th>{{ __('invoices.payment_date') }}</th> <th>{{ __('invoices.gateway') }}</th>
<th>{{ __('invoices.amount') }}</th> <th>{{ __('invoices.amount') }}</th>
<th>{{ __('invoices.payment_method') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($invoice->transactions as $transaction) @foreach($invoice->transactions as $transaction)
<tr> <tr>
<td style="font-weight: 600;">{{ $transaction->created_at->format('d M Y H:i') }}</td>
<td>{{ $transaction->transaction_id }}</td> <td>{{ $transaction->transaction_id }}</td>
<td>{{ $transaction->created_at->format('d/m/Y') }}</td> <td>{{ $transaction->gateway?->name }}</td>
<td>{{ $transaction->formattedAmount }}</td> <td>{{ $transaction->formattedAmount }}</td>
<td>{{ $transaction->gateway ? $transaction->gateway->name : '' }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
@endif @endif
<!-- Footer -->
<div class="footer">
{{ config('settings.company_name') }} &bull; Generated {{ now()->format('d M Y H:i') }}
</div>
</body> </body>
</html> </html>

View File

@@ -46,10 +46,10 @@
</div> </div>
@endif @endif
<!--<button wire:click="downloadPDF" class="group flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-primary to-primary/80 text-white font-semibold rounded-2xl transition-all duration-300 transform hover:scale-105 hover:shadow-xl"> <button wire:click="downloadPDF" class="group flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-primary to-primary/80 text-white font-semibold rounded-2xl transition-all duration-300 transform hover:scale-105 hover:shadow-xl">
<x-ri-download-line class="size-5 transition-transform group-hover:scale-110" /> <x-ri-download-line class="size-5 transition-transform group-hover:scale-110" />
<span>Download</span> <span>Download</span>
</button>--> </button>
</div> </div>
</div> </div>