feat: styled PDF invoice export with download button
This commit is contained in:
@@ -3,153 +3,347 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
/* Firefox supports this but not yet `system-ui` */
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif,
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji';
|
||||
font-size: 16px;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #1a1a2e;
|
||||
line-height: 1.6;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
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%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
table th {
|
||||
.items-table thead tr {
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
.items-table th {
|
||||
text-align: left;
|
||||
color: #999;
|
||||
border-bottom: 2px solid #ddd;
|
||||
padding: 10px 0 15px 0;
|
||||
font-size: 0.75em;
|
||||
font-weight: 900;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #1a1a2e;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
table th:last-child {
|
||||
.items-table th:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
padding: 0;
|
||||
.items-table td {
|
||||
padding: 14px 20px;
|
||||
font-size: 13px;
|
||||
color: #1a1a2e;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
table tr td:last-child {
|
||||
.items-table td:last-child {
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
}
|
||||
.items-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.invoice-items tbody tr:first-child td {
|
||||
padding-top: 10px;
|
||||
/* Totals */
|
||||
.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 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.invoice-info td {
|
||||
padding: 2px 0;
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@if(config('settings.logo'))
|
||||
<div style="margin: 20px 0 70px 0;">
|
||||
<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">
|
||||
<!-- Header -->
|
||||
<table style="width: 100%; margin-bottom: 30px; border-bottom: 3px solid #1A759F; padding-bottom: 16px;">
|
||||
<tr>
|
||||
<td rowspan="2" style="font-size: 1em;vertical-align: top;">
|
||||
<strong>{{ __('invoices.issued_to') }}</strong><br>
|
||||
{{ $invoice->user->name }} <br />
|
||||
@foreach($invoice->user->properties()->with('parent_property')->whereHas('parent_property', function
|
||||
($query) {
|
||||
$query->where('show_on_invoice', true);
|
||||
})->get() as $property)
|
||||
{{ $property->value }} <br />
|
||||
@endforeach
|
||||
<td style="vertical-align: middle;">
|
||||
@if(config('settings.logo'))
|
||||
<img style="height: 36px;" src="{{ public_path('storage/' . config('settings.logo')) }}" alt="{{ config('app.name') }}">
|
||||
@endif
|
||||
<div style="margin-top: 8px;">
|
||||
<span style="font-size: 28px; font-weight: 900; color: #1a1a2e;">{{ __('invoices.invoice', ['id' => $invoice->number]) }}</span>
|
||||
</div>
|
||||
<div style="width: 80px; height: 4px; background: linear-gradient(to right, #1A759F, #52B69A); border-radius: 2px; margin-top: 6px;"></div>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ strtoupper(__('invoices.bill_to')) }}</strong> <br />
|
||||
{{ config('settings.company_name') }} <br />
|
||||
{{ config('settings.company_address') }} {{ config('settings.company_address2') }} <br />
|
||||
{{ 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 style="text-align: right; vertical-align: middle;">
|
||||
<span class="status-badge {{ $invoice->status == 'paid' ? 'status-paid' : 'status-pending' }}">
|
||||
{{ $invoice->status == 'paid' ? __('invoices.paid') : __('invoices.payment_pending') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
<tr>
|
||||
<th>{{ __('invoices.item') }}</th>
|
||||
<th style="width: 100px">{{ __('invoices.quantity') }}</th>
|
||||
<th>{{ __('invoices.unit_price') }}</th>
|
||||
<th>{{ __('invoices.price') }}</th>
|
||||
<th>{{ __('invoices.quantity') }}</th>
|
||||
<th>{{ __('invoices.total') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoice->items as $item)
|
||||
<tr>
|
||||
<td>{{ $item->description }}</td>
|
||||
<td>{{ $item->quantity }}</td>
|
||||
<td style="font-weight: 600;">{{ $item->description }}</td>
|
||||
<td>{{ $item->formattedPrice }}</td>
|
||||
<td>{{ $item->quantity }}</td>
|
||||
<td>{{ $item->formattedTotal }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if($invoice->transactions->count() > 0)
|
||||
<table style="margin-top: 80px;" class="invoice-items">
|
||||
<!-- Totals -->
|
||||
<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>
|
||||
<tr>
|
||||
<th>{{ __('invoices.date') }}</th>
|
||||
<th>{{ __('invoices.transaction_id') }}</th>
|
||||
<th>{{ __('invoices.payment_date') }}</th>
|
||||
<th>{{ __('invoices.gateway') }}</th>
|
||||
<th>{{ __('invoices.amount') }}</th>
|
||||
<th>{{ __('invoices.payment_method') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($invoice->transactions as $transaction)
|
||||
<tr>
|
||||
<td style="font-weight: 600;">{{ $transaction->created_at->format('d M Y H:i') }}</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->gateway ? $transaction->gateway->name : '' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer">
|
||||
{{ config('settings.company_name') }} • Generated {{ now()->format('d M Y H:i') }}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user