refine: Polish mobile Orders UI based on feedback

Addressed all three feedback points from user testing.

1. OrderCard Layout Improvements
   Problem: Card felt too dense, cramped spacing

   Changes:
   - Increased icon size: 10x10 → 12x12
   - Increased icon padding: w-10 h-10 → w-12 h-12
   - Rounded corners: rounded-lg → rounded-xl
   - Added shadow-sm for depth
   - Increased gap between elements: gap-3 → gap-4
   - Added space-y-2 for vertical rhythm
   - Made order number bolder: font-semibold → font-bold
   - Increased order number size: text-base → text-lg
   - Made customer name font-medium (was muted)
   - Made total amount bolder and colored: font-semibold → font-bold text-primary
   - Increased total size: text-base → text-lg
   - Better status badge: px-2 py-0.5 → px-2.5 py-1, font-medium → font-semibold
   - Larger checkbox: default → w-5 h-5
   - Centered chevron vertically: mt-2 → self-center

   Result: More breathing room, better hierarchy, easier to scan

2. FilterBottomSheet Z-Index & Padding
   Problem: Bottom sheet covered by FAB and bottom nav

   Changes:
   - Increased backdrop z-index: z-40 → z-[60]
   - Increased sheet z-index: z-50 → z-[70] (above FAB z-50)
   - Made sheet flexbox: added flex flex-col
   - Made content scrollable: added flex-1 overflow-y-auto
   - Added bottom padding: pb-24 (space for bottom nav)

   Result: Sheet now covers FAB, content scrolls, bottom nav visible

3. Contextual Headers for Order Pages
   Problem: Order Detail, New, Edit pages are actionable but had no headers

   Solution: Added contextual headers to all three pages

   Order Detail:
   - Header: "Order #337"
   - Actions: [Invoice] [Edit] buttons
   - Shows order number dynamically
   - Hides in print mode

   New Order:
   - Header: "New Order"
   - No actions (form has submit)

   Edit Order:
   - Header: "Edit Order #337"
   - No actions (form has submit)
   - Shows order number dynamically

   Implementation:
   - Import usePageHeader
   - useEffect to set/clear header
   - Order Detail: Custom action buttons
   - New/Edit: Simple title only

Files Modified:
- routes/Orders/components/OrderCard.tsx
- routes/Orders/components/FilterBottomSheet.tsx
- routes/Orders/Detail.tsx
- routes/Orders/New.tsx
- routes/Orders/Edit.tsx

Result:
 Cards feel spacious and scannable
 Filter sheet properly layered
 Order pages have contextual headers
 Consistent mobile UX across all order flows
 Professional, polished feel! 🎯
This commit is contained in:
dwindown
2025-11-08 13:35:24 +07:00
parent e0a236fc64
commit c62fbd9436
6 changed files with 73 additions and 20 deletions

View File

