feat: styled PDF invoice export with download button
This commit is contained in:
@@ -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') }} • Generated {{ now()->format('d M Y H:i') }}
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user