From e0a236fc642778432e030fbef7a4131ebf4f9e3a Mon Sep 17 00:00:00 2001 From: dwindown Date: Sat, 8 Nov 2025 13:16:19 +0700 Subject: [PATCH] feat: Modern mobile-first Orders UI redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented complete mobile-first redesign of Orders page with app-like UX. Problem: - Desktop table layout on mobile (cramped, not touch-friendly) - "New order" button redundant with FAB - Desktop-style filters not mobile-optimized - Checkbox selection too small for touch - Old-school pagination Solution: Full Modern Mobile-First Redesign New Components Created: 1. OrderCard.tsx - Card-based layout for mobile - Touch-friendly tap targets - Order number + status badge - Customer name - Items brief - Date + total amount - Chevron indicator - Checkbox for selection - Tap card → navigate to detail 2. FilterBottomSheet.tsx - Modern bottom sheet UI - Drag handle - Status filter - Date range picker - Sort order - Active filter count badge - Reset + Apply buttons - Smooth slide-in animation 3. SearchBar.tsx - Search input with icon - Filter button with badge - Clean, modern design - Touch-optimized Orders Page Redesign: Mobile Layout: ┌─────────────────────────────────┐ │ [🔍 Search orders...] [⚙] │ ← Search + Filter ├─────────────────────────────────┤ │ ┌─────────────────────────────┐ │ │ │ 📦 #337 💰 │ │ ← Order card │ │ Processing │ │ │ │ Dwindi Ramadhana │ │ │ │ 2 items · Product A, ... │ │ │ │ 2 hours ago Rp64.500 │ │ │ └─────────────────────────────┘ │ │ ┌─────────────────────────────┐ │ │ │ 📦 #336 ✓ │ │ │ │ Completed │ │ │ │ John Doe │ │ │ │ 1 item · Product B │ │ │ │ Yesterday Rp125.000 │ │ │ └─────────────────────────────┘ │ ├─────────────────────────────────┤ │ [Previous] Page 1 [Next] │ ├─────────────────────────────────┤ │ Dashboard Orders Products ... │ └─────────────────────────────────┘ ( + ) ← FAB Desktop Layout: - Keeps table view (familiar for desktop users) - Inline filters at top - All existing functionality preserved Features Implemented: ✅ Card-based mobile layout ✅ Search orders (by number, customer, status) ✅ Bottom sheet filters ✅ Active filter count badge ✅ Pull-to-refresh indicator ✅ Bulk selection with sticky action bar ✅ Touch-optimized tap targets ✅ Smooth animations ✅ Empty states with helpful messages ✅ Responsive: cards on mobile, table on desktop ✅ FAB for new order (removed redundant button) ✅ Clean, modern, app-like UX Mobile-Specific Improvements: 1. No "New order" button at top (use FAB) 2. Search bar replaces desktop filters 3. Filter icon opens bottom sheet 4. Cards instead of cramped table 5. Larger touch targets 6. Sticky bulk action bar 7. Pull-to-refresh support 8. Better empty states Desktop Unchanged: - Table layout preserved - Inline filters - All existing features work Result: ✅ Modern, app-like mobile UI ✅ Touch-friendly interactions ✅ Clean, uncluttered design ✅ Fast, responsive ✅ Desktop functionality preserved ✅ Consistent with mobile-first vision Files Created: - routes/Orders/components/OrderCard.tsx - routes/Orders/components/FilterBottomSheet.tsx - routes/Orders/components/SearchBar.tsx Files Modified: - routes/Orders/index.tsx (complete redesign) The Orders page is now a modern, mobile-first experience! 🎯 --- .../components/nav/DashboardSubmenuBar.tsx | 2 +- admin-spa/src/components/nav/SubmenuBar.tsx | 2 +- admin-spa/src/routes/More/index.tsx | 2 +- .../Orders/components/FilterBottomSheet.tsx | 153 ++++++ .../routes/Orders/components/OrderCard.tsx | 102 ++++ .../routes/Orders/components/SearchBar.tsx | 41 ++ admin-spa/src/routes/Orders/index.old.tsx | 478 +++++++++++++++++ admin-spa/src/routes/Orders/index.tsx | 499 ++++++++++-------- 8 files changed, 1059 insertions(+), 220 deletions(-) create mode 100644 admin-spa/src/routes/Orders/components/FilterBottomSheet.tsx create mode 100644 admin-spa/src/routes/Orders/components/OrderCard.tsx create mode 100644 admin-spa/src/routes/Orders/components/SearchBar.tsx create mode 100644 admin-spa/src/routes/Orders/index.old.tsx diff --git a/admin-spa/src/components/nav/DashboardSubmenuBar.tsx b/admin-spa/src/components/nav/DashboardSubmenuBar.tsx index 13f2cdd..10b4164 100644 --- a/admin-spa/src/components/nav/DashboardSubmenuBar.tsx +++ b/admin-spa/src/components/nav/DashboardSubmenuBar.tsx @@ -38,7 +38,7 @@ export default function DashboardSubmenuBar({ items = [], fullscreen = false, he // Fix: Always use exact match to prevent first submenu from being always active const isActive = !!it.path && pathname === it.path; const cls = [ - 'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap', + 'ui-ctrl inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap', 'focus:outline-none focus:ring-0 focus:shadow-none', isActive ? 'bg-accent text-accent-foreground' : 'hover:bg-accent hover:text-accent-foreground', ].join(' '); diff --git a/admin-spa/src/components/nav/SubmenuBar.tsx b/admin-spa/src/components/nav/SubmenuBar.tsx index b294c1d..174a403 100644 --- a/admin-spa/src/components/nav/SubmenuBar.tsx +++ b/admin-spa/src/components/nav/SubmenuBar.tsx @@ -25,7 +25,7 @@ export default function SubmenuBar({ items = [], fullscreen = false, headerVisib // Fix: Always use exact match to prevent first submenu from being always active const isActive = !!it.path && pathname === it.path; const cls = [ - 'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap', + 'ui-ctrl inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap', 'focus:outline-none focus:ring-0 focus:shadow-none', isActive ? 'bg-accent text-accent-foreground' : 'hover:bg-accent hover:text-accent-foreground', ].join(' '); diff --git a/admin-spa/src/routes/More/index.tsx b/admin-spa/src/routes/More/index.tsx index f071dde..79197b1 100644 --- a/admin-spa/src/routes/More/index.tsx +++ b/admin-spa/src/routes/More/index.tsx @@ -50,7 +50,7 @@ export default function MorePage() { }; return ( -
+
{/* Remove inline header - use PageHeader component instead */}

