diff --git a/frontend/app/(dashboard)/shipments/page.tsx b/frontend/app/(dashboard)/shipments/page.tsx index 3e937bfa..d1a55081 100644 --- a/frontend/app/(dashboard)/shipments/page.tsx +++ b/frontend/app/(dashboard)/shipments/page.tsx @@ -9,7 +9,7 @@ import { ShipmentCard } from '../../../components/shipment/shipment-card'; import { ShipmentCardSkeleton } from '../../../components/ui/skeleton'; import { Button } from '../../../components/ui/button'; import { toast } from 'sonner'; -import { apiClient } from '../../../lib/api/client'; +import { ExportButton, type ExportFilters } from '../../../package/components/ExportButton'; const STATUS_TABS: { label: string; value: ShipmentStatus | 'all' }[] = [ { label: 'All', value: 'all' }, @@ -25,25 +25,13 @@ export default function ShipmentsPage() { const [result, setResult] = useState(null); const [activeTab, setActiveTab] = useState('all'); const [loading, setLoading] = useState(true); - const [exporting, setExporting] = useState(false); - const exportCsv = async () => { - setExporting(true); - try { - const blob = await apiClient('/shipments/export?format=csv', { - headers: { Accept: 'text/csv' }, - }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'shipments.csv'; - a.click(); - URL.revokeObjectURL(url); - } catch { - toast.error('Failed to export CSV. Please try again.'); - } finally { - setExporting(false); + const getActiveFilters = (): ExportFilters => { + const filters: ExportFilters = {}; + if (activeTab !== 'all') { + filters.status = activeTab; } + return filters; }; useEffect(() => { @@ -64,25 +52,11 @@ export default function ShipmentsPage() {

{pageTitle}

- + /> {isShipper && ( + ); + })} +
+
+ ); + }; + + const monthsToShow = months; + + return ( +
+ {/* Trigger Button */} + + )} + + + {/* Hidden input for form integration */} + {name && ( + <> + + + + )} + + {/* Dropdown */} + {open && ( +
+ {/* Preset Ranges */} +
+ {PRESET_RANGES.map((preset) => ( + + ))} +
+ + {/* Calendar Navigation */} +
+ + + {format(displayMonth, 'MMMM yyyy')} + {monthsToShow === 2 && ( + - {format(addMonths(displayMonth, 1), 'MMMM yyyy')} + )} + + +
+ + {/* Calendar Grid */} +
+ {renderCalendar(0)} + {monthsToShow === 2 && renderCalendar(1)} +
+ + {/* Selection hint */} +
+ {selectionPhase === 'start' + ? 'Select start date' + : 'Select end date'} +
+ + {/* Action Buttons */} +
+ + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/package/components/DateRangePicker/index.ts b/frontend/package/components/DateRangePicker/index.ts new file mode 100644 index 00000000..aca99138 --- /dev/null +++ b/frontend/package/components/DateRangePicker/index.ts @@ -0,0 +1,2 @@ +export { DateRangePicker } from './DateRangePicker'; +export type { DateRangePickerProps, DateRange } from './DateRangePicker'; \ No newline at end of file diff --git a/frontend/package/components/ExportButton/ExportButton.tsx b/frontend/package/components/ExportButton/ExportButton.tsx new file mode 100644 index 00000000..c85c63c5 --- /dev/null +++ b/frontend/package/components/ExportButton/ExportButton.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { useState } from 'react'; +import { toast } from 'sonner'; +import { apiClient } from '../../../lib/api/client'; +import { ShipmentStatus } from '../../../types/shipment.types'; +import { Button } from '@/components/ui/button'; + +export interface ExportFilters { + /** Status filter for shipments */ + status?: ShipmentStatus | 'all'; + /** Search query for origin/destination */ + search?: string; + /** Origin filter */ + origin?: string; + /** Destination filter */ + destination?: string; + /** Start date for date range filter (ISO string) */ + startDate?: string; + /** End date for date range filter (ISO string) */ + endDate?: string; +} + +export interface ExportButtonProps { + /** Current active filters to include in the export */ + filters?: ExportFilters; + /** Custom class name for styling */ + className?: string; + /** Disabled state */ + disabled?: boolean; + /** Custom label for the button */ + label?: string; + /** Variant of the button */ + variant?: 'default' | 'outline' | 'ghost'; + /** Size of the button */ + size?: 'sm' | 'md' | 'lg'; + /** Callback when export starts */ + onExportStart?: () => void; + /** Callback when export succeeds */ + onExportSuccess?: () => void; + /** Callback when export fails */ + onExportError?: (error: Error) => void; +} + +/** + * ExportButton component for exporting shipment data as CSV. + * Triggers a file download using the current active filter state. + */ +export function ExportButton({ + filters, + className = '', + disabled = false, + label = 'Export CSV', + variant = 'outline', + size = 'sm', + onExportStart, + onExportSuccess, + onExportError, +}: ExportButtonProps) { + const [exporting, setExporting] = useState(false); + + const buildQueryString = (): string => { + const params = new URLSearchParams(); + params.set('format', 'csv'); + + if (filters) { + if (filters.status && filters.status !== 'all') { + params.set('status', filters.status); + } + if (filters.search) { + params.set('search', filters.search); + } + if (filters.origin) { + params.set('origin', filters.origin); + } + if (filters.destination) { + params.set('destination', filters.destination); + } + if (filters.startDate) { + params.set('startDate', filters.startDate); + } + if (filters.endDate) { + params.set('endDate', filters.endDate); + } + } + + return params.toString(); + }; + + const handleClick = async () => { + if (exporting || disabled) return; + + setExporting(true); + onExportStart?.(); + + try { + const queryString = buildQueryString(); + const blob = await apiClient(`/shipments/export?${queryString}`, { + headers: { Accept: 'text/csv' }, + }); + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `shipments-export-${new Date().toISOString().split('T')[0]}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + onExportSuccess?.(); + toast.success('Shipments exported successfully'); + } catch (error) { + onExportError?.(error as Error); + toast.error('Failed to export CSV. Please try again.'); + } finally { + setExporting(false); + } + }; + + // Button base styles + const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'; + + // Variant styles + const variantStyles = { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + ghost: 'hover:bg-accent hover:text-accent-foreground', + }; + + // Size styles + const sizeStyles = { + sm: 'h-8 px-3 text-xs', + md: 'h-10 px-4 text-sm', + lg: 'h-12 px-6 text-base', + }; + + const buttonClasses = [ + baseStyles, + variantStyles[variant], + sizeStyles[size], + className, + ].filter(Boolean).join(' '); + + return ( + + ); +} \ No newline at end of file diff --git a/frontend/package/components/ExportButton/index.ts b/frontend/package/components/ExportButton/index.ts new file mode 100644 index 00000000..09c1fedb --- /dev/null +++ b/frontend/package/components/ExportButton/index.ts @@ -0,0 +1,2 @@ +export { ExportButton } from './ExportButton'; +export type { ExportButtonProps, ExportFilters } from './ExportButton'; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4da8d25d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "FrieghtFlow", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}