@burdenoff/microfe-movethewheels 2026.510.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/dist/AIAssistantPage-hD0VYJdH.js +210 -0
- package/dist/AnalyticsPage-DHTHCUtr.js +201 -0
- package/dist/CreateOrderPage-Cprg4Y9V.js +471 -0
- package/dist/CustomerDetailsPage-DNDEw7IW.js +239 -0
- package/dist/CustomersPage-CDjjeCEL.js +119 -0
- package/dist/DashboardPage-8iTPXRAG.js +374 -0
- package/dist/DataTable-CRIKfdIN.js +239 -0
- package/dist/DriverDetailsPage-CRyRCno7.js +297 -0
- package/dist/DriversPage-16O8fVmf.js +127 -0
- package/dist/FinancePage-BYUxK5dR.js +154 -0
- package/dist/FleetPage-CHYETCWT.js +293 -0
- package/dist/ImportExportPage-C3MKKxfc.js +232 -0
- package/dist/InventoryPage--822AxZM.js +223 -0
- package/dist/LiveTrackingPage-Dp3rTJDr.js +332 -0
- package/dist/MarketplacePage-DjEqudfM.js +192 -0
- package/dist/MetricCard-GTbxAk1a.js +135 -0
- package/dist/OrderDetailsPage-BIuYG0ub.js +398 -0
- package/dist/OrdersListPage-CW5V0Uvh.js +257 -0
- package/dist/PageLayout-B7b0vl0R.js +1894 -0
- package/dist/ProductDetailsPage-Q3X7AT-7.js +168 -0
- package/dist/ProductsPage-CUj9JpnW.js +131 -0
- package/dist/ReportsPage-DblO5CdJ.js +227 -0
- package/dist/RouteDetailsPage-CLctgk6A.js +240 -0
- package/dist/RoutesPage-8hrv6RWT.js +116 -0
- package/dist/SettingsPage-BJ5BQeqn.js +247 -0
- package/dist/StatusBadge-BrrwraIA.js +206 -0
- package/dist/TrackingPage-BGqHDh-w.js +322 -0
- package/dist/VehicleDetailsPage-XnDH4iQR.js +194 -0
- package/dist/VehiclesPage-Cs4XxHkA.js +127 -0
- package/dist/WarehouseDetailsPage-GemdMvr_.js +215 -0
- package/dist/WarehousesPage-QTiuDuXy.js +121 -0
- package/dist/arrow-left-6CiLhqVp.js +11 -0
- package/dist/box-BunB_4UH.js +18 -0
- package/dist/chart-column-DWwVEVQ-.js +22 -0
- package/dist/chevron-right-DhZVf20o.js +8 -0
- package/dist/circle-alert-D5f6RZxt.js +26 -0
- package/dist/circle-check-big-D-JMHcTe.js +11 -0
- package/dist/clock-CvwBKbQP.js +13 -0
- package/dist/dev/main.d.ts +1 -0
- package/dist/dollar-sign-CP9qeU5d.js +14 -0
- package/dist/download-CIuG04pJ.js +21 -0
- package/dist/file-text-Dd_thxkn.js +26 -0
- package/dist/filter-DyRMX9CU.js +8 -0
- package/dist/formatters-_vJlC-47.js +50 -0
- package/dist/generated/global-operations.d.ts +1 -0
- package/dist/generated/global-types.d.ts +20715 -0
- package/dist/generated/wspace-operations.d.ts +3704 -0
- package/dist/generated/wspace-types.d.ts +53362 -0
- package/dist/graphqlClient-CdJyR_ed.js +55 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +772 -0
- package/dist/map-BqH1cBJi.js +18 -0
- package/dist/map-pin-CFBOmh-A.js +13 -0
- package/dist/movethewheels/MoveTheWheelsRoot.d.ts +25 -0
- package/dist/movethewheels/MoveTheWheelsRoutes.d.ts +7 -0
- package/dist/movethewheels/components/DataTable.d.ts +32 -0
- package/dist/movethewheels/components/MetricCard.d.ts +43 -0
- package/dist/movethewheels/components/PageLayout.d.ts +68 -0
- package/dist/movethewheels/components/StatusBadge.d.ts +49 -0
- package/dist/movethewheels/components/index.d.ts +10 -0
- package/dist/movethewheels/components/ui.d.ts +22 -0
- package/dist/movethewheels/constants/index.d.ts +24 -0
- package/dist/movethewheels/constants/mockData.d.ts +33 -0
- package/dist/movethewheels/hooks/index.d.ts +12 -0
- package/dist/movethewheels/hooks/useAnalytics.d.ts +118 -0
- package/dist/movethewheels/hooks/useCustomers.d.ts +37 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +71 -0
- package/dist/movethewheels/hooks/useInventory.d.ts +60 -0
- package/dist/movethewheels/hooks/useOrders.d.ts +47 -0
- package/dist/movethewheels/hooks/useRoutes.d.ts +41 -0
- package/dist/movethewheels/hooks/useTracking.d.ts +69 -0
- package/dist/movethewheels/index.d.ts +30 -0
- package/dist/movethewheels/pages/AIAssistantPage.d.ts +4 -0
- package/dist/movethewheels/pages/AnalyticsPage.d.ts +4 -0
- package/dist/movethewheels/pages/CreateOrderPage.d.ts +6 -0
- package/dist/movethewheels/pages/CustomerDetailsPage.d.ts +4 -0
- package/dist/movethewheels/pages/CustomersPage.d.ts +4 -0
- package/dist/movethewheels/pages/DashboardPage.d.ts +6 -0
- package/dist/movethewheels/pages/DriverDetailsPage.d.ts +4 -0
- package/dist/movethewheels/pages/DriversPage.d.ts +4 -0
- package/dist/movethewheels/pages/FinancePage.d.ts +4 -0
- package/dist/movethewheels/pages/FleetPage.d.ts +6 -0
- package/dist/movethewheels/pages/ImportExportPage.d.ts +4 -0
- package/dist/movethewheels/pages/InventoryPage.d.ts +4 -0
- package/dist/movethewheels/pages/LiveTrackingPage.d.ts +6 -0
- package/dist/movethewheels/pages/MarketplacePage.d.ts +4 -0
- package/dist/movethewheels/pages/OrderDetailsPage.d.ts +6 -0
- package/dist/movethewheels/pages/OrdersListPage.d.ts +6 -0
- package/dist/movethewheels/pages/ProductDetailsPage.d.ts +4 -0
- package/dist/movethewheels/pages/ProductsPage.d.ts +4 -0
- package/dist/movethewheels/pages/ReportsPage.d.ts +4 -0
- package/dist/movethewheels/pages/RouteDetailsPage.d.ts +4 -0
- package/dist/movethewheels/pages/RoutesPage.d.ts +4 -0
- package/dist/movethewheels/pages/SettingsPage.d.ts +4 -0
- package/dist/movethewheels/pages/TrackingPage.d.ts +6 -0
- package/dist/movethewheels/pages/VehicleDetailsPage.d.ts +4 -0
- package/dist/movethewheels/pages/VehiclesPage.d.ts +4 -0
- package/dist/movethewheels/pages/WarehouseDetailsPage.d.ts +4 -0
- package/dist/movethewheels/pages/WarehousesPage.d.ts +4 -0
- package/dist/movethewheels/providers/MoveTheWheelsProvider.d.ts +16 -0
- package/dist/movethewheels/store/movethewheelsStore.d.ts +73 -0
- package/dist/movethewheels/types/index.d.ts +655 -0
- package/dist/movethewheels/utils/cn.d.ts +6 -0
- package/dist/movethewheels/utils/formatters.d.ts +60 -0
- package/dist/movethewheels/utils/graphqlClient.d.ts +11 -0
- package/dist/movethewheels/utils/index.d.ts +7 -0
- package/dist/movethewheels/utils/navigation.d.ts +23 -0
- package/dist/navigation-BgnOfsVd.js +6 -0
- package/dist/navigation-C2fY_aS9.js +8 -0
- package/dist/package-DVZbDRcV.js +22 -0
- package/dist/phone-KdwpVmC4.js +18 -0
- package/dist/plus-Bl7uX6Ji.js +11 -0
- package/dist/refresh-cw-BYjl3K-8.js +22 -0
- package/dist/route-Ce_poKFi.js +51 -0
- package/dist/save-C-qDVat-.js +18 -0
- package/dist/search-5pdn5eOO.js +13 -0
- package/dist/settings-C4kIDsYg.js +28 -0
- package/dist/square-pen-BwQ67vLE.js +11 -0
- package/dist/star-BlVsC3Ad.js +8 -0
- package/dist/store-DTmQT5M0.js +26 -0
- package/dist/trending-up-C1faflCI.js +11 -0
- package/dist/triangle-alert-CUoVAA4L.js +18 -0
- package/dist/truck-BmDAzu05.js +30 -0
- package/dist/useAnalytics-ph7eTIK6.js +297 -0
- package/dist/useCustomers-bS3a4ytk.js +186 -0
- package/dist/useFleet-BdETplNE.js +398 -0
- package/dist/useInventory-Dwn18FPz.js +323 -0
- package/dist/useOrders-D_3_hGMp.js +324 -0
- package/dist/useRoutes-v4aBaS-E.js +224 -0
- package/dist/useTracking-De2KIUNu.js +261 -0
- package/dist/user-BplzDrLP.js +13 -0
- package/dist/users-i-igmsP4.js +24 -0
- package/dist/warehouse-DewG0PXh.js +25 -0
- package/dist/wrench-CoSDEIC7.js +31 -0
- package/package.json +107 -0
- package/src/dev/main.tsx +110 -0
- package/src/dev/styles.css +139 -0
- package/src/generated/global-operations.ts +2 -0
- package/src/generated/global-types.ts +24048 -0
- package/src/generated/wspace-operations.ts +3734 -0
- package/src/generated/wspace-types.ts +60715 -0
- package/src/index.ts +4 -0
- package/src/movethewheels/MoveTheWheelsRoot.tsx +258 -0
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +119 -0
- package/src/movethewheels/components/DataTable.tsx +367 -0
- package/src/movethewheels/components/MetricCard.tsx +180 -0
- package/src/movethewheels/components/PageLayout.tsx +234 -0
- package/src/movethewheels/components/StatusBadge.tsx +243 -0
- package/src/movethewheels/components/index.ts +26 -0
- package/src/movethewheels/components/ui.tsx +124 -0
- package/src/movethewheels/constants/index.ts +65 -0
- package/src/movethewheels/constants/mockData.ts +1342 -0
- package/src/movethewheels/hooks/index.ts +55 -0
- package/src/movethewheels/hooks/useAnalytics.ts +476 -0
- package/src/movethewheels/hooks/useCustomers.ts +359 -0
- package/src/movethewheels/hooks/useFleet.ts +778 -0
- package/src/movethewheels/hooks/useInventory.ts +632 -0
- package/src/movethewheels/hooks/useOrders.ts +703 -0
- package/src/movethewheels/hooks/useRoutes.ts +453 -0
- package/src/movethewheels/hooks/useTracking.ts +505 -0
- package/src/movethewheels/index.ts +68 -0
- package/src/movethewheels/pages/AIAssistantPage.tsx +160 -0
- package/src/movethewheels/pages/AnalyticsPage.tsx +190 -0
- package/src/movethewheels/pages/CreateOrderPage.tsx +454 -0
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +207 -0
- package/src/movethewheels/pages/CustomersPage.tsx +115 -0
- package/src/movethewheels/pages/DashboardPage.tsx +414 -0
- package/src/movethewheels/pages/DriverDetailsPage.tsx +261 -0
- package/src/movethewheels/pages/DriversPage.tsx +118 -0
- package/src/movethewheels/pages/FinancePage.tsx +141 -0
- package/src/movethewheels/pages/FleetPage.tsx +289 -0
- package/src/movethewheels/pages/ImportExportPage.tsx +165 -0
- package/src/movethewheels/pages/InventoryPage.tsx +212 -0
- package/src/movethewheels/pages/LiveTrackingPage.tsx +325 -0
- package/src/movethewheels/pages/MarketplacePage.tsx +235 -0
- package/src/movethewheels/pages/OrderDetailsPage.tsx +387 -0
- package/src/movethewheels/pages/OrdersListPage.tsx +241 -0
- package/src/movethewheels/pages/ProductDetailsPage.tsx +155 -0
- package/src/movethewheels/pages/ProductsPage.tsx +124 -0
- package/src/movethewheels/pages/ReportsPage.tsx +164 -0
- package/src/movethewheels/pages/RouteDetailsPage.tsx +245 -0
- package/src/movethewheels/pages/RoutesPage.tsx +104 -0
- package/src/movethewheels/pages/SettingsPage.tsx +242 -0
- package/src/movethewheels/pages/TrackingPage.tsx +419 -0
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +218 -0
- package/src/movethewheels/pages/VehiclesPage.tsx +124 -0
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +216 -0
- package/src/movethewheels/pages/WarehousesPage.tsx +122 -0
- package/src/movethewheels/providers/MoveTheWheelsProvider.tsx +66 -0
- package/src/movethewheels/store/movethewheelsStore.ts +136 -0
- package/src/movethewheels/types/index.ts +744 -0
- package/src/movethewheels/utils/cn.ts +9 -0
- package/src/movethewheels/utils/formatters.ts +215 -0
- package/src/movethewheels/utils/graphqlClient.ts +63 -0
- package/src/movethewheels/utils/index.ts +8 -0
- package/src/movethewheels/utils/navigation.ts +70 -0
- package/src/operations/global/.gitkeep +0 -0
- package/src/operations/wspace/movethewheels/fragments/core.graphql +191 -0
- package/src/operations/wspace/movethewheels/mutations/entities.graphql +87 -0
- package/src/operations/wspace/movethewheels/mutations/logistics.graphql +86 -0
- package/src/operations/wspace/movethewheels/mutations/marketplace-reports.graphql +81 -0
- package/src/operations/wspace/movethewheels/mutations/orders.graphql +21 -0
- package/src/operations/wspace/movethewheels/queries/dashboard.graphql +61 -0
- package/src/operations/wspace/movethewheels/queries/entities.graphql +83 -0
- package/src/operations/wspace/movethewheels/queries/logistics.graphql +84 -0
- package/src/operations/wspace/movethewheels/queries/marketplace-reports.graphql +40 -0
- package/src/operations/wspace/movethewheels/queries/orders.graphql +43 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { useState, useMemo, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Table,
|
|
4
|
+
TableBody,
|
|
5
|
+
TableCell,
|
|
6
|
+
TableHead,
|
|
7
|
+
TableHeader,
|
|
8
|
+
TableRow,
|
|
9
|
+
Button,
|
|
10
|
+
Input,
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
Skeleton,
|
|
17
|
+
} from './ui';
|
|
18
|
+
import { cn } from '../utils/cn';
|
|
19
|
+
import {
|
|
20
|
+
ChevronLeft,
|
|
21
|
+
ChevronRight,
|
|
22
|
+
ChevronsLeft,
|
|
23
|
+
ChevronsRight,
|
|
24
|
+
Search,
|
|
25
|
+
ArrowUpDown,
|
|
26
|
+
ArrowUp,
|
|
27
|
+
ArrowDown,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
|
|
30
|
+
export interface Column<T> {
|
|
31
|
+
id: string;
|
|
32
|
+
header: string | ReactNode;
|
|
33
|
+
accessorKey?: keyof T;
|
|
34
|
+
cell?: (row: T) => ReactNode;
|
|
35
|
+
sortable?: boolean;
|
|
36
|
+
className?: string;
|
|
37
|
+
headerClassName?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface DataTableProps<T> {
|
|
41
|
+
columns: Column<T>[];
|
|
42
|
+
data: T[];
|
|
43
|
+
isLoading?: boolean;
|
|
44
|
+
searchable?: boolean;
|
|
45
|
+
searchPlaceholder?: string;
|
|
46
|
+
searchFields?: (keyof T)[];
|
|
47
|
+
pagination?: boolean;
|
|
48
|
+
pageSize?: number;
|
|
49
|
+
pageSizeOptions?: number[];
|
|
50
|
+
onRowClick?: (row: T) => void;
|
|
51
|
+
rowClassName?: string | ((row: T) => string);
|
|
52
|
+
emptyMessage?: string;
|
|
53
|
+
className?: string;
|
|
54
|
+
getRowId?: (row: T) => string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type SortDirection = 'asc' | 'desc' | null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* DataTable - A reusable data table component with sorting, search, and pagination
|
|
61
|
+
*/
|
|
62
|
+
export function DataTable<T extends object>({
|
|
63
|
+
columns,
|
|
64
|
+
data,
|
|
65
|
+
isLoading = false,
|
|
66
|
+
searchable = false,
|
|
67
|
+
searchPlaceholder = 'Search...',
|
|
68
|
+
searchFields,
|
|
69
|
+
pagination = true,
|
|
70
|
+
pageSize: initialPageSize = 10,
|
|
71
|
+
pageSizeOptions = [5, 10, 20, 50],
|
|
72
|
+
onRowClick,
|
|
73
|
+
rowClassName,
|
|
74
|
+
emptyMessage = 'No data found',
|
|
75
|
+
className,
|
|
76
|
+
getRowId,
|
|
77
|
+
}: DataTableProps<T>) {
|
|
78
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
79
|
+
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
80
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
81
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
82
|
+
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
83
|
+
|
|
84
|
+
// Filter data based on search
|
|
85
|
+
const filteredData = useMemo(() => {
|
|
86
|
+
if (!searchQuery || !searchFields || searchFields.length === 0) {
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const query = searchQuery.toLowerCase();
|
|
91
|
+
return data.filter((row) =>
|
|
92
|
+
searchFields.some((field) => {
|
|
93
|
+
const value = row[field] as unknown;
|
|
94
|
+
return value != null && String(value).toLowerCase().includes(query);
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}, [data, searchQuery, searchFields]);
|
|
98
|
+
|
|
99
|
+
// Sort data
|
|
100
|
+
const sortedData = useMemo(() => {
|
|
101
|
+
if (!sortColumn || !sortDirection) {
|
|
102
|
+
return filteredData;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const column = columns.find((c) => c.id === sortColumn);
|
|
106
|
+
if (!column?.accessorKey) {
|
|
107
|
+
return filteredData;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...filteredData].sort((a, b) => {
|
|
111
|
+
const aVal = a[column.accessorKey as keyof T] as unknown;
|
|
112
|
+
const bVal = b[column.accessorKey as keyof T] as unknown;
|
|
113
|
+
|
|
114
|
+
if (aVal == null) return 1;
|
|
115
|
+
if (bVal == null) return -1;
|
|
116
|
+
|
|
117
|
+
let comparison = 0;
|
|
118
|
+
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
119
|
+
comparison = aVal.localeCompare(bVal);
|
|
120
|
+
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
121
|
+
comparison = aVal - bVal;
|
|
122
|
+
} else if (aVal instanceof Date && bVal instanceof Date) {
|
|
123
|
+
comparison = aVal.getTime() - bVal.getTime();
|
|
124
|
+
} else {
|
|
125
|
+
comparison = String(aVal).localeCompare(String(bVal));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return sortDirection === 'asc' ? comparison : -comparison;
|
|
129
|
+
});
|
|
130
|
+
}, [filteredData, sortColumn, sortDirection, columns]);
|
|
131
|
+
|
|
132
|
+
// Paginate data
|
|
133
|
+
const paginatedData = useMemo(() => {
|
|
134
|
+
if (!pagination) return sortedData;
|
|
135
|
+
|
|
136
|
+
const start = (currentPage - 1) * pageSize;
|
|
137
|
+
return sortedData.slice(start, start + pageSize);
|
|
138
|
+
}, [sortedData, currentPage, pageSize, pagination]);
|
|
139
|
+
|
|
140
|
+
const totalPages = Math.ceil(sortedData.length / pageSize);
|
|
141
|
+
|
|
142
|
+
// Handle sort
|
|
143
|
+
const handleSort = (columnId: string) => {
|
|
144
|
+
if (sortColumn === columnId) {
|
|
145
|
+
if (sortDirection === 'asc') {
|
|
146
|
+
setSortDirection('desc');
|
|
147
|
+
} else if (sortDirection === 'desc') {
|
|
148
|
+
setSortColumn(null);
|
|
149
|
+
setSortDirection(null);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
setSortColumn(columnId);
|
|
153
|
+
setSortDirection('asc');
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Get sort icon
|
|
158
|
+
const getSortIcon = (columnId: string) => {
|
|
159
|
+
if (sortColumn !== columnId) {
|
|
160
|
+
return <ArrowUpDown className="w-4 h-4 text-muted-foreground/50" />;
|
|
161
|
+
}
|
|
162
|
+
if (sortDirection === 'asc') {
|
|
163
|
+
return <ArrowUp className="w-4 h-4 text-primary" />;
|
|
164
|
+
}
|
|
165
|
+
return <ArrowDown className="w-4 h-4 text-primary" />;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Get cell value
|
|
169
|
+
const getCellValue = (row: T, column: Column<T>) => {
|
|
170
|
+
if (column.cell) {
|
|
171
|
+
return column.cell(row);
|
|
172
|
+
}
|
|
173
|
+
if (column.accessorKey) {
|
|
174
|
+
const value = row[column.accessorKey] as unknown;
|
|
175
|
+
if (value == null) return '-';
|
|
176
|
+
if (value instanceof Date) return value.toLocaleDateString();
|
|
177
|
+
return String(value);
|
|
178
|
+
}
|
|
179
|
+
return '-';
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Loading skeleton
|
|
183
|
+
if (isLoading) {
|
|
184
|
+
return (
|
|
185
|
+
<div className={cn('space-y-4', className)}>
|
|
186
|
+
{searchable && (
|
|
187
|
+
<div className="flex items-center gap-4">
|
|
188
|
+
<Skeleton className="h-10 w-64" />
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
<div className="rounded-md border">
|
|
192
|
+
<Table>
|
|
193
|
+
<TableHeader>
|
|
194
|
+
<TableRow>
|
|
195
|
+
{columns.map((column) => (
|
|
196
|
+
<TableHead key={column.id}>
|
|
197
|
+
<Skeleton className="h-4 w-20" />
|
|
198
|
+
</TableHead>
|
|
199
|
+
))}
|
|
200
|
+
</TableRow>
|
|
201
|
+
</TableHeader>
|
|
202
|
+
<TableBody>
|
|
203
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
204
|
+
<TableRow key={i}>
|
|
205
|
+
{columns.map((column) => (
|
|
206
|
+
<TableCell key={column.id}>
|
|
207
|
+
<Skeleton className="h-4 w-full max-w-[100px]" />
|
|
208
|
+
</TableCell>
|
|
209
|
+
))}
|
|
210
|
+
</TableRow>
|
|
211
|
+
))}
|
|
212
|
+
</TableBody>
|
|
213
|
+
</Table>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className={cn('space-y-4', className)}>
|
|
221
|
+
{/* Search */}
|
|
222
|
+
{searchable && (
|
|
223
|
+
<div className="flex items-center gap-4">
|
|
224
|
+
<div className="relative flex-1 max-w-sm">
|
|
225
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
226
|
+
<Input
|
|
227
|
+
placeholder={searchPlaceholder}
|
|
228
|
+
value={searchQuery}
|
|
229
|
+
onChange={(e) => {
|
|
230
|
+
setSearchQuery(e.target.value);
|
|
231
|
+
setCurrentPage(1);
|
|
232
|
+
}}
|
|
233
|
+
className="pl-9"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Table */}
|
|
240
|
+
<div className="rounded-md border">
|
|
241
|
+
<Table>
|
|
242
|
+
<TableHeader>
|
|
243
|
+
<TableRow>
|
|
244
|
+
{columns.map((column) => (
|
|
245
|
+
<TableHead
|
|
246
|
+
key={column.id}
|
|
247
|
+
className={cn(
|
|
248
|
+
column.sortable && 'cursor-pointer select-none hover:bg-muted/50',
|
|
249
|
+
column.headerClassName
|
|
250
|
+
)}
|
|
251
|
+
onClick={() => column.sortable && handleSort(column.id)}
|
|
252
|
+
>
|
|
253
|
+
<div className="flex items-center gap-2">
|
|
254
|
+
{column.header}
|
|
255
|
+
{column.sortable && getSortIcon(column.id)}
|
|
256
|
+
</div>
|
|
257
|
+
</TableHead>
|
|
258
|
+
))}
|
|
259
|
+
</TableRow>
|
|
260
|
+
</TableHeader>
|
|
261
|
+
<TableBody>
|
|
262
|
+
{paginatedData.length === 0 ? (
|
|
263
|
+
<TableRow>
|
|
264
|
+
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
265
|
+
<p className="text-muted-foreground">{emptyMessage}</p>
|
|
266
|
+
</TableCell>
|
|
267
|
+
</TableRow>
|
|
268
|
+
) : (
|
|
269
|
+
paginatedData.map((row, index) => {
|
|
270
|
+
const rowId = getRowId ? getRowId(row) : index;
|
|
271
|
+
const rowClasses =
|
|
272
|
+
typeof rowClassName === 'function' ? rowClassName(row) : rowClassName;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<TableRow
|
|
276
|
+
key={rowId}
|
|
277
|
+
className={cn(onRowClick && 'cursor-pointer hover:bg-muted/50', rowClasses)}
|
|
278
|
+
onClick={() => onRowClick?.(row)}
|
|
279
|
+
>
|
|
280
|
+
{columns.map((column) => (
|
|
281
|
+
<TableCell key={column.id} className={column.className}>
|
|
282
|
+
{getCellValue(row, column)}
|
|
283
|
+
</TableCell>
|
|
284
|
+
))}
|
|
285
|
+
</TableRow>
|
|
286
|
+
);
|
|
287
|
+
})
|
|
288
|
+
)}
|
|
289
|
+
</TableBody>
|
|
290
|
+
</Table>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* Pagination */}
|
|
294
|
+
{pagination && sortedData.length > 0 && (
|
|
295
|
+
<div className="flex items-center justify-between">
|
|
296
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
297
|
+
<span>
|
|
298
|
+
Showing {(currentPage - 1) * pageSize + 1} to{' '}
|
|
299
|
+
{Math.min(currentPage * pageSize, sortedData.length)} of {sortedData.length}
|
|
300
|
+
</span>
|
|
301
|
+
<Select
|
|
302
|
+
value={String(pageSize)}
|
|
303
|
+
onValueChange={(value) => {
|
|
304
|
+
setPageSize(Number(value));
|
|
305
|
+
setCurrentPage(1);
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
<SelectTrigger className="w-[70px] h-8">
|
|
309
|
+
<SelectValue />
|
|
310
|
+
</SelectTrigger>
|
|
311
|
+
<SelectContent>
|
|
312
|
+
{pageSizeOptions.map((size) => (
|
|
313
|
+
<SelectItem key={size} value={String(size)}>
|
|
314
|
+
{size}
|
|
315
|
+
</SelectItem>
|
|
316
|
+
))}
|
|
317
|
+
</SelectContent>
|
|
318
|
+
</Select>
|
|
319
|
+
<span>per page</span>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div className="flex items-center gap-1">
|
|
323
|
+
<Button
|
|
324
|
+
variant="outline"
|
|
325
|
+
size="icon"
|
|
326
|
+
className="h-8 w-8"
|
|
327
|
+
onClick={() => setCurrentPage(1)}
|
|
328
|
+
disabled={currentPage === 1}
|
|
329
|
+
>
|
|
330
|
+
<ChevronsLeft className="h-4 w-4" />
|
|
331
|
+
</Button>
|
|
332
|
+
<Button
|
|
333
|
+
variant="outline"
|
|
334
|
+
size="icon"
|
|
335
|
+
className="h-8 w-8"
|
|
336
|
+
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
337
|
+
disabled={currentPage === 1}
|
|
338
|
+
>
|
|
339
|
+
<ChevronLeft className="h-4 w-4" />
|
|
340
|
+
</Button>
|
|
341
|
+
<span className="px-3 text-sm">
|
|
342
|
+
Page {currentPage} of {totalPages}
|
|
343
|
+
</span>
|
|
344
|
+
<Button
|
|
345
|
+
variant="outline"
|
|
346
|
+
size="icon"
|
|
347
|
+
className="h-8 w-8"
|
|
348
|
+
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
|
349
|
+
disabled={currentPage === totalPages}
|
|
350
|
+
>
|
|
351
|
+
<ChevronRight className="h-4 w-4" />
|
|
352
|
+
</Button>
|
|
353
|
+
<Button
|
|
354
|
+
variant="outline"
|
|
355
|
+
size="icon"
|
|
356
|
+
className="h-8 w-8"
|
|
357
|
+
onClick={() => setCurrentPage(totalPages)}
|
|
358
|
+
disabled={currentPage === totalPages}
|
|
359
|
+
>
|
|
360
|
+
<ChevronsRight className="h-4 w-4" />
|
|
361
|
+
</Button>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { Card, CardContent } from './ui';
|
|
4
|
+
|
|
5
|
+
interface MetricCardProps {
|
|
6
|
+
/** Main value to display */
|
|
7
|
+
value: string | number;
|
|
8
|
+
/** Label describing the metric */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Optional icon */
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
/** Change from previous period (e.g., "+12%", "-5%") */
|
|
13
|
+
change?: string;
|
|
14
|
+
/** Whether the change is positive, negative, or neutral */
|
|
15
|
+
changeType?: 'positive' | 'negative' | 'neutral';
|
|
16
|
+
/** Optional subtitle or additional info */
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
/** Optional trend indicator */
|
|
19
|
+
trend?: 'up' | 'down' | 'flat';
|
|
20
|
+
/** Card size variant */
|
|
21
|
+
size?: 'sm' | 'md' | 'lg';
|
|
22
|
+
/** Optional click handler */
|
|
23
|
+
onClick?: () => void;
|
|
24
|
+
/** Additional CSS classes */
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sizeClasses = {
|
|
29
|
+
sm: {
|
|
30
|
+
card: 'p-3',
|
|
31
|
+
value: 'text-xl font-bold',
|
|
32
|
+
label: 'text-xs',
|
|
33
|
+
icon: 'w-8 h-8',
|
|
34
|
+
},
|
|
35
|
+
md: {
|
|
36
|
+
card: 'p-4',
|
|
37
|
+
value: 'text-2xl font-bold',
|
|
38
|
+
label: 'text-sm',
|
|
39
|
+
icon: 'w-10 h-10',
|
|
40
|
+
},
|
|
41
|
+
lg: {
|
|
42
|
+
card: 'p-6',
|
|
43
|
+
value: 'text-3xl font-bold',
|
|
44
|
+
label: 'text-base',
|
|
45
|
+
icon: 'w-12 h-12',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* MetricCard - Displays a key metric with optional change indicator
|
|
51
|
+
*/
|
|
52
|
+
export function MetricCard({
|
|
53
|
+
value,
|
|
54
|
+
label,
|
|
55
|
+
icon,
|
|
56
|
+
change,
|
|
57
|
+
changeType = 'neutral',
|
|
58
|
+
subtitle,
|
|
59
|
+
trend,
|
|
60
|
+
size = 'md',
|
|
61
|
+
onClick,
|
|
62
|
+
className,
|
|
63
|
+
}: MetricCardProps) {
|
|
64
|
+
const styles = sizeClasses[size];
|
|
65
|
+
|
|
66
|
+
const changeColors = {
|
|
67
|
+
positive: 'text-green-600 dark:text-green-400',
|
|
68
|
+
negative: 'text-red-600 dark:text-red-400',
|
|
69
|
+
neutral: 'text-muted-foreground',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const trendIcons = {
|
|
73
|
+
up: (
|
|
74
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
75
|
+
<path
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
strokeWidth={2}
|
|
79
|
+
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
80
|
+
/>
|
|
81
|
+
</svg>
|
|
82
|
+
),
|
|
83
|
+
down: (
|
|
84
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
85
|
+
<path
|
|
86
|
+
strokeLinecap="round"
|
|
87
|
+
strokeLinejoin="round"
|
|
88
|
+
strokeWidth={2}
|
|
89
|
+
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
|
90
|
+
/>
|
|
91
|
+
</svg>
|
|
92
|
+
),
|
|
93
|
+
flat: (
|
|
94
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
95
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
|
|
96
|
+
</svg>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Card
|
|
102
|
+
className={cn(
|
|
103
|
+
'overflow-hidden transition-all duration-200',
|
|
104
|
+
onClick && 'cursor-pointer hover:shadow-md hover:border-primary/30',
|
|
105
|
+
className
|
|
106
|
+
)}
|
|
107
|
+
onClick={onClick}
|
|
108
|
+
>
|
|
109
|
+
<CardContent className={styles.card}>
|
|
110
|
+
<div className="flex items-start justify-between">
|
|
111
|
+
<div className="space-y-1">
|
|
112
|
+
<p className={cn('text-muted-foreground', styles.label)}>{label}</p>
|
|
113
|
+
<p className={cn('text-foreground tracking-tight', styles.value)}>
|
|
114
|
+
{typeof value === 'number' ? value.toLocaleString() : value}
|
|
115
|
+
</p>
|
|
116
|
+
{subtitle && <p className="text-xs text-muted-foreground">{subtitle}</p>}
|
|
117
|
+
{change && (
|
|
118
|
+
<div className={cn('flex items-center gap-1 text-sm', changeColors[changeType])}>
|
|
119
|
+
{trend && trendIcons[trend]}
|
|
120
|
+
<span>{change}</span>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
{icon && (
|
|
125
|
+
<div
|
|
126
|
+
className={cn('flex-shrink-0 p-2 rounded-lg bg-primary/10 text-primary', styles.icon)}
|
|
127
|
+
>
|
|
128
|
+
{icon}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</CardContent>
|
|
133
|
+
</Card>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* MetricCardSkeleton - Loading state for MetricCard
|
|
139
|
+
*/
|
|
140
|
+
export function MetricCardSkeleton({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
|
|
141
|
+
const styles = sizeClasses[size];
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Card className="overflow-hidden">
|
|
145
|
+
<CardContent className={styles.card}>
|
|
146
|
+
<div className="flex items-start justify-between">
|
|
147
|
+
<div className="space-y-2 flex-1">
|
|
148
|
+
<div className="h-4 w-24 bg-muted animate-pulse rounded" />
|
|
149
|
+
<div className="h-8 w-32 bg-muted animate-pulse rounded" />
|
|
150
|
+
<div className="h-3 w-20 bg-muted animate-pulse rounded" />
|
|
151
|
+
</div>
|
|
152
|
+
<div className={cn('bg-muted animate-pulse rounded-lg', styles.icon)} />
|
|
153
|
+
</div>
|
|
154
|
+
</CardContent>
|
|
155
|
+
</Card>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* MetricGrid - Grid container for metric cards
|
|
161
|
+
*/
|
|
162
|
+
export function MetricGrid({
|
|
163
|
+
children,
|
|
164
|
+
columns = 4,
|
|
165
|
+
className,
|
|
166
|
+
}: {
|
|
167
|
+
children: ReactNode;
|
|
168
|
+
columns?: 2 | 3 | 4 | 5 | 6;
|
|
169
|
+
className?: string;
|
|
170
|
+
}) {
|
|
171
|
+
const colClasses = {
|
|
172
|
+
2: 'grid-cols-1 sm:grid-cols-2',
|
|
173
|
+
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
|
174
|
+
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
|
175
|
+
5: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5',
|
|
176
|
+
6: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6',
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return <div className={cn('grid gap-4', colClasses[columns], className)}>{children}</div>;
|
|
180
|
+
}
|