diff --git a/admin-spa/src/routes/Orders/components/FilterBottomSheet.tsx b/admin-spa/src/routes/Orders/components/FilterBottomSheet.tsx new file mode 100644 index 0000000..711431d --- /dev/null +++ b/admin-spa/src/routes/Orders/components/FilterBottomSheet.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { X } from 'lucide-react'; +import { __ } from '@/lib/i18n'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import DateRange from '@/components/filters/DateRange'; +import OrderBy from '@/components/filters/OrderBy'; + +interface FilterBottomSheetProps { + open: boolean; + onClose: () => void; + filters: { + status?: string; + dateStart?: string; + dateEnd?: string; + orderby: 'date' | 'id' | 'modified' | 'total'; + order: 'asc' | 'desc'; + }; + onFiltersChange: (filters: any) => void; + onReset: () => void; + activeFiltersCount: number; +} + +export function FilterBottomSheet({ + open, + onClose, + filters, + onFiltersChange, + onReset, + activeFiltersCount, +}: FilterBottomSheetProps) { + if (!open) return null; + + const hasActiveFilters = activeFiltersCount > 0; + + return ( + <> + {/* Backdrop */} +

+ + {/* Bottom Sheet */} +
+ {/* Drag Handle */} +
+
+
+ + {/* Header */} +
+

{__('Filters')}

+ +
+ + {/* Content */} +
+ {/* Status Filter */} +
+ + +
+ + {/* Date Range Filter */} +
+ + { + onFiltersChange({ + ...filters, + dateStart: v.date_start, + dateEnd: v.date_end, + }); + }} + /> +
+ + {/* Sort Order Filter */} +
+ + { + onFiltersChange({ + ...filters, + orderby: (v.orderby ?? 'date') as 'date' | 'id' | 'modified' | 'total', + order: (v.order ?? 'desc') as 'asc' | 'desc', + }); + }} + /> +
+
+ + {/* Footer */} +
+ {hasActiveFilters && ( + + )} + +
+
+ + ); +} diff --git a/admin-spa/src/routes/Orders/components/OrderCard.tsx b/admin-spa/src/routes/Orders/components/OrderCard.tsx new file mode 100644 index 0000000..7dce108 --- /dev/null +++ b/admin-spa/src/routes/Orders/components/OrderCard.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { ChevronRight, Package } from 'lucide-react'; +import { __ } from '@/lib/i18n'; +import { formatMoney } from '@/lib/currency'; +import { formatRelativeOrDate } from '@/lib/dates'; +import { Checkbox } from '@/components/ui/checkbox'; + +const statusStyle: Record = { + pending: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300', + processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300', + completed: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300', + 'on-hold': 'bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-300', + cancelled: 'bg-zinc-200 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-300', + refunded: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300', + failed: 'bg-rose-100 text-rose-800 dark:bg-rose-900/30 dark:text-rose-300', +}; + +interface OrderCardProps { + order: any; + selected?: boolean; + onSelect?: (id: number) => void; + currencyConfig: any; +} + +export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCardProps) { + const statusClass = statusStyle[order.status?.toLowerCase()] || 'bg-slate-100 text-slate-800'; + + return ( + +
+ {/* Checkbox */} + {onSelect && ( +
{ + e.preventDefault(); + e.stopPropagation(); + onSelect(order.id); + }} + className="pt-1" + > + +
+ )} + + {/* Icon */} +
+ +
+ + {/* Content */} +
+ {/* Order Number & Status */} +
+

#{order.number}

+ + {order.status || 'unknown'} + +
+ + {/* Customer */} +
+ {order.customer || __('Guest')} +
+ + {/* Items Brief */} + {order.items_brief && ( +
+ {order.items_count} {order.items_count === 1 ? __('item') : __('items')} · {order.items_brief} +
+ )} + + {/* Date & Total */} +
+ + {formatRelativeOrDate(order.date_ts)} + + + {formatMoney(order.total, { + currency: order.currency || currencyConfig.currency, + symbol: order.currency_symbol || currencyConfig.symbol, + thousandSep: currencyConfig.thousand_sep, + decimalSep: currencyConfig.decimal_sep, + position: currencyConfig.position, + decimals: currencyConfig.decimals, + })} + +
+
+ + {/* Chevron */} + +
+ + ); +} diff --git a/admin-spa/src/routes/Orders/components/SearchBar.tsx b/admin-spa/src/routes/Orders/components/SearchBar.tsx new file mode 100644 index 0000000..201bbda --- /dev/null +++ b/admin-spa/src/routes/Orders/components/SearchBar.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Search, SlidersHorizontal } from 'lucide-react'; +import { __ } from '@/lib/i18n'; + +interface SearchBarProps { + value: string; + onChange: (value: string) => void; + onFilterClick: () => void; + filterCount?: number; +} + +export function SearchBar({ value, onChange, onFilterClick, filterCount = 0 }: SearchBarProps) { + return ( +
+ {/* Search Input */} +
+ + onChange(e.target.value)} + placeholder={__('Search orders...')} + className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+ + {/* Filter Button */} + +
+ ); +} diff --git a/admin-spa/src/routes/Orders/index.old.tsx b/admin-spa/src/routes/Orders/index.old.tsx new file mode 100644 index 0000000..085f412 --- /dev/null +++ b/admin-spa/src/routes/Orders/index.old.tsx @@ -0,0 +1,478 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, keepPreviousData } from '@tanstack/react-query'; +import { api } from '@/lib/api'; +import { Filter, PackageOpen, Trash2 } from 'lucide-react'; +import { ErrorCard } from '@/components/ErrorCard'; +import { getPageLoadErrorMessage } from '@/lib/errorHandling'; +import { __ } from '@/lib/i18n'; +import { useFABConfig } from '@/hooks/useFABConfig'; +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { toast } from 'sonner'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { formatRelativeOrDate } from "@/lib/dates"; +import { Link, useNavigate } from 'react-router-dom'; + +function ItemsCell({ row }: { row: any }) { + const count: number = typeof row.items_count === 'number' ? row.items_count : 0; + const brief: string = row.items_brief || ''; + const linesTotal: number | undefined = typeof row.lines_total === 'number' ? row.lines_total : undefined; + const linesPreview: number | undefined = typeof row.lines_preview === 'number' ? row.lines_preview : undefined; + const extra = linesTotal && linesPreview ? Math.max(0, linesTotal - linesPreview) : 0; + + const label = `${count || '—'} item${count === 1 ? '' : 's'}`; + const inline = brief + (extra > 0 ? ` +${extra} more` : ''); + + return ( +
+ + + + {label} + {inline ? <> · {inline} : null} + + + +
{label}
+
+ {row.items_full || brief || 'No items'} +
+
+
+
+ ); +} +import { Skeleton } from '@/components/ui/skeleton'; +import { formatMoney, getStoreCurrency } from '@/lib/currency'; +import DateRange from '@/components/filters/DateRange'; +import OrderBy from '@/components/filters/OrderBy'; +import { setQuery, getQuery } from '@/lib/query-params'; + +const statusStyle: Record = { + pending: 'bg-amber-100 text-amber-800', + processing: 'bg-blue-100 text-blue-800', + completed: 'bg-emerald-100 text-emerald-800', + 'on-hold': 'bg-slate-200 text-slate-800', + cancelled: 'bg-zinc-200 text-zinc-800', + refunded: 'bg-purple-100 text-purple-800', + failed: 'bg-rose-100 text-rose-800', +}; + +function StatusBadge({ value }: { value?: string }) { + const v = (value || '').toLowerCase(); + const cls = statusStyle[v] || 'bg-slate-100 text-slate-800'; + return ( + {v || 'unknown'} + ); +} + +export default function Orders() { + useFABConfig('orders'); // Add FAB for creating orders + const initial = getQuery(); + const [page, setPage] = useState(Number(initial.page ?? 1) || 1); + const [status, setStatus] = useState(initial.status || undefined); + const [dateStart, setDateStart] = useState(initial.date_start || undefined); + const [dateEnd, setDateEnd] = useState(initial.date_end || undefined); + const [orderby, setOrderby] = useState<'date'|'id'|'modified'|'total'>((initial.orderby as any) || 'date'); + const [order, setOrder] = useState<'asc'|'desc'>((initial.order as any) || 'desc'); + const [selectedIds, setSelectedIds] = useState([]); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const perPage = 20; + + React.useEffect(() => { + setQuery({ page, status, date_start: dateStart, date_end: dateEnd, orderby, order }); + }, [page, status, dateStart, dateEnd, orderby, order]); + + const q = useQuery({ + queryKey: ['orders', { page, perPage, status, dateStart, dateEnd, orderby, order }], + queryFn: () => api.get('/orders', { + page, per_page: perPage, + status, + date_start: dateStart, + date_end: dateEnd, + orderby, + order, + }), + placeholderData: keepPreviousData, + }); + + const data = q.data as undefined | { rows: any[]; total: number; page: number; per_page: number }; + const nav = useNavigate(); + const store = getStoreCurrency(); + + // Bulk delete mutation + const deleteMutation = useMutation({ + mutationFn: async (ids: number[]) => { + const results = await Promise.allSettled( + ids.map(id => api.del(`/orders/${id}`)) + ); + const failed = results.filter(r => r.status === 'rejected').length; + return { total: ids.length, failed }; + }, + onSuccess: (result) => { + const { total, failed } = result; + if (failed === 0) { + toast.success(__('Orders deleted successfully')); + } else if (failed < total) { + toast.warning(__(`${total - failed} orders deleted, ${failed} failed`)); + } else { + toast.error(__('Failed to delete orders')); + } + setSelectedIds([]); + setShowDeleteDialog(false); + q.refetch(); + }, + onError: () => { + toast.error(__('Failed to delete orders')); + setShowDeleteDialog(false); + }, + }); + + // Checkbox handlers + const allIds = data?.rows?.map(r => r.id) || []; + const allSelected = allIds.length > 0 && selectedIds.length === allIds.length; + const someSelected = selectedIds.length > 0 && selectedIds.length < allIds.length; + + const toggleAll = () => { + if (allSelected) { + setSelectedIds([]); + } else { + setSelectedIds(allIds); + } + }; + + const toggleRow = (id: number) => { + setSelectedIds(prev => + prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id] + ); + }; + + const handleDeleteClick = () => { + if (selectedIds.length > 0) { + setShowDeleteDialog(true); + } + }; + + const confirmDelete = () => { + deleteMutation.mutate(selectedIds); + }; + + return ( +
+
+
+ + {selectedIds.length > 0 && ( + + )} + {/* Mobile: condensed Filters button with HoverCard */} +
+ + + + + +
+ +
+ + { setPage(1); setDateStart(v.date_start); setDateEnd(v.date_end); }} + /> + + { + setPage(1); + setOrderby((v.orderby ?? 'date') as 'date' | 'id' | 'modified' | 'total'); + setOrder((v.order ?? 'desc') as 'asc' | 'desc'); + }} + /> + +
+ {(status || dateStart || dateEnd || orderby !== 'date' || order !== 'desc') ? ( + + ) : } + {q.isFetching && {__('Loading…')}} +
+
+
+
+
+ + + {/* Desktop: full inline filters */} +
+
+ + + + { setPage(1); setDateStart(v.date_start); setDateEnd(v.date_end); }} + /> + + { + setPage(1); + setOrderby((v.orderby ?? 'date') as 'date' | 'id' | 'modified' | 'total'); + setOrder((v.order ?? 'desc') as 'asc' | 'desc'); + }} + /> + +
+ {status && ( + + )} + {q.isFetching && {__('Loading…')}} +
+
+ +
+ {q.isLoading && ( +
+ {Array.from({ length: 10 }).map((_, i) => ( + + ))} +
+ )} + + {q.isError && ( + q.refetch()} + /> + )} + + {!q.isLoading && !q.isError && ( + + + + + + + + + + + + + + + {data?.rows?.map((row) => ( + + + + + + + + + + + ))} + + {(!data || data.rows.length === 0) && ( + + + + )} + +
+ + {__('Order')}{__('Date')}{__('Customer')}{__('Items')}{__('Status')}{__('Total')}{__('Actions')}
+ toggleRow(row.id)} + aria-label={__('Select order')} + /> + + #{row.number} + + + {formatRelativeOrDate(row.date_ts)} + + {row.customer || '—'} + + + {formatMoney(row.total, { + currency: row.currency || store.currency, + symbol: row.currency_symbol || store.symbol, + thousandSep: store.thousand_sep, + decimalSep: store.decimal_sep, + position: store.position, + decimals: store.decimals, + })} + + {__('Open')} + {__('Edit')} +
+
+ +
{__('No orders found')}
+ {status ? ( +

{__('Try adjusting filters.')}

+ ) : ( +

{__('Once you receive orders, they\'ll show up here.')}

+ )} +
+
+ )} +
+ +
+ +
{__('Page')} {page}
+ +
+ + {/* Delete Confirmation Dialog */} + + + + {__('Delete Orders')} + + {__('Are you sure you want to delete')} {selectedIds.length} {selectedIds.length === 1 ? __('order') : __('orders')}? +
+ {__('This action cannot be undone.')} +
+
+ + setShowDeleteDialog(false)} + disabled={deleteMutation.isPending} + > + {__('Cancel')} + + + {deleteMutation.isPending ? __('Deleting...') : __('Delete')} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/admin-spa/src/routes/Orders/index.tsx b/admin-spa/src/routes/Orders/index.tsx index 085f412..e845217 100644 --- a/admin-spa/src/routes/Orders/index.tsx +++ b/admin-spa/src/routes/Orders/index.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { useQuery, useMutation, keepPreviousData } from '@tanstack/react-query'; import { api } from '@/lib/api'; -import { Filter, PackageOpen, Trash2 } from 'lucide-react'; +import { Filter, PackageOpen, Trash2, RefreshCw } from 'lucide-react'; import { ErrorCard } from '@/components/ErrorCard'; import { getPageLoadErrorMessage } from '@/lib/errorHandling'; import { __ } from '@/lib/i18n'; @@ -25,12 +25,19 @@ import { SelectContent, SelectGroup, SelectItem, - SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { formatRelativeOrDate } from "@/lib/dates"; import { Link, useNavigate } from 'react-router-dom'; +import { Skeleton } from '@/components/ui/skeleton'; +import { formatMoney, getStoreCurrency } from '@/lib/currency'; +import DateRange from '@/components/filters/DateRange'; +import OrderBy from '@/components/filters/OrderBy'; +import { setQuery, getQuery } from '@/lib/query-params'; +import { OrderCard } from './components/OrderCard'; +import { FilterBottomSheet } from './components/FilterBottomSheet'; +import { SearchBar } from './components/SearchBar'; function ItemsCell({ row }: { row: any }) { const count: number = typeof row.items_count === 'number' ? row.items_count : 0; @@ -61,11 +68,6 @@ function ItemsCell({ row }: { row: any }) {
); } -import { Skeleton } from '@/components/ui/skeleton'; -import { formatMoney, getStoreCurrency } from '@/lib/currency'; -import DateRange from '@/components/filters/DateRange'; -import OrderBy from '@/components/filters/OrderBy'; -import { setQuery, getQuery } from '@/lib/query-params'; const statusStyle: Record = { pending: 'bg-amber-100 text-amber-800', @@ -96,6 +98,9 @@ export default function Orders() { const [order, setOrder] = useState<'asc'|'desc'>((initial.order as any) || 'desc'); const [selectedIds, setSelectedIds] = useState([]); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [filterSheetOpen, setFilterSheetOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [isRefreshing, setIsRefreshing] = useState(false); const perPage = 20; React.useEffect(() => { @@ -119,6 +124,35 @@ export default function Orders() { const nav = useNavigate(); const store = getStoreCurrency(); + // Pull to refresh + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + await q.refetch(); + setTimeout(() => setIsRefreshing(false), 500); + }, [q]); + + // Filter orders by search query + const filteredOrders = React.useMemo(() => { + if (!data?.rows) return []; + if (!searchQuery.trim()) return data.rows; + + const query = searchQuery.toLowerCase(); + return data.rows.filter(order => + order.number?.toString().includes(query) || + order.customer?.toLowerCase().includes(query) || + order.status?.toLowerCase().includes(query) + ); + }, [data?.rows, searchQuery]); + + // Count active filters + const activeFiltersCount = React.useMemo(() => { + let count = 0; + if (status) count++; + if (dateStart || dateEnd) count++; + if (orderby !== 'date' || order !== 'desc') count++; + return count; + }, [status, dateStart, dateEnd, orderby, order]); + // Bulk delete mutation const deleteMutation = useMutation({ mutationFn: async (ids: number[]) => { @@ -148,7 +182,7 @@ export default function Orders() { }); // Checkbox handlers - const allIds = data?.rows?.map(r => r.id) || []; + const allIds = filteredOrders.map(r => r.id) || []; const allSelected = allIds.length > 0 && selectedIds.length === allIds.length; const someSelected = selectedIds.length > 0 && selectedIds.length < allIds.length; @@ -176,100 +210,59 @@ export default function Orders() { deleteMutation.mutate(selectedIds); }; + const handleFiltersChange = (newFilters: any) => { + setPage(1); + setStatus(newFilters.status); + setDateStart(newFilters.dateStart); + setDateEnd(newFilters.dateEnd); + setOrderby(newFilters.orderby); + setOrder(newFilters.order); + }; + + const handleResetFilters = () => { + setStatus(undefined); + setDateStart(undefined); + setDateEnd(undefined); + setOrderby('date'); + setOrder('desc'); + setPage(1); + }; + return ( -
-
-
- - {selectedIds.length > 0 && ( - - )} - {/* Mobile: condensed Filters button with HoverCard */} -
- - - - - -
- -
- - { setPage(1); setDateStart(v.date_start); setDateEnd(v.date_end); }} - /> - - { - setPage(1); - setOrderby((v.orderby ?? 'date') as 'date' | 'id' | 'modified' | 'total'); - setOrder((v.order ?? 'desc') as 'asc' | 'desc'); - }} - /> - -
- {(status || dateStart || dateEnd || orderby !== 'date' || order !== 'desc') ? ( - - ) : } - {q.isFetching && {__('Loading…')}} -
-
-
+ {selectedIds.length > 0 && ( + + )}
-
- - {/* Desktop: full inline filters */} -
-
+