Packages

phoenix_kit

1.7.21
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules billing web invoice_print.html.heex
Raw

lib/modules/billing/web/invoice_print.html.heex

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Invoice {@invoice.invoice_number} - {@project_title}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #1f2937;
background: #f3f4f6;
padding: 20px;
}
.invoice-container {
max-width: 800px;
margin: 0 auto;
background: white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
border-radius: 8px;
overflow: hidden;
}
.print-controls {
max-width: 800px;
margin: 0 auto 20px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.print-controls button,
.print-controls a {
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-print {
background: #2563eb;
color: white;
border: none;
}
.btn-print:hover {
background: #1d4ed8;
}
.btn-back {
background: white;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-back:hover {
background: #f9fafb;
}
.invoice-header {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
color: white;
padding: 40px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.invoice-title h1 {
font-size: 32px;
font-weight: 700;
margin-bottom: 5px;
}
.invoice-number {
font-size: 18px;
opacity: 0.9;
}
.invoice-status {
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
}
.status-draft { background: #fef3c7; color: #b45309; }
.status-sent { background: #dbeafe; color: #1d4ed8; }
.status-paid { background: #d1fae5; color: #047857; }
.status-void { background: #fee2e2; color: #b91c1c; }
.status-overdue { background: #fee2e2; color: #b91c1c; }
.invoice-body {
padding: 40px;
}
.invoice-meta {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
}
.meta-section h3 {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: #6b7280;
margin-bottom: 10px;
}
.meta-section p {
line-height: 1.8;
}
.meta-section strong {
font-weight: 600;
}
.company-name {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.line-items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.line-items-table th {
background: #f9fafb;
padding: 14px 16px;
text-align: left;
font-weight: 600;
color: #374151;
border-bottom: 2px solid #e5e7eb;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.line-items-table td {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
}
.line-items-table .text-right {
text-align: right;
}
.item-name {
font-weight: 500;
}
.item-description {
font-size: 13px;
color: #6b7280;
margin-top: 4px;
}
.totals-section {
display: flex;
justify-content: flex-end;
margin-bottom: 40px;
}
.totals-table {
width: 300px;
}
.totals-table tr td {
padding: 10px 16px;
}
.totals-table .label {
text-align: right;
color: #6b7280;
}
.totals-table .value {
text-align: right;
font-weight: 500;
}
.totals-table .total-row {
font-size: 18px;
font-weight: 700;
border-top: 2px solid #1e3a5f;
}
.totals-table .total-row td {
padding-top: 16px;
color: #1e3a5f;
}
.payment-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
.payment-box {
padding: 24px;
border-radius: 8px;
}
.due-date-box {
background: #fef3c7;
border: 1px solid #f59e0b;
text-align: center;
}
.due-date-box .due-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: #b45309;
margin-bottom: 8px;
}
.due-date-box .due-date {
font-size: 20px;
font-weight: 700;
color: #92400e;
}
.due-date-box .payment-terms {
font-size: 13px;
color: #b45309;
margin-top: 8px;
}
.bank-details-box {
background: #f0f9ff;
border: 1px solid #0ea5e9;
}
.bank-details-box h4 {
font-size: 14px;
font-weight: 600;
color: #0369a1;
margin-bottom: 16px;
}
.bank-details-box table {
width: 100%;
}
.bank-details-box td {
padding: 6px 0;
}
.bank-details-box .label {
color: #6b7280;
width: 100px;
}
.bank-details-box .value {
font-family: 'Courier New', monospace;
font-weight: 500;
}
.invoice-footer {
background: #f9fafb;
padding: 30px 40px;
border-top: 1px solid #e5e7eb;
}
.footer-company {
margin-bottom: 15px;
}
.footer-company .name {
font-weight: 600;
color: #1f2937;
}
.footer-company .details {
font-size: 13px;
color: #6b7280;
}
.footer-note {
font-size: 13px;
color: #6b7280;
}
.receipt-badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: #d1fae5;
color: #047857;
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
margin-top: 20px;
}
.refund-badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: #fef3c7;
color: #b45309;
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
margin-top: 10px;
}
.payment-history {
margin-top: 30px;
padding: 20px;
background: #f9fafb;
border-radius: 8px;
}
.payment-history h4 {
font-size: 13px;
font-weight: 600;
color: #6b7280;
margin-bottom: 15px;
}
.payment-history-table {
width: 100%;
border-collapse: collapse;
}
.payment-history-table th,
.payment-history-table td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.payment-history-table th {
font-weight: 600;
color: #374151;
font-size: 12px;
text-transform: uppercase;
}
.payment-history-table .refund-row {
color: #b45309;
}
.payment-history-table .payment-row {
color: #047857;
}
@media print {
body {
background: white;
padding: 0;
}
.print-controls {
display: none;
}
.invoice-container {
box-shadow: none;
border-radius: 0;
}
.invoice-header {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.status-draft,
.status-sent,
.status-paid,
.status-void,
.status-overdue,
.due-date-box,
.bank-details-box,
.receipt-badge,
.refund-badge,
.payment-history {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
</head>
<body>
<div class="print-controls">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/billing/invoices/#{@invoice.id}")}
class="btn-back"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m15 18-6-6 6-6" />
</svg>
Back
</.link>
<button onclick="window.print()" class="btn-print">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" /><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6" /><rect
x="6"
y="14"
width="12"
height="8"
rx="1"
/>
</svg>
Print Invoice
</button>
</div>
<div class="invoice-container">
<div class="invoice-header">
<div class="invoice-title">
<h1>INVOICE</h1>
<div class="invoice-number">{@invoice.invoice_number}</div>
</div>
<div class={"invoice-status status-#{@invoice.status}"}>
{String.upcase(@invoice.status)}
</div>
</div>
<div class="invoice-body">
<div class="invoice-meta">
<div class="meta-section">
<h3>From</h3>
<p>
<%!-- Customer/Client billing details (who pays) --%>
<%= if @invoice.billing_details && map_size(@invoice.billing_details) > 0 do %>
<%= if @invoice.billing_details["type"] == "company" do %>
<span class="company-name">{@invoice.billing_details["company_name"]}</span>
<br />
<%= if @invoice.billing_details["company_vat_number"] do %>
VAT: {@invoice.billing_details["company_vat_number"]}<br />
<% end %>
<% else %>
<span class="company-name">
{@invoice.billing_details["first_name"]} {@invoice.billing_details[
"last_name"
]}
</span>
<br />
<% end %>
<%= if @invoice.billing_details["address_line1"] do %>
{@invoice.billing_details["address_line1"]}<br />
<% end %>
<%= if @invoice.billing_details["address_line2"] do %>
{@invoice.billing_details["address_line2"]}<br />
<% end %>
<%= if @invoice.billing_details["city"] do %>
{@invoice.billing_details["city"]}
<%= if @invoice.billing_details["postal_code"] do %>
, {@invoice.billing_details["postal_code"]}
<% end %>
<br />
<% end %>
<%= if @invoice.billing_details["country"] do %>
{@invoice.billing_details["country"]}
<% end %>
<% else %>
<%= if @invoice.user do %>
<span class="company-name">{@invoice.user.email}</span>
<% else %>
<em>No billing information</em>
<% end %>
<% end %>
</p>
</div>
<div class="meta-section">
<h3>Bill To</h3>
<p>
<%!-- Our company details (who receives payment) --%>
<strong>{@company.name}</strong>
<br />
<%= for line <- String.split(@company.address || "", "\n") do %>
{line}<br />
<% end %>
<%= if @company.vat != "" do %>
VAT: {@company.vat}
<% end %>
</p>
</div>
<div class="meta-section" style="text-align: right;">
<h3>Invoice Details</h3>
<p>
<strong>Date:</strong> {Calendar.strftime(@invoice.inserted_at, "%B %d, %Y")}<br />
<strong>Due Date:</strong> {if @invoice.due_date,
do: Calendar.strftime(@invoice.due_date, "%B %d, %Y"),
else: "-"}<br />
<strong>Currency:</strong> {@invoice.currency}
<%= if @invoice.order do %>
<br /><strong>Order:</strong> {@invoice.order.order_number}
<% end %>
</p>
</div>
</div>
<table class="line-items-table">
<thead>
<tr>
<th>Description</th>
<th class="text-right" style="width: 80px;">Qty</th>
<th class="text-right" style="width: 120px;">Unit Price</th>
<th class="text-right" style="width: 120px;">Amount</th>
</tr>
</thead>
<tbody>
<%= for item <- @invoice.line_items || [] do %>
<tr>
<td>
<div class="item-name">{item["name"]}</div>
<%= if item["description"] && item["description"] != "" do %>
<div class="item-description">{item["description"]}</div>
<% end %>
</td>
<td class="text-right">{item["quantity"]}</td>
<td class="text-right">{item["unit_price"]} {@invoice.currency}</td>
<td class="text-right">{item["total"]} {@invoice.currency}</td>
</tr>
<% end %>
</tbody>
</table>
<div class="totals-section">
<table class="totals-table">
<tr>
<td class="label">Subtotal:</td>
<td class="value">
{Decimal.to_string(@invoice.subtotal || Decimal.new(0), :normal)} {@invoice.currency}
</td>
</tr>
<%= if Decimal.gt?(@invoice.tax_amount || Decimal.new(0), Decimal.new(0)) do %>
<tr>
<td class="label">
Tax ({Decimal.round(Decimal.mult(@invoice.tax_rate || Decimal.new(0), 100), 2)
|> Decimal.normalize()
|> Decimal.to_string()}%):
</td>
<td class="value">
{Decimal.to_string(@invoice.tax_amount, :normal)} {@invoice.currency}
</td>
</tr>
<% end %>
<tr class="total-row">
<td class="label">Total:</td>
<td class="value">
{Decimal.to_string(@invoice.total || Decimal.new(0), :normal)} {@invoice.currency}
</td>
</tr>
</table>
</div>
<%= if @invoice.status != "paid" do %>
<div class="payment-section">
<div class="payment-box due-date-box">
<div class="due-label">Payment Due</div>
<div class="due-date">
{if @invoice.due_date,
do: Calendar.strftime(@invoice.due_date, "%B %d, %Y"),
else: "On receipt"}
</div>
<%= if @invoice.payment_terms do %>
<div class="payment-terms">{@invoice.payment_terms}</div>
<% end %>
</div>
<div class="payment-box bank-details-box">
<h4>Bank Transfer Details</h4>
<table>
<tr>
<td class="label">Bank:</td>
<td class="value">
{@invoice.bank_details["bank_name"] || @company.bank_name}
</td>
</tr>
<tr>
<td class="label">IBAN:</td>
<td class="value">{@invoice.bank_details["iban"] || @company.bank_iban}</td>
</tr>
<tr>
<td class="label">SWIFT:</td>
<td class="value">{@invoice.bank_details["swift"] || @company.bank_swift}</td>
</tr>
<tr>
<td class="label">Reference:</td>
<td class="value">{@invoice.invoice_number}</td>
</tr>
</table>
</div>
</div>
<% end %>
<%= if @invoice.status == "paid" && @invoice.receipt_number do %>
<div class="receipt-badge">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
</svg>
PAID - Receipt #{@invoice.receipt_number}
<%= if @invoice.paid_at do %>
<span style="opacity: 0.8; margin-left: 10px;">
on {Calendar.strftime(@invoice.paid_at, "%B %d, %Y")}
</span>
<% end %>
</div>
<% end %>
<%!-- Refund information --%>
<%= if @refund_info do %>
<div class="refund-badge">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
</svg>
REFUNDED - {Decimal.to_string(@refund_info.total, :normal)} {@invoice.currency}
<span style="opacity: 0.8; margin-left: 10px;">
on {Calendar.strftime(@refund_info.latest_date, "%B %d, %Y")}
</span>
</div>
<%!-- Payment history table when refunds exist --%>
<div class="payment-history">
<h4>Payment History</h4>
<table class="payment-history-table">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Method</th>
<th style="text-align: right;">Amount</th>
</tr>
</thead>
<tbody>
<%= for txn <- @invoice.transactions do %>
<tr class={
if Decimal.negative?(txn.amount), do: "refund-row", else: "payment-row"
}>
<td>{Calendar.strftime(txn.inserted_at, "%B %d, %Y")}</td>
<td>
<%= if Decimal.negative?(txn.amount) do %>
Refund
<% else %>
Payment
<% end %>
</td>
<td>{String.capitalize(txn.payment_method || "bank")}</td>
<td style="text-align: right;">
<%= if Decimal.negative?(txn.amount) do %>
-{Decimal.to_string(Decimal.abs(txn.amount), :normal)} {@invoice.currency}
<% else %>
+{Decimal.to_string(txn.amount, :normal)} {@invoice.currency}
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<%= if @invoice.notes do %>
<div style="margin-top: 30px; padding: 20px; background: #f9fafb; border-radius: 8px;">
<h4 style="font-size: 13px; font-weight: 600; color: #6b7280; margin-bottom: 10px;">
Notes
</h4>
<p style="white-space: pre-wrap;">{@invoice.notes}</p>
</div>
<% end %>
</div>
<div class="invoice-footer">
<div class="footer-company">
<div class="name">{@company.name}</div>
<div class="details">
{@company.address}
<%= if @company.vat != "" do %>
&bull; VAT: {@company.vat}
<% end %>
</div>
</div>
<div class="footer-note">
Thank you for your business. If you have any questions about this invoice, please contact us.
</div>
</div>
</div>
</body>
</html>