@@ -21,6 +21,7 @@ import { showErrorToast, showSuccessToast, getPageLoadErrorMessage } from '@/lib
import { ErrorCard } from '@/components/ErrorCard'; import { ErrorCard } from '@/components/ErrorCard';
import { InlineLoadingState } from '@/components/LoadingState'; import { InlineLoadingState } from '@/components/LoadingState';
import { __ } from '@/lib/i18n'; import { __ } from '@/lib/i18n';
import { usePageHeader } from '@/contexts/PageHeaderContext';
function Money({ value, currency, symbol }: { value?: number; currency?: string; symbol?: string }) { function Money({ value, currency, symbol }: { value?: number; currency?: string; symbol?: string }) {
return <>{formatMoney(value, { currency, symbol })}</>; return <>{formatMoney(value, { currency, symbol })}</>;
@@ -44,6 +45,7 @@ export default function OrderShow() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const qc = useQueryClient(); const qc = useQueryClient();
const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW'; const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW';
const { setPageHeader, clearPageHeader } = usePageHeader();
const [params, setParams] = useSearchParams(); const [params, setParams] = useSearchParams();
const mode = params.get('mode'); // undefined | 'label' | 'invoice' const mode = params.get('mode'); // undefined | 'label' | 'invoice'
@@ -140,6 +142,36 @@ export default function OrderShow() {
retryPaymentMutation.mutate(); retryPaymentMutation.mutate();
} }
// Set page header with actions
useEffect(() => {
if (!order || isPrintMode) {
clearPageHeader();
return;
}
const actions = (
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={printInvoice}>
<FileText className="w-4 h-4 mr-1" />
{__('Invoice')}
</Button>
<Link to={`/orders/${id}/edit`}>
<Button size="sm">
<Pencil className="w-4 h-4 mr-1" />
{__('Edit')}
</Button>
</Link>
</div>
);
setPageHeader(
order.number ? `${__('Order')} #${order.number}` : __('Order'),
actions
);
return () => clearPageHeader();
}, [order, isPrintMode, id, setPageHeader, clearPageHeader]);
useEffect(() => { useEffect(() => {
if (!isPrintMode || !qrRef.current || !order) return; if (!isPrintMode || !qrRef.current || !order) return;
(async () => { (async () => {

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import OrderForm from '@/routes/Orders/partials/OrderForm'; import OrderForm from '@/routes/Orders/partials/OrderForm';
@@ -8,12 +8,14 @@ import { ErrorCard } from '@/components/ErrorCard';
import { LoadingState } from '@/components/LoadingState'; import { LoadingState } from '@/components/LoadingState';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { __, sprintf } from '@/lib/i18n'; import { __, sprintf } from '@/lib/i18n';
import { usePageHeader } from '@/contexts/PageHeaderContext';
export default function OrdersEdit() { export default function OrdersEdit() {
const { id } = useParams(); const { id } = useParams();
const orderId = Number(id); const orderId = Number(id);
const nav = useNavigate(); const nav = useNavigate();
const qc = useQueryClient(); const qc = useQueryClient();
const { setPageHeader, clearPageHeader } = usePageHeader();
const countriesQ = useQuery({ queryKey: ['countries'], queryFn: OrdersApi.countries }); const countriesQ = useQuery({ queryKey: ['countries'], queryFn: OrdersApi.countries });
const paymentsQ = useQuery({ queryKey: ['payments'], queryFn: OrdersApi.payments }); const paymentsQ = useQuery({ queryKey: ['payments'], queryFn: OrdersApi.payments });
@@ -56,6 +58,16 @@ export default function OrdersEdit() {
const order = orderQ.data || {}; const order = orderQ.data || {};
// Set page header
useEffect(() => {
if (order.number) {
setPageHeader(sprintf(__('Edit Order #%s'), order.number));
} else {
setPageHeader(__('Edit Order'));
}
return () => clearPageHeader();
}, [order.number, setPageHeader, clearPageHeader]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { OrdersApi } from '@/lib/api'; import { OrdersApi } from '@/lib/api';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -6,10 +6,13 @@ import OrderForm from '@/routes/Orders/partials/OrderForm';
import { getStoreCurrency } from '@/lib/currency'; import { getStoreCurrency } from '@/lib/currency';
import { showErrorToast, showSuccessToast } from '@/lib/errorHandling'; import { showErrorToast, showSuccessToast } from '@/lib/errorHandling';
import { __, sprintf } from '@/lib/i18n'; import { __, sprintf } from '@/lib/i18n';
import { usePageHeader } from '@/contexts/PageHeaderContext';
import { Button } from '@/components/ui/button';
export default function OrdersNew() { export default function OrdersNew() {
const nav = useNavigate(); const nav = useNavigate();
const qc = useQueryClient(); const qc = useQueryClient();
const { setPageHeader, clearPageHeader } = usePageHeader();
// Countries from Woo (allowed + default + states) // Countries from Woo (allowed + default + states)
const countriesQ = useQuery({ queryKey: ['countries'], queryFn: OrdersApi.countries }); const countriesQ = useQuery({ queryKey: ['countries'], queryFn: OrdersApi.countries });
@@ -37,6 +40,12 @@ export default function OrdersNew() {
// Prefer global store currency injected by PHP // Prefer global store currency injected by PHP
const { currency: storeCurrency, symbol: storeSymbol } = getStoreCurrency(); const { currency: storeCurrency, symbol: storeSymbol } = getStoreCurrency();
// Set page header
useEffect(() => {
setPageHeader(__('New Order'));
return () => clearPageHeader();
}, [setPageHeader, clearPageHeader]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@@ -44,12 +44,12 @@ export function FilterBottomSheet({
<> <>
{/* Backdrop */} {/* Backdrop */}
<div <div
className="fixed inset-0 bg-black/50 z-40 md:hidden" className="fixed inset-0 bg-black/50 z-[60] md:hidden"
onClick={onClose} onClick={onClose}
/> />
{/* Bottom Sheet */} {/* Bottom Sheet */}
<div className="fixed inset-x-0 bottom-0 z-50 bg-background rounded-t-2xl shadow-2xl max-h-[85vh] overflow-y-auto md:hidden animate-in slide-in-from-bottom duration-300"> <div className="fixed inset-x-0 bottom-0 z-[70] bg-background rounded-t-2xl shadow-2xl max-h-[85vh] flex flex-col md:hidden animate-in slide-in-from-bottom duration-300">
{/* Drag Handle */} {/* Drag Handle */}
<div className="flex justify-center pt-3 pb-2"> <div className="flex justify-center pt-3 pb-2">
<div className="w-12 h-1.5 bg-muted-foreground/30 rounded-full" /> <div className="w-12 h-1.5 bg-muted-foreground/30 rounded-full" />
@@ -67,7 +67,7 @@ export function FilterBottomSheet({
</div> </div>
{/* Content */} {/* Content */}
<div className="p-4 space-y-4"> <div className="flex-1 overflow-y-auto p-4 space-y-4 pb-24">
{/* Status Filter */} {/* Status Filter */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">{__('Status')}</label> <label className="text-sm font-medium">{__('Status')}</label>

View File

@@ -29,9 +29,9 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
return ( return (
<Link <Link
to={`/orders/${order.id}`} to={`/orders/${order.id}`}
className="block bg-card border border-border rounded-lg p-4 hover:bg-accent/50 transition-colors active:scale-[0.98] active:transition-transform" className="block bg-card border border-border rounded-xl p-4 hover:bg-accent/50 transition-colors active:scale-[0.98] active:transition-transform shadow-sm"
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-4">
{/* Checkbox */} {/* Checkbox */}
{onSelect && ( {onSelect && (
<div <div
@@ -40,48 +40,49 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
e.stopPropagation(); e.stopPropagation();
onSelect(order.id); onSelect(order.id);
}} }}
className="pt-1" className="pt-0.5"
> >
<Checkbox <Checkbox
checked={selected} checked={selected}
aria-label={__('Select order')} aria-label={__('Select order')}
className="w-5 h-5"
/> />
</div> </div>
)} )}
{/* Icon */} {/* Icon */}
<div className="flex-shrink-0 w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center"> <div className="flex-shrink-0 w-12 h-12 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
<Package className="w-5 h-5" /> <Package className="w-6 h-6" />
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 space-y-2">
{/* Order Number & Status */} {/* Order Number & Status */}
<div className="flex items-center justify-between gap-2 mb-1"> <div className="flex items-start justify-between gap-3">
<h3 className="font-semibold text-base">#{order.number}</h3> <h3 className="font-bold text-lg leading-tight">#{order.number}</h3>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium capitalize ${statusClass}`}> <span className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold capitalize whitespace-nowrap ${statusClass}`}>
{order.status || 'unknown'} {order.status || 'unknown'}
</span> </span>
</div> </div>
{/* Customer */} {/* Customer */}
<div className="text-sm text-muted-foreground mb-2"> <div className="text-sm font-medium text-foreground">
{order.customer || __('Guest')} {order.customer || __('Guest')}
</div> </div>
{/* Items Brief */} {/* Items Brief */}
{order.items_brief && ( {order.items_brief && (
<div className="text-sm text-muted-foreground mb-2 truncate"> <div className="text-sm text-muted-foreground truncate">
{order.items_count} {order.items_count === 1 ? __('item') : __('items')} · {order.items_brief} {order.items_count} {order.items_count === 1 ? __('item') : __('items')} · {order.items_brief}
</div> </div>
)} )}
{/* Date & Total */} {/* Date & Total */}
<div className="flex items-center justify-between gap-2 mt-2"> <div className="flex items-center justify-between gap-3 pt-1">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{formatRelativeOrDate(order.date_ts)} {formatRelativeOrDate(order.date_ts)}
</span> </span>
<span className="font-semibold text-base tabular-nums"> <span className="font-bold text-lg tabular-nums text-primary">
{formatMoney(order.total, { {formatMoney(order.total, {
currency: order.currency || currencyConfig.currency, currency: order.currency || currencyConfig.currency,
symbol: order.currency_symbol || currencyConfig.symbol, symbol: order.currency_symbol || currencyConfig.symbol,
@@ -95,7 +96,7 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
</div> </div>
{/* Chevron */} {/* Chevron */}
<ChevronRight className="w-5 h-5 text-muted-foreground flex-shrink-0 mt-2" /> <ChevronRight className="w-5 h-5 text-muted-foreground flex-shrink-0 self-center" />
</div> </div>
</Link> </Link>
); );

View File

@@ -16,7 +16,6 @@ export function SearchBar({ value, onChange, onFilterClick, filterCount = 0 }: S
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input <input
type="text"
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder={__('Search orders...')} placeholder={__('Search orders...')}