feat: Create dedicated Invoice and Label pages

Invoice Page (/orders/:id/invoice):
- A4-ready layout (210mm x 297mm)
- Store header, invoice number, QR code
- Billing/shipping address sections
- Styled items table with alternating rows
- Totals summary with conditional display
- Thank you footer
- Back to Order and Print buttons

Label Page (/orders/:id/label):
- 4x6 inch thermal label layout
- Ship To address with phone
- Items list (physical products only)
- Shipping method
- QR code for scanning
- Back to Order and Print buttons

Order Detail:
- Removed print-mode logic
- Removed print-only layouts
- Invoice/Label buttons now link to dedicated pages
- Label button still hidden for virtual-only orders
This commit is contained in:
Dwindi Ramadhana
2026-01-06 20:57:57 +07:00
parent 40aee67c46
commit 1cef11a1d2
4 changed files with 369 additions and 187 deletions

View File

@@ -13,6 +13,8 @@ import OrdersIndex from '@/routes/Orders';
import OrderNew from '@/routes/Orders/New'; import OrderNew from '@/routes/Orders/New';
import OrderEdit from '@/routes/Orders/Edit'; import OrderEdit from '@/routes/Orders/Edit';
import OrderDetail from '@/routes/Orders/Detail'; import OrderDetail from '@/routes/Orders/Detail';
import OrderInvoice from '@/routes/Orders/Invoice';
import OrderLabel from '@/routes/Orders/Label';
import ProductsIndex from '@/routes/Products'; import ProductsIndex from '@/routes/Products';
import ProductNew from '@/routes/Products/New'; import ProductNew from '@/routes/Products/New';
import ProductEdit from '@/routes/Products/Edit'; import ProductEdit from '@/routes/Products/Edit';
@@ -551,6 +553,8 @@ function AppRoutes() {
<Route path="/orders/new" element={<OrderNew />} /> <Route path="/orders/new" element={<OrderNew />} />
<Route path="/orders/:id" element={<OrderDetail />} /> <Route path="/orders/:id" element={<OrderDetail />} />
<Route path="/orders/:id/edit" element={<OrderEdit />} /> <Route path="/orders/:id/edit" element={<OrderEdit />} />
<Route path="/orders/:id/invoice" element={<OrderInvoice />} />
<Route path="/orders/:id/label" element={<OrderLabel />} />
{/* Coupons (under Marketing) */} {/* Coupons (under Marketing) */}
<Route path="/coupons" element={<CouponsIndex />} /> <Route path="/coupons" element={<CouponsIndex />} />

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams, useSearchParams, Link, useNavigate } from 'react-router-dom'; import { useParams, Link, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api, OrdersApi } from '@/lib/api'; import { api, OrdersApi } from '@/lib/api';
import { formatRelativeOrDate } from '@/lib/dates'; import { formatRelativeOrDate } from '@/lib/dates';
import { formatMoney } from '@/lib/currency'; import { formatMoney } from '@/lib/currency';
import { ArrowLeft, Printer, ExternalLink, Loader2, Ticket, FileText, Pencil, RefreshCw } from 'lucide-react'; import { ExternalLink, Loader2, Ticket, FileText, RefreshCw } from 'lucide-react';
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
import { import {
AlertDialog, AlertDialog,
@@ -48,28 +48,7 @@ export default function OrderShow() {
const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW'; const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW';
const { setPageHeader, clearPageHeader } = usePageHeader(); const { setPageHeader, clearPageHeader } = usePageHeader();
const [params, setParams] = useSearchParams();
const mode = params.get('mode'); // undefined | 'label' | 'invoice'
const isPrintMode = mode === 'label' || mode === 'invoice';
function triggerPrint(nextMode: 'label' | 'invoice') {
params.set('mode', nextMode);
setParams(params, { replace: true });
setTimeout(() => {
window.print();
params.delete('mode');
setParams(params, { replace: true });
}, 50);
}
function printLabel() {
triggerPrint('label');
}
function printInvoice() {
triggerPrint('invoice');
}
const [showRetryDialog, setShowRetryDialog] = useState(false); const [showRetryDialog, setShowRetryDialog] = useState(false);
const qrRef = useRef<HTMLCanvasElement | null>(null);
const q = useQuery({ const q = useQuery({
queryKey: ['order', id], queryKey: ['order', id],
enabled: !!id, enabled: !!id,
@@ -154,7 +133,7 @@ export default function OrderShow() {
// Set contextual header with Back button and Edit action // Set contextual header with Back button and Edit action
useEffect(() => { useEffect(() => {
if (!order || isPrintMode) { if (!order) {
clearPageHeader(); clearPageHeader();
return; return;
} }
@@ -178,37 +157,20 @@ export default function OrderShow() {
); );
return () => clearPageHeader(); return () => clearPageHeader();
}, [order, isPrintMode, id, setPageHeader, clearPageHeader, nav]); }, [order, id, setPageHeader, clearPageHeader, nav]);
useEffect(() => {
if (!isPrintMode || !qrRef.current || !order) return;
(async () => {
try {
const mod = await import('qrcode');
const QR = (mod as any).default || (mod as any);
const text = `ORDER:${order.number || id}`;
await QR.toCanvas(qrRef.current, text, { width: 128, margin: 1 });
} catch (_) {
// optional dependency not installed; silently ignore
}
})();
}, [mode, order, id, isPrintMode]);
return ( return (
<div className={`space-y-4 ${mode === 'label' ? 'woonoow-label-mode' : ''}`}> <div className="space-y-4">
{/* Desktop extra actions - hidden on mobile, shown on desktop */} {/* Desktop extra actions - hidden on mobile, shown on desktop */}
<div className="hidden md:flex flex-wrap items-center gap-2"> <div className="hidden md:flex flex-wrap items-center gap-2">
<div className="ml-auto flex flex-wrap items-center gap-2"> <div className="ml-auto flex flex-wrap items-center gap-2">
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printInvoice} title={__('Print order')}> <Link to={`/orders/${id}/invoice`} className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 hover:bg-gray-50">
<Printer className="w-4 h-4" /> {__('Print')}
</button>
<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')} <FileText className="w-4 h-4" /> {__('Invoice')}
</button> </Link>
{!isVirtualOnly && ( {!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')}> <Link to={`/orders/${id}/label`} className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 hover:bg-gray-50">
<Ticket className="w-4 h-4" /> {__('Label')} <Ticket className="w-4 h-4" /> {__('Label')}
</button> </Link>
)} )}
</div> </div>
</div> </div>
@@ -472,145 +434,6 @@ export default function OrderShow() {
</div> </div>
</div> </div>
)} )}
{/* Print-only layouts */}
{order && (
<div className="print-only">
{mode === 'invoice' && (
<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-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="text-xl font-semibold text-gray-900">{siteTitle}</div>
<div className="text-sm text-gray-500 mt-1">{window.location.origin}</div>
</div>
</div>
{/* Invoice Meta */}
<div className="grid grid-cols-2 gap-8 mb-8">
<div>
<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="flex justify-end">
<canvas ref={qrRef} className="w-24 h-24" style={{ border: '1px solid #e5e7eb' }} />
</div>
</div>
{/* 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 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, 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>
{/* 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' && (
<div className="p-4 print-4x6">
<div className="border rounded p-4 h-full">
<div className="flex justify-between items-start mb-3">
<div className="text-base font-semibold">#{order.number}</div>
<canvas ref={qrRef} className="w-24 h-24 border" />
</div>
<div className="mb-3">
<div className="text-xs opacity-60 mb-1">{__('Ship To')}</div>
<div className="text-sm" dangerouslySetInnerHTML={{ __html: order.shipping?.address || order.billing?.address || '' }} />
</div>
<div className="text-xs opacity-60 mb-1">{__('Items')}</div>
<ul className="text-sm list-disc pl-4">
{(order.items || []).map((it: any) => (
<li key={it.id}>{it.name} ×{it.qty}</li>
))}
</ul>
</div>
</div>
)}
</div>
)}
</div> </div>
); );
} }

View File

@@ -0,0 +1,219 @@
import React, { useEffect, useRef } from 'react';
import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { formatMoney } from '@/lib/currency';
import { ArrowLeft, Printer } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { InlineLoadingState } from '@/components/LoadingState';
import { ErrorCard } from '@/components/ErrorCard';
import { getPageLoadErrorMessage } from '@/lib/errorHandling';
import { __ } from '@/lib/i18n';
function Money({ value, currency, symbol }: { value?: number; currency?: string; symbol?: string }) {
return <>{formatMoney(value, { currency, symbol })}</>;
}
export default function Invoice() {
const { id } = useParams<{ id: string }>();
const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW';
const qrRef = useRef<HTMLCanvasElement | null>(null);
const q = useQuery({
queryKey: ['order', id],
enabled: !!id,
queryFn: () => api.get(`/orders/${id}`),
});
const order = q.data;
// Check if all items are virtual
const isVirtualOnly = React.useMemo(() => {
if (!order?.items || order.items.length === 0) return false;
return order.items.every((item: any) => item.virtual || item.downloadable);
}, [order?.items]);
// Generate QR code
useEffect(() => {
if (!qrRef.current || !order) return;
(async () => {
try {
const mod = await import('qrcode');
const QR = (mod as any).default || (mod as any);
const text = `ORDER:${order.number || id}`;
await QR.toCanvas(qrRef.current, text, { width: 96, margin: 1 });
} catch (_) {
// QR library not available
}
})();
}, [order, id]);
const handlePrint = () => {
window.print();
};
if (q.isLoading) {
return <InlineLoadingState message={__('Loading invoice...')} />;
}
if (q.isError) {
return (
<ErrorCard
title={__('Failed to load order')}
message={getPageLoadErrorMessage(q.error)}
onRetry={() => q.refetch()}
/>
);
}
if (!order) return null;
return (
<div className="min-h-screen bg-gray-100">
{/* Actions bar - hidden on print */}
<div className="no-print bg-white border-b sticky top-0 z-10">
<div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
<Link to={`/orders/${id}`}>
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
{__('Back to Order')}
</Button>
</Link>
<Button onClick={handlePrint} size="sm">
<Printer className="w-4 h-4 mr-2" />
{__('Print Invoice')}
</Button>
</div>
</div>
{/* Invoice content */}
<div className="py-8 print:py-0">
<div
className="print-a4 bg-white shadow-lg print:shadow-none mx-auto"
style={{
width: '210mm',
minHeight: '297mm',
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-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="text-xl font-semibold text-gray-900">{siteTitle}</div>
<div className="text-sm text-gray-500 mt-1">{window.location.origin}</div>
</div>
</div>
{/* Invoice Meta */}
<div className="grid grid-cols-2 gap-8 mb-8">
<div>
<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="flex justify-end">
<canvas ref={qrRef} className="w-24 h-24" style={{ border: '1px solid #e5e7eb' }} />
</div>
</div>
{/* 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 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, 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>
{/* 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>
</div>
</div>
);
}

View File

@@ -0,0 +1,136 @@
import React, { useEffect, useRef } from 'react';
import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { ArrowLeft, Printer } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { InlineLoadingState } from '@/components/LoadingState';
import { ErrorCard } from '@/components/ErrorCard';
import { getPageLoadErrorMessage } from '@/lib/errorHandling';
import { __ } from '@/lib/i18n';
export default function Label() {
const { id } = useParams<{ id: string }>();
const qrRef = useRef<HTMLCanvasElement | null>(null);
const q = useQuery({
queryKey: ['order', id],
enabled: !!id,
queryFn: () => api.get(`/orders/${id}`),
});
const order = q.data;
// Generate QR code
useEffect(() => {
if (!qrRef.current || !order) return;
(async () => {
try {
const mod = await import('qrcode');
const QR = (mod as any).default || (mod as any);
const text = `ORDER:${order.number || id}`;
await QR.toCanvas(qrRef.current, text, { width: 128, margin: 1 });
} catch (_) {
// QR library not available
}
})();
}, [order, id]);
const handlePrint = () => {
window.print();
};
if (q.isLoading) {
return <InlineLoadingState message={__('Loading label...')} />;
}
if (q.isError) {
return (
<ErrorCard
title={__('Failed to load order')}
message={getPageLoadErrorMessage(q.error)}
onRetry={() => q.refetch()}
/>
);
}
if (!order) return null;
return (
<div className="min-h-screen bg-gray-100">
{/* Actions bar - hidden on print */}
<div className="no-print bg-white border-b sticky top-0 z-10">
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center justify-between">
<Link to={`/orders/${id}`}>
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
{__('Back to Order')}
</Button>
</Link>
<Button onClick={handlePrint} size="sm">
<Printer className="w-4 h-4 mr-2" />
{__('Print Label')}
</Button>
</div>
</div>
{/* Label content - 4x6 inch thermal label size */}
<div className="py-8 print:py-0 flex justify-center">
<div
className="print-4x6 bg-white shadow-lg print:shadow-none"
style={{
width: '4in',
height: '6in',
padding: '0.5in',
boxSizing: 'border-box'
}}
>
{/* Order Number & QR */}
<div className="flex justify-between items-start mb-4">
<div>
<div className="text-2xl font-bold text-gray-900">#{order.number}</div>
<div className="text-xs text-gray-500 mt-1">
{new Date((order.date_ts || 0) * 1000).toLocaleDateString()}
</div>
</div>
<canvas ref={qrRef} className="w-24 h-24" style={{ border: '1px solid #e5e7eb' }} />
</div>
{/* Ship To */}
<div className="mb-4 p-3 border-2 border-gray-900 rounded">
<div className="text-xs font-bold text-gray-500 uppercase tracking-wide mb-2">{__('SHIP TO')}</div>
<div className="text-lg font-bold text-gray-900">{order.shipping?.name || order.billing?.name || '—'}</div>
{(order.shipping?.phone || order.billing?.phone) && (
<div className="text-sm text-gray-700 mt-1">{order.shipping?.phone || order.billing?.phone}</div>
)}
<div
className="text-sm text-gray-700 mt-2 leading-relaxed"
dangerouslySetInnerHTML={{ __html: order.shipping?.address || order.billing?.address || '' }}
/>
</div>
{/* Items */}
<div className="mb-4">
<div className="text-xs font-bold text-gray-500 uppercase tracking-wide mb-2">{__('ITEMS')}</div>
<ul className="text-sm space-y-1">
{(order.items || []).filter((it: any) => !it.virtual && !it.downloadable).map((it: any) => (
<li key={it.id} className="flex justify-between">
<span className="truncate">{it.name}</span>
<span className="font-medium ml-2">×{it.qty}</span>
</li>
))}
</ul>
</div>
{/* Shipping Method */}
{order.shipping_method && (
<div className="pt-3 border-t border-gray-200">
<div className="text-xs font-bold text-gray-500 uppercase tracking-wide mb-1">{__('SHIPPING METHOD')}</div>
<div className="text-sm font-medium text-gray-900">{order.shipping_method}</div>
</div>
)}
</div>
</div>
</div>
);
}