@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.
Files changed (208) hide show
  1. package/README.md +82 -0
  2. package/dist/AIAssistantPage-hD0VYJdH.js +210 -0
  3. package/dist/AnalyticsPage-DHTHCUtr.js +201 -0
  4. package/dist/CreateOrderPage-Cprg4Y9V.js +471 -0
  5. package/dist/CustomerDetailsPage-DNDEw7IW.js +239 -0
  6. package/dist/CustomersPage-CDjjeCEL.js +119 -0
  7. package/dist/DashboardPage-8iTPXRAG.js +374 -0
  8. package/dist/DataTable-CRIKfdIN.js +239 -0
  9. package/dist/DriverDetailsPage-CRyRCno7.js +297 -0
  10. package/dist/DriversPage-16O8fVmf.js +127 -0
  11. package/dist/FinancePage-BYUxK5dR.js +154 -0
  12. package/dist/FleetPage-CHYETCWT.js +293 -0
  13. package/dist/ImportExportPage-C3MKKxfc.js +232 -0
  14. package/dist/InventoryPage--822AxZM.js +223 -0
  15. package/dist/LiveTrackingPage-Dp3rTJDr.js +332 -0
  16. package/dist/MarketplacePage-DjEqudfM.js +192 -0
  17. package/dist/MetricCard-GTbxAk1a.js +135 -0
  18. package/dist/OrderDetailsPage-BIuYG0ub.js +398 -0
  19. package/dist/OrdersListPage-CW5V0Uvh.js +257 -0
  20. package/dist/PageLayout-B7b0vl0R.js +1894 -0
  21. package/dist/ProductDetailsPage-Q3X7AT-7.js +168 -0
  22. package/dist/ProductsPage-CUj9JpnW.js +131 -0
  23. package/dist/ReportsPage-DblO5CdJ.js +227 -0
  24. package/dist/RouteDetailsPage-CLctgk6A.js +240 -0
  25. package/dist/RoutesPage-8hrv6RWT.js +116 -0
  26. package/dist/SettingsPage-BJ5BQeqn.js +247 -0
  27. package/dist/StatusBadge-BrrwraIA.js +206 -0
  28. package/dist/TrackingPage-BGqHDh-w.js +322 -0
  29. package/dist/VehicleDetailsPage-XnDH4iQR.js +194 -0
  30. package/dist/VehiclesPage-Cs4XxHkA.js +127 -0
  31. package/dist/WarehouseDetailsPage-GemdMvr_.js +215 -0
  32. package/dist/WarehousesPage-QTiuDuXy.js +121 -0
  33. package/dist/arrow-left-6CiLhqVp.js +11 -0
  34. package/dist/box-BunB_4UH.js +18 -0
  35. package/dist/chart-column-DWwVEVQ-.js +22 -0
  36. package/dist/chevron-right-DhZVf20o.js +8 -0
  37. package/dist/circle-alert-D5f6RZxt.js +26 -0
  38. package/dist/circle-check-big-D-JMHcTe.js +11 -0
  39. package/dist/clock-CvwBKbQP.js +13 -0
  40. package/dist/dev/main.d.ts +1 -0
  41. package/dist/dollar-sign-CP9qeU5d.js +14 -0
  42. package/dist/download-CIuG04pJ.js +21 -0
  43. package/dist/file-text-Dd_thxkn.js +26 -0
  44. package/dist/filter-DyRMX9CU.js +8 -0
  45. package/dist/formatters-_vJlC-47.js +50 -0
  46. package/dist/generated/global-operations.d.ts +1 -0
  47. package/dist/generated/global-types.d.ts +20715 -0
  48. package/dist/generated/wspace-operations.d.ts +3704 -0
  49. package/dist/generated/wspace-types.d.ts +53362 -0
  50. package/dist/graphqlClient-CdJyR_ed.js +55 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.js +772 -0
  53. package/dist/map-BqH1cBJi.js +18 -0
  54. package/dist/map-pin-CFBOmh-A.js +13 -0
  55. package/dist/movethewheels/MoveTheWheelsRoot.d.ts +25 -0
  56. package/dist/movethewheels/MoveTheWheelsRoutes.d.ts +7 -0
  57. package/dist/movethewheels/components/DataTable.d.ts +32 -0
  58. package/dist/movethewheels/components/MetricCard.d.ts +43 -0
  59. package/dist/movethewheels/components/PageLayout.d.ts +68 -0
  60. package/dist/movethewheels/components/StatusBadge.d.ts +49 -0
  61. package/dist/movethewheels/components/index.d.ts +10 -0
  62. package/dist/movethewheels/components/ui.d.ts +22 -0
  63. package/dist/movethewheels/constants/index.d.ts +24 -0
  64. package/dist/movethewheels/constants/mockData.d.ts +33 -0
  65. package/dist/movethewheels/hooks/index.d.ts +12 -0
  66. package/dist/movethewheels/hooks/useAnalytics.d.ts +118 -0
  67. package/dist/movethewheels/hooks/useCustomers.d.ts +37 -0
  68. package/dist/movethewheels/hooks/useFleet.d.ts +71 -0
  69. package/dist/movethewheels/hooks/useInventory.d.ts +60 -0
  70. package/dist/movethewheels/hooks/useOrders.d.ts +47 -0
  71. package/dist/movethewheels/hooks/useRoutes.d.ts +41 -0
  72. package/dist/movethewheels/hooks/useTracking.d.ts +69 -0
  73. package/dist/movethewheels/index.d.ts +30 -0
  74. package/dist/movethewheels/pages/AIAssistantPage.d.ts +4 -0
  75. package/dist/movethewheels/pages/AnalyticsPage.d.ts +4 -0
  76. package/dist/movethewheels/pages/CreateOrderPage.d.ts +6 -0
  77. package/dist/movethewheels/pages/CustomerDetailsPage.d.ts +4 -0
  78. package/dist/movethewheels/pages/CustomersPage.d.ts +4 -0
  79. package/dist/movethewheels/pages/DashboardPage.d.ts +6 -0
  80. package/dist/movethewheels/pages/DriverDetailsPage.d.ts +4 -0
  81. package/dist/movethewheels/pages/DriversPage.d.ts +4 -0
  82. package/dist/movethewheels/pages/FinancePage.d.ts +4 -0
  83. package/dist/movethewheels/pages/FleetPage.d.ts +6 -0
  84. package/dist/movethewheels/pages/ImportExportPage.d.ts +4 -0
  85. package/dist/movethewheels/pages/InventoryPage.d.ts +4 -0
  86. package/dist/movethewheels/pages/LiveTrackingPage.d.ts +6 -0
  87. package/dist/movethewheels/pages/MarketplacePage.d.ts +4 -0
  88. package/dist/movethewheels/pages/OrderDetailsPage.d.ts +6 -0
  89. package/dist/movethewheels/pages/OrdersListPage.d.ts +6 -0
  90. package/dist/movethewheels/pages/ProductDetailsPage.d.ts +4 -0
  91. package/dist/movethewheels/pages/ProductsPage.d.ts +4 -0
  92. package/dist/movethewheels/pages/ReportsPage.d.ts +4 -0
  93. package/dist/movethewheels/pages/RouteDetailsPage.d.ts +4 -0
  94. package/dist/movethewheels/pages/RoutesPage.d.ts +4 -0
  95. package/dist/movethewheels/pages/SettingsPage.d.ts +4 -0
  96. package/dist/movethewheels/pages/TrackingPage.d.ts +6 -0
  97. package/dist/movethewheels/pages/VehicleDetailsPage.d.ts +4 -0
  98. package/dist/movethewheels/pages/VehiclesPage.d.ts +4 -0
  99. package/dist/movethewheels/pages/WarehouseDetailsPage.d.ts +4 -0
  100. package/dist/movethewheels/pages/WarehousesPage.d.ts +4 -0
  101. package/dist/movethewheels/providers/MoveTheWheelsProvider.d.ts +16 -0
  102. package/dist/movethewheels/store/movethewheelsStore.d.ts +73 -0
  103. package/dist/movethewheels/types/index.d.ts +655 -0
  104. package/dist/movethewheels/utils/cn.d.ts +6 -0
  105. package/dist/movethewheels/utils/formatters.d.ts +60 -0
  106. package/dist/movethewheels/utils/graphqlClient.d.ts +11 -0
  107. package/dist/movethewheels/utils/index.d.ts +7 -0
  108. package/dist/movethewheels/utils/navigation.d.ts +23 -0
  109. package/dist/navigation-BgnOfsVd.js +6 -0
  110. package/dist/navigation-C2fY_aS9.js +8 -0
  111. package/dist/package-DVZbDRcV.js +22 -0
  112. package/dist/phone-KdwpVmC4.js +18 -0
  113. package/dist/plus-Bl7uX6Ji.js +11 -0
  114. package/dist/refresh-cw-BYjl3K-8.js +22 -0
  115. package/dist/route-Ce_poKFi.js +51 -0
  116. package/dist/save-C-qDVat-.js +18 -0
  117. package/dist/search-5pdn5eOO.js +13 -0
  118. package/dist/settings-C4kIDsYg.js +28 -0
  119. package/dist/square-pen-BwQ67vLE.js +11 -0
  120. package/dist/star-BlVsC3Ad.js +8 -0
  121. package/dist/store-DTmQT5M0.js +26 -0
  122. package/dist/trending-up-C1faflCI.js +11 -0
  123. package/dist/triangle-alert-CUoVAA4L.js +18 -0
  124. package/dist/truck-BmDAzu05.js +30 -0
  125. package/dist/useAnalytics-ph7eTIK6.js +297 -0
  126. package/dist/useCustomers-bS3a4ytk.js +186 -0
  127. package/dist/useFleet-BdETplNE.js +398 -0
  128. package/dist/useInventory-Dwn18FPz.js +323 -0
  129. package/dist/useOrders-D_3_hGMp.js +324 -0
  130. package/dist/useRoutes-v4aBaS-E.js +224 -0
  131. package/dist/useTracking-De2KIUNu.js +261 -0
  132. package/dist/user-BplzDrLP.js +13 -0
  133. package/dist/users-i-igmsP4.js +24 -0
  134. package/dist/warehouse-DewG0PXh.js +25 -0
  135. package/dist/wrench-CoSDEIC7.js +31 -0
  136. package/package.json +107 -0
  137. package/src/dev/main.tsx +110 -0
  138. package/src/dev/styles.css +139 -0
  139. package/src/generated/global-operations.ts +2 -0
  140. package/src/generated/global-types.ts +24048 -0
  141. package/src/generated/wspace-operations.ts +3734 -0
  142. package/src/generated/wspace-types.ts +60715 -0
  143. package/src/index.ts +4 -0
  144. package/src/movethewheels/MoveTheWheelsRoot.tsx +258 -0
  145. package/src/movethewheels/MoveTheWheelsRoutes.tsx +119 -0
  146. package/src/movethewheels/components/DataTable.tsx +367 -0
  147. package/src/movethewheels/components/MetricCard.tsx +180 -0
  148. package/src/movethewheels/components/PageLayout.tsx +234 -0
  149. package/src/movethewheels/components/StatusBadge.tsx +243 -0
  150. package/src/movethewheels/components/index.ts +26 -0
  151. package/src/movethewheels/components/ui.tsx +124 -0
  152. package/src/movethewheels/constants/index.ts +65 -0
  153. package/src/movethewheels/constants/mockData.ts +1342 -0
  154. package/src/movethewheels/hooks/index.ts +55 -0
  155. package/src/movethewheels/hooks/useAnalytics.ts +476 -0
  156. package/src/movethewheels/hooks/useCustomers.ts +359 -0
  157. package/src/movethewheels/hooks/useFleet.ts +778 -0
  158. package/src/movethewheels/hooks/useInventory.ts +632 -0
  159. package/src/movethewheels/hooks/useOrders.ts +703 -0
  160. package/src/movethewheels/hooks/useRoutes.ts +453 -0
  161. package/src/movethewheels/hooks/useTracking.ts +505 -0
  162. package/src/movethewheels/index.ts +68 -0
  163. package/src/movethewheels/pages/AIAssistantPage.tsx +160 -0
  164. package/src/movethewheels/pages/AnalyticsPage.tsx +190 -0
  165. package/src/movethewheels/pages/CreateOrderPage.tsx +454 -0
  166. package/src/movethewheels/pages/CustomerDetailsPage.tsx +207 -0
  167. package/src/movethewheels/pages/CustomersPage.tsx +115 -0
  168. package/src/movethewheels/pages/DashboardPage.tsx +414 -0
  169. package/src/movethewheels/pages/DriverDetailsPage.tsx +261 -0
  170. package/src/movethewheels/pages/DriversPage.tsx +118 -0
  171. package/src/movethewheels/pages/FinancePage.tsx +141 -0
  172. package/src/movethewheels/pages/FleetPage.tsx +289 -0
  173. package/src/movethewheels/pages/ImportExportPage.tsx +165 -0
  174. package/src/movethewheels/pages/InventoryPage.tsx +212 -0
  175. package/src/movethewheels/pages/LiveTrackingPage.tsx +325 -0
  176. package/src/movethewheels/pages/MarketplacePage.tsx +235 -0
  177. package/src/movethewheels/pages/OrderDetailsPage.tsx +387 -0
  178. package/src/movethewheels/pages/OrdersListPage.tsx +241 -0
  179. package/src/movethewheels/pages/ProductDetailsPage.tsx +155 -0
  180. package/src/movethewheels/pages/ProductsPage.tsx +124 -0
  181. package/src/movethewheels/pages/ReportsPage.tsx +164 -0
  182. package/src/movethewheels/pages/RouteDetailsPage.tsx +245 -0
  183. package/src/movethewheels/pages/RoutesPage.tsx +104 -0
  184. package/src/movethewheels/pages/SettingsPage.tsx +242 -0
  185. package/src/movethewheels/pages/TrackingPage.tsx +419 -0
  186. package/src/movethewheels/pages/VehicleDetailsPage.tsx +218 -0
  187. package/src/movethewheels/pages/VehiclesPage.tsx +124 -0
  188. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +216 -0
  189. package/src/movethewheels/pages/WarehousesPage.tsx +122 -0
  190. package/src/movethewheels/providers/MoveTheWheelsProvider.tsx +66 -0
  191. package/src/movethewheels/store/movethewheelsStore.ts +136 -0
  192. package/src/movethewheels/types/index.ts +744 -0
  193. package/src/movethewheels/utils/cn.ts +9 -0
  194. package/src/movethewheels/utils/formatters.ts +215 -0
  195. package/src/movethewheels/utils/graphqlClient.ts +63 -0
  196. package/src/movethewheels/utils/index.ts +8 -0
  197. package/src/movethewheels/utils/navigation.ts +70 -0
  198. package/src/operations/global/.gitkeep +0 -0
  199. package/src/operations/wspace/movethewheels/fragments/core.graphql +191 -0
  200. package/src/operations/wspace/movethewheels/mutations/entities.graphql +87 -0
  201. package/src/operations/wspace/movethewheels/mutations/logistics.graphql +86 -0
  202. package/src/operations/wspace/movethewheels/mutations/marketplace-reports.graphql +81 -0
  203. package/src/operations/wspace/movethewheels/mutations/orders.graphql +21 -0
  204. package/src/operations/wspace/movethewheels/queries/dashboard.graphql +61 -0
  205. package/src/operations/wspace/movethewheels/queries/entities.graphql +83 -0
  206. package/src/operations/wspace/movethewheels/queries/logistics.graphql +84 -0
  207. package/src/operations/wspace/movethewheels/queries/marketplace-reports.graphql +40 -0
  208. 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
+ }