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:
Dwindi Ramadhana
2026-01-05 19:16:13 +07:00
parent 2efc6a7605
commit 40aee67c46
5 changed files with 733 additions and 57 deletions

View File

@@ -34,6 +34,7 @@
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@@ -63,10 +64,23 @@
}
@layer base {
* { @apply border-border; }
body { @apply bg-background text-foreground; }
h1, h2, h3, h4, h5, h6 { @apply text-foreground; }
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply text-foreground;
}
/* Override WordPress common.css focus/active styles */
a:focus,
a:active {
@@ -126,11 +140,14 @@
/* Page defaults for print */
@page {
size: auto; /* let the browser choose */
margin: 12mm; /* comfortable default */
size: auto;
/* let the browser choose */
margin: 12mm;
/* comfortable default */
}
@media print {
/* Hide WordPress admin chrome */
#adminmenuback,
#adminmenuwrap,
@@ -139,44 +156,124 @@
#wpfooter,
#screen-meta,
.notice,
.update-nag { display: none !important; }
.update-nag {
display: none !important;
}
/* Reset layout to full-bleed for our app */
html, body, #wpwrap, #wpcontent { background: #fff !important; margin: 0 !important; padding: 0 !important; }
#woonoow-admin-app, #woonoow-admin-app > div { margin: 0 !important; padding: 0 !important; max-width: 100% !important; }
html,
body,
#wpwrap,
#wpcontent {
background: #fff !important;
margin: 0 !important;
padding: 0 !important;
}
#woonoow-admin-app,
#woonoow-admin-app>div {
margin: 0 !important;
padding: 0 !important;
max-width: 100% !important;
}
/* Hide elements flagged as no-print, reveal print-only */
.no-print { display: none !important; }
.print-only { display: block !important; }
.no-print {
display: none !important;
}
.print-only {
display: block !important;
}
/* Improve table row density on paper */
.print-tight tr > * { padding-top: 6px !important; padding-bottom: 6px !important; }
.print-tight tr>* {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
}
/* By default, label-only content stays hidden unless in print or label mode */
.print-only { display: none; }
.print-only {
display: none;
}
/* Label mode toggled by router (?mode=label) */
.woonoow-label-mode .print-only { display: block; }
.woonoow-label-mode .print-only {
display: block;
}
.woonoow-label-mode .no-print-label,
.woonoow-label-mode .wp-header-end,
.woonoow-label-mode .wrap { display: none !important; }
.woonoow-label-mode .wrap {
display: none !important;
}
/* Optional page presets (opt-in by adding the class to a wrapper before printing) */
.print-a4 { }
.print-letter { }
.print-4x6 { }
.print-a4 {}
.print-letter {}
.print-4x6 {}
@media print {
.print-a4 { }
.print-letter { }
/* A4 Invoice layout */
@page {
size: A4;
margin: 0;
}
.print-a4 {
width: 210mm !important;
min-height: 297mm !important;
padding: 20mm !important;
margin: 0 auto !important;
box-sizing: border-box !important;
background: white !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.print-a4 * {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Ensure backgrounds print */
.print-a4 .bg-gray-50 {
background-color: #f9fafb !important;
}
.print-a4 .bg-gray-900 {
background-color: #111827 !important;
}
.print-a4 .text-white {
color: white !important;
}
.print-letter {}
/* Thermal label (4x6in) with minimal margins */
.print-4x6 { width: 6in; }
.print-4x6 * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.print-4x6 {
width: 6in;
}
.print-4x6 * {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
/* --- WooNooW: Popper menus & fullscreen fixes --- */
[data-radix-popper-content-wrapper] { z-index: 2147483647 !important; }
body.woonoow-fullscreen .woonoow-app { overflow: visible; }
[data-radix-popper-content-wrapper] {
z-index: 2147483647 !important;
}
body.woonoow-fullscreen .woonoow-app {
overflow: visible;
}
/* --- WooCommerce Admin Notices --- */
.woocommerce-message,

View File

@@ -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' && (