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')}
); }