import React, { useEffect, useRef, useState } from 'react'; import { useParams, useSearchParams, Link, useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api, OrdersApi } from '@/lib/api'; import { formatRelativeOrDate } from '@/lib/dates'; import { formatMoney } from '@/lib/currency'; import { ArrowLeft, Printer, ExternalLink, Loader2, Ticket, FileText, Pencil, RefreshCw } from 'lucide-react'; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { showErrorToast, showSuccessToast, getPageLoadErrorMessage } from '@/lib/errorHandling'; import { ErrorCard } from '@/components/ErrorCard'; import { InlineLoadingState } from '@/components/LoadingState'; import { __ } from '@/lib/i18n'; import { usePageHeader } from '@/contexts/PageHeaderContext'; function Money({ value, currency, symbol }: { value?: number; currency?: string; symbol?: string }) { return <>{formatMoney(value, { currency, symbol })}; } function StatusBadge({ status }: { status?: string }) { const s = (status || '').toLowerCase(); const cls = 'inline-flex items-center rounded px-2 py-1 text-xs font-medium border'; let tone = 'bg-gray-100 text-gray-700 border-gray-200'; if (s === 'completed' || s === 'paid') tone = 'bg-green-100 text-green-800 border-green-200'; else if (s === 'processing') tone = 'bg-yellow-100 text-yellow-800 border-yellow-200'; else if (s === 'on-hold') tone = 'bg-amber-100 text-amber-800 border-amber-200'; else if (s === 'pending') tone = 'bg-orange-100 text-orange-800 border-orange-200'; else if (s === 'cancelled' || s === 'failed' || s === 'refunded') tone = 'bg-red-100 text-red-800 border-red-200'; return {status ? status[0].toUpperCase() + status.slice(1) : '—'}; } const STATUS_OPTIONS = ['pending', 'processing', 'completed', 'on-hold', 'cancelled', 'refunded', 'failed']; export default function OrderShow() { const { id } = useParams<{ id: string }>(); const qc = useQueryClient(); const nav = useNavigate(); const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW'; 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 qrRef = useRef(null); const q = useQuery({ queryKey: ['order', id], enabled: !!id, queryFn: () => api.get(`/orders/${id}`), }); const order = q.data; // Check if all items are virtual (digital products only) 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]); // Mutation for status update with optimistic update const statusMutation = useMutation({ mutationFn: (nextStatus: string) => OrdersApi.update(Number(id), { status: nextStatus }), onMutate: async (nextStatus) => { // Cancel outgoing refetches await qc.cancelQueries({ queryKey: ['order', id] }); // Snapshot previous value const previous = qc.getQueryData(['order', id]); // Optimistically update qc.setQueryData(['order', id], (old: any) => ({ ...old, status: nextStatus, })); return { previous }; }, onSuccess: () => { showSuccessToast(__('Order status updated')); // Refetch to get server state q.refetch(); }, onError: (err: any, _variables, context) => { // Rollback on error if (context?.previous) { qc.setQueryData(['order', id], context.previous); } showErrorToast(err, __('Failed to update status')); }, }); function handleStatusChange(nextStatus: string) { if (!id) return; statusMutation.mutate(nextStatus); } // Mutation for retry payment const retryPaymentMutation = useMutation({ mutationFn: () => api.post(`/orders/${id}/retry-payment`, {}), onSuccess: () => { showSuccessToast(__('Payment processing retried')); q.refetch(); }, onError: (err: any) => { showErrorToast(err, __('Failed to retry payment')); }, }); function handleRetryPayment() { if (!id) return; setShowRetryDialog(true); } function confirmRetryPayment() { setShowRetryDialog(false); retryPaymentMutation.mutate(); } // Smart back handler: go back in history if available, otherwise fallback to /orders const handleBack = () => { if (window.history.state?.idx > 0) { nav(-1); // Go back in history } else { nav('/orders'); // Fallback to orders index } }; // Set contextual header with Back button and Edit action useEffect(() => { if (!order || isPrintMode) { clearPageHeader(); return; } const actions = (
); setPageHeader( order.number ? `${__('Order')} #${order.number}` : __('Order'), actions ); return () => clearPageHeader(); }, [order, isPrintMode, 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 (
{/* Desktop extra actions - hidden on mobile, shown on desktop */}
{__('Orders')}
{q.isLoading && } {q.isError && ( q.refetch()} /> )} {order && (
{/* Left column */}
{/* Summary */}
{__('Summary')}
{statusMutation.isPending && ( )}
{__('Date')}
{formatRelativeOrDate(order.date_ts)}
{__('Payment')}
{order.payment_method || '—'}
{__('Shipping')}
{order.shipping_method || '—'}
{__('Status')}
{/* Payment Instructions */} {order.payment_meta && order.payment_meta.length > 0 && (
{__('Payment Instructions')}
{['pending', 'on-hold', 'failed'].includes(order.status) && ( <> {__('Retry Payment')} {__('Are you sure you want to retry payment processing for this order?')}
{__('This will create a new payment transaction.')}
setShowRetryDialog(false)}> {__('Cancel')} {retryPaymentMutation.isPending ? ( ) : ( )} {__('Retry Payment')}
)}
{order.payment_meta.map((meta: any) => (
{meta.label}
{meta.key.includes('url') || meta.key.includes('redirect') ? ( {meta.value} ) : meta.key.includes('amount') ? ( ) : ( meta.value )}
))}
)} {/* Items */}
{__('Items')}
{/* Desktop/table view */}
{order.items?.map((it: any) => ( ))} {!order.items?.length && ( )}
{__('Product')} {__('Qty')} {__('Subtotal')} {__('Total')}
{it.name}
{it.sku ?
SKU: {it.sku}
: null}
×{it.qty}
{__('No items')}
{/* Mobile/card view */}
{order.items?.length ? ( order.items.map((it: any) => (
{it.name}
{it.sku ?
SKU: {it.sku}
: null}
×{it.qty}
{__('Subtotal')}
{__('Total')}
)) ) : (
{__('No items')}
)}
{/* Notes */}
{__('Order Notes')}
{order.notes?.length ? order.notes.map((n: any, idx: number) => (
{n.date ? new Date(n.date).toLocaleString() : ''} {n.is_customer_note ? '· customer' : ''}
{n.content}
)) :
{__('No notes')}
}
{/* Right column */}
{__('Totals')}
{__('Subtotal')}
{__('Discount')}
{__('Shipping')}
{__('Tax')}
{__('Total')}
{__('Billing')}
{order.billing?.name || '—'}
{order.billing?.email && (
{order.billing.email}
)} {order.billing?.phone && (
{order.billing.phone}
)}
{/* Only show shipping for physical products */} {!isVirtualOnly && (
{__('Shipping')}
{order.shipping?.name || '—'}
)} {/* Customer Note */} {order.customer_note && (
{__('Customer Note')}
{order.customer_note}
)}
)} {/* Print-only layouts */} {order && (
{mode === 'invoice' && (
Invoice
Order #{order.number} · {new Date((order.date_ts||0)*1000).toLocaleString()}
{siteTitle}
{window.location.origin}
{__('Bill To')}
{(order.items || []).map((it:any) => ( ))}
Product Qty Subtotal Total
{it.name} ×{it.qty}
Subtotal
Discount
Shipping
Tax
Total
)} {mode === 'label' && (
#{order.number}
{__('Ship To')}
{__('Items')}
    {(order.items||[]).map((it:any)=> (
  • {it.name} ×{it.qty}
  • ))}
)}
)}
); }