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:
@@ -21,6 +21,7 @@ import { showErrorToast, showSuccessToast, getPageLoadErrorMessage } from '@/lib
|
||||
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 })}</>;
|
||||
@@ -44,6 +45,7 @@ export default function OrderShow() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const qc = useQueryClient();
|
||||
const siteTitle = (window as any).wnw?.siteTitle || 'WooNooW';
|
||||
const { setPageHeader, clearPageHeader } = usePageHeader();
|
||||
|
||||
const [params, setParams] = useSearchParams();
|
||||
const mode = params.get('mode'); // undefined | 'label' | 'invoice'
|
||||
@@ -140,6 +142,36 @@ export default function OrderShow() {
|
||||
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(() => {
|
||||
if (!isPrintMode || !qrRef.current || !order) return;
|
||||
(async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import OrderForm from '@/routes/Orders/partials/OrderForm';
|
||||
@@ -8,12 +8,14 @@ import { ErrorCard } from '@/components/ErrorCard';
|
||||
import { LoadingState } from '@/components/LoadingState';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { __, sprintf } from '@/lib/i18n';
|
||||
import { usePageHeader } from '@/contexts/PageHeaderContext';
|
||||
|
||||
export default function OrdersEdit() {
|
||||
const { id } = useParams();
|
||||
const orderId = Number(id);
|
||||
const nav = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
const { setPageHeader, clearPageHeader } = usePageHeader();
|
||||
|
||||
const countriesQ = useQuery({ queryKey: ['countries'], queryFn: OrdersApi.countries });
|
||||
const paymentsQ = useQuery({ queryKey: ['payments'], queryFn: OrdersApi.payments });
|
||||
@@ -56,6 +58,16 @@ export default function OrdersEdit() {
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { OrdersApi } from '@/lib/api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -6,10 +6,13 @@ import OrderForm from '@/routes/Orders/partials/OrderForm';
|
||||
import { getStoreCurrency } from '@/lib/currency';
|
||||
import { showErrorToast, showSuccessToast } from '@/lib/errorHandling';
|
||||
import { __, sprintf } from '@/lib/i18n';
|
||||
import { usePageHeader } from '@/contexts/PageHeaderContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export default function OrdersNew() {
|
||||
const nav = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
const { setPageHeader, clearPageHeader } = usePageHeader();
|
||||
|
||||
// Countries from Woo (allowed + default + states)
|
||||
const countriesQ = useQuery({ queryKey: ['countries'], queryFn: OrdersApi.countries });
|
||||
@@ -37,6 +40,12 @@ export default function OrdersNew() {
|
||||
// Prefer global store currency injected by PHP
|
||||
const { currency: storeCurrency, symbol: storeSymbol } = getStoreCurrency();
|
||||
|
||||
// Set page header
|
||||
useEffect(() => {
|
||||
setPageHeader(__('New Order'));
|
||||
return () => clearPageHeader();
|
||||
}, [setPageHeader, clearPageHeader]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -44,12 +44,12 @@ export function FilterBottomSheet({
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<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}
|
||||
/>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="flex justify-center pt-3 pb-2">
|
||||
<div className="w-12 h-1.5 bg-muted-foreground/30 rounded-full" />
|
||||
@@ -67,7 +67,7 @@ export function FilterBottomSheet({
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-24">
|
||||
{/* Status Filter */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">{__('Status')}</label>
|
||||
|
||||
@@ -29,9 +29,9 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
|
||||
return (
|
||||
<Link
|
||||
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 */}
|
||||
{onSelect && (
|
||||
<div
|
||||
@@ -40,48 +40,49 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
|
||||
e.stopPropagation();
|
||||
onSelect(order.id);
|
||||
}}
|
||||
className="pt-1"
|
||||
className="pt-0.5"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
aria-label={__('Select order')}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center">
|
||||
<Package className="w-5 h-5" />
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
|
||||
<Package className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
{/* Order Number & Status */}
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<h3 className="font-semibold text-base">#{order.number}</h3>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium capitalize ${statusClass}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h3 className="font-bold text-lg leading-tight">#{order.number}</h3>
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold capitalize whitespace-nowrap ${statusClass}`}>
|
||||
{order.status || 'unknown'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Customer */}
|
||||
<div className="text-sm text-muted-foreground mb-2">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{order.customer || __('Guest')}
|
||||
</div>
|
||||
|
||||
{/* 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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
{formatRelativeOrDate(order.date_ts)}
|
||||
</span>
|
||||
<span className="font-semibold text-base tabular-nums">
|
||||
<span className="font-bold text-lg tabular-nums text-primary">
|
||||
{formatMoney(order.total, {
|
||||
currency: order.currency || currencyConfig.currency,
|
||||
symbol: order.currency_symbol || currencyConfig.symbol,
|
||||
@@ -95,7 +96,7 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,6 @@ export function SearchBar({ value, onChange, onFilterClick, filterCount = 0 }: S
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={__('Search orders...')}
|
||||
|
||||
Reference in New Issue
Block a user