feat: Implement A4 invoice layout and hide Label for virtual orders
Invoice: - Enhanced A4-ready layout with proper structure - Store header with invoice number - Billing/shipping address sections - Styled items table with alternating rows - Totals summary with conditional display - Thank you footer Label: - Label button now hidden for virtual-only orders - Uses existing isVirtualOnly detection Print CSS: - Added @page A4 size directive - Print-color-adjust for background colors - 20mm padding for proper margins Documentation: - Updated subscription module plan (comprehensive) - Updated affiliate module plan (comprehensive) - Created shipping label standardization plan
This commit is contained in:
@@ -205,9 +205,11 @@ export default function OrderShow() {
|
||||
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printInvoice} title={__('Print invoice')}>
|
||||
<FileText className="w-4 h-4" /> {__('Invoice')}
|
||||
</button>
|
||||
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printLabel} title={__('Print shipping label')}>
|
||||
<Ticket className="w-4 h-4" /> {__('Label')}
|
||||
</button>
|
||||
{!isVirtualOnly && (
|
||||
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printLabel} title={__('Print shipping label')}>
|
||||
<Ticket className="w-4 h-4" /> {__('Label')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -475,55 +477,116 @@ export default function OrderShow() {
|
||||
{order && (
|
||||
<div className="print-only">
|
||||
{mode === 'invoice' && (
|
||||
<div className="max-w-[800px] mx-auto p-6 text-sm">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="print-a4 bg-white" style={{ minHeight: '297mm', width: '210mm', margin: '0 auto', padding: '20mm', boxSizing: 'border-box' }}>
|
||||
{/* Invoice Header */}
|
||||
<div className="flex items-start justify-between mb-8 pb-6 border-b-2 border-gray-200">
|
||||
<div>
|
||||
<div className="text-xl font-semibold">Invoice</div>
|
||||
<div className="opacity-60">Order #{order.number} · {new Date((order.date_ts || 0) * 1000).toLocaleString()}</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{__('INVOICE')}</div>
|
||||
<div className="text-sm text-gray-600 mt-1">#{order.number}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-medium">{siteTitle}</div>
|
||||
<div className="opacity-60 text-xs">{window.location.origin}</div>
|
||||
<div className="text-xl font-semibold text-gray-900">{siteTitle}</div>
|
||||
<div className="text-sm text-gray-500 mt-1">{window.location.origin}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
|
||||
{/* Invoice Meta */}
|
||||
<div className="grid grid-cols-2 gap-8 mb-8">
|
||||
<div>
|
||||
<div className="text-xs opacity-60 mb-1">{__('Bill To')}</div>
|
||||
<div className="text-sm" dangerouslySetInnerHTML={{ __html: order.billing?.address || order.billing?.name || '' }} />
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">{__('Invoice Date')}</div>
|
||||
<div className="text-sm text-gray-900">{new Date((order.date_ts || 0) * 1000).toLocaleDateString()}</div>
|
||||
{order.payment_method && (
|
||||
<>
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2 mt-4">{__('Payment Method')}</div>
|
||||
<div className="text-sm text-gray-900">{order.payment_method}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<canvas ref={qrRef} className="inline-block w-24 h-24 border" />
|
||||
<div className="flex justify-end">
|
||||
<canvas ref={qrRef} className="w-24 h-24" style={{ border: '1px solid #e5e7eb' }} />
|
||||
</div>
|
||||
</div>
|
||||
<table className="w-full border-collapse mb-6">
|
||||
|
||||
{/* Billing & Shipping */}
|
||||
<div className="grid grid-cols-2 gap-8 mb-8">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">{__('Bill To')}</div>
|
||||
<div className="text-sm text-gray-900 font-medium">{order.billing?.name || '—'}</div>
|
||||
{order.billing?.email && <div className="text-sm text-gray-600">{order.billing.email}</div>}
|
||||
{order.billing?.phone && <div className="text-sm text-gray-600">{order.billing.phone}</div>}
|
||||
<div className="text-sm text-gray-600 mt-2" dangerouslySetInnerHTML={{ __html: order.billing?.address || '' }} />
|
||||
</div>
|
||||
{!isVirtualOnly && order.shipping?.name && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">{__('Ship To')}</div>
|
||||
<div className="text-sm text-gray-900 font-medium">{order.shipping?.name || '—'}</div>
|
||||
<div className="text-sm text-gray-600 mt-2" dangerouslySetInnerHTML={{ __html: order.shipping?.address || '' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Items Table */}
|
||||
<table className="w-full mb-8" style={{ borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left border-b py-2 pr-2">Product</th>
|
||||
<th className="text-right border-b py-2 px-2">Qty</th>
|
||||
<th className="text-right border-b py-2 px-2">Subtotal</th>
|
||||
<th className="text-right border-b py-2 pl-2">Total</th>
|
||||
<tr className="bg-gray-900 text-white">
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold">{__('Product')}</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-semibold w-20">{__('Qty')}</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-semibold w-32">{__('Price')}</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-semibold w-32">{__('Total')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(order.items || []).map((it: any) => (
|
||||
<tr key={it.id}>
|
||||
<td className="py-1 pr-2">{it.name}</td>
|
||||
<td className="py-1 px-2 text-right">×{it.qty}</td>
|
||||
<td className="py-1 px-2 text-right"><Money value={it.subtotal} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
<td className="py-1 pl-2 text-right"><Money value={it.total} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
{(order.items || []).map((it: any, idx: number) => (
|
||||
<tr key={it.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="py-3 px-4 text-sm">
|
||||
<div className="font-medium text-gray-900">{it.name}</div>
|
||||
{it.sku && <div className="text-xs text-gray-500">SKU: {it.sku}</div>}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-center text-gray-600">{it.qty}</td>
|
||||
<td className="py-3 px-4 text-sm text-right text-gray-600"><Money value={it.subtotal / it.qty} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
<td className="py-3 px-4 text-sm text-right font-medium text-gray-900"><Money value={it.total} currency={order.currency} symbol={order.currency_symbol} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex justify-end">
|
||||
<div className="min-w-[260px]">
|
||||
<div className="flex justify-between"><span>Subtotal</span><span><Money value={order.totals?.subtotal} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between"><span>Discount</span><span><Money value={order.totals?.discount} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between"><span>Shipping</span><span><Money value={order.totals?.shipping} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between"><span>Tax</span><span><Money value={order.totals?.tax} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
<div className="flex justify-between font-semibold border-t mt-2 pt-2"><span>Total</span><span><Money value={order.totals?.total} currency={order.currency} symbol={order.currency_symbol} /></span></div>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="flex justify-end mb-8">
|
||||
<div className="w-72">
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Subtotal')}</span>
|
||||
<span className="text-gray-900"><Money value={order.totals?.subtotal} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
{(order.totals?.discount || 0) > 0 && (
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Discount')}</span>
|
||||
<span className="text-green-600">-<Money value={order.totals?.discount} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
)}
|
||||
{(order.totals?.shipping || 0) > 0 && (
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Shipping')}</span>
|
||||
<span className="text-gray-900"><Money value={order.totals?.shipping} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
)}
|
||||
{(order.totals?.tax || 0) > 0 && (
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-gray-600">{__('Tax')}</span>
|
||||
<span className="text-gray-900"><Money value={order.totals?.tax} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between py-3 mt-2 border-t-2 border-gray-900">
|
||||
<span className="text-lg font-bold text-gray-900">{__('Total')}</span>
|
||||
<span className="text-lg font-bold text-gray-900"><Money value={order.totals?.total} currency={order.currency} symbol={order.currency_symbol} /></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto pt-8 border-t border-gray-200 text-center text-xs text-gray-500">
|
||||
<p>{__('Thank you for your business!')}</p>
|
||||
<p className="mt-1">{siteTitle} • {window.location.origin}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'label' && (
|
||||
|
||||
Reference in New Issue
Block a user