@burdenoff/microfe-movethewheels 2026.703.3 → 2026.703.5

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 (173) hide show
  1. package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
  2. package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
  3. package/dist/CreateOrderPage-CWHx8yys.js +490 -0
  4. package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
  5. package/dist/CustomersPage-Bbvwvn8k.js +205 -0
  6. package/dist/DashboardPage-DrEhNf7N.js +542 -0
  7. package/dist/DataTable-CqMUD4er.js +340 -0
  8. package/dist/DocumentsCard-edDkgnJj.js +169 -0
  9. package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
  10. package/dist/DriversPage-CHF2ZEzd.js +221 -0
  11. package/dist/FinancePage-Dn9GrW-1.js +175 -0
  12. package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
  13. package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
  14. package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
  15. package/dist/InventoryPage-l5A1f753.js +259 -0
  16. package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
  17. package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
  18. package/dist/MetricCard-y3bWHKUQ.js +191 -0
  19. package/dist/Notice-DOO-tjpV.js +46 -0
  20. package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
  21. package/dist/OrdersListPage-DhZJoQeI.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
  23. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
  24. package/dist/ProductsPage-C8sn-1tN.js +186 -0
  25. package/dist/ReportsPage-D1tb0dcz.js +377 -0
  26. package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
  27. package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
  28. package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
  29. package/dist/Sparkline-K2RMJ58u.js +16729 -0
  30. package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
  31. package/dist/TrackingPage-BZrg4RCu.js +368 -0
  32. package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
  33. package/dist/VehiclesPage-D5BELbtM.js +184 -0
  34. package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
  35. package/dist/WarehousesPage-C1Y1M867.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
  37. package/dist/arrow-right-CmKOmIkh.js +11 -0
  38. package/dist/arrow-up-BJijRLAZ.js +17 -0
  39. package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
  40. package/dist/boxes-B_OGvQiW.js +54 -0
  41. package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
  42. package/dist/charts-Tj_PPTfA.js +2018 -0
  43. package/dist/chevron-left-B1rZyWWb.js +8 -0
  44. package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
  47. package/dist/circle-check-Cs1OVjtV.js +13 -0
  48. package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
  49. package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
  50. package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
  51. package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
  52. package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
  53. package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
  54. package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +150 -172
  57. package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
  58. package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-yzLqiK_C.js +5070 -0
  61. package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
  62. package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
  63. package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
  64. package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
  65. package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
  66. package/dist/route-CWjPrtAN.js +22 -0
  67. package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
  68. package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
  69. package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
  70. package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
  71. package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
  72. package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
  73. package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
  74. package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
  75. package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
  76. package/dist/upload-DanJY1Vo.js +21 -0
  77. package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
  78. package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
  79. package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
  80. package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
  81. package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
  82. package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
  83. package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
  84. package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
  85. package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
  86. package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
  87. package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
  88. package/dist/user-check-IMhk-qkX.js +20 -0
  89. package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
  90. package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
  91. package/dist/wrench-Dg-sgH2e.js +8 -0
  92. package/dist/x-C4ObLVEa.js +11 -0
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/DataTable.tsx +252 -84
  95. package/src/movethewheels/components/DocumentsCard.tsx +102 -0
  96. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  97. package/src/movethewheels/components/MetricCard.tsx +171 -73
  98. package/src/movethewheels/components/Notice.tsx +61 -0
  99. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  100. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  101. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  102. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  103. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  104. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  105. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  106. package/src/movethewheels/components/charts/index.ts +9 -0
  107. package/src/movethewheels/components/index.ts +51 -1
  108. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  109. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  110. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  111. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  112. package/src/movethewheels/components/maps/index.ts +16 -0
  113. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  114. package/src/movethewheels/components/maps/map-markers.css +121 -0
  115. package/src/movethewheels/hooks/index.ts +9 -0
  116. package/src/movethewheels/hooks/useAnalytics.ts +109 -4
  117. package/src/movethewheels/hooks/useCustomers.ts +11 -4
  118. package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
  119. package/src/movethewheels/hooks/useFleet.ts +26 -7
  120. package/src/movethewheels/hooks/useInventory.ts +70 -7
  121. package/src/movethewheels/hooks/useOrders.ts +32 -2
  122. package/src/movethewheels/hooks/useReports.ts +14 -4
  123. package/src/movethewheels/hooks/useRoutes.ts +20 -3
  124. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  125. package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
  126. package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
  127. package/src/movethewheels/pages/CustomersPage.tsx +53 -28
  128. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  129. package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
  130. package/src/movethewheels/pages/DriversPage.tsx +59 -31
  131. package/src/movethewheels/pages/FinancePage.tsx +103 -82
  132. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  133. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  134. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  135. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  136. package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
  137. package/src/movethewheels/pages/ProductsPage.tsx +52 -28
  138. package/src/movethewheels/pages/ReportsPage.tsx +88 -4
  139. package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
  140. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  141. package/src/movethewheels/pages/SettingsPage.tsx +5 -21
  142. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  143. package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
  144. package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
  145. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  146. package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
  147. package/src/movethewheels/types/index.ts +9 -1
  148. package/src/movethewheels/utils/download.ts +62 -0
  149. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  150. package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
  151. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  152. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  153. package/dist/CustomersPage-BKxMS1AV.js +0 -161
  154. package/dist/DashboardPage-BVjQi0EB.js +0 -387
  155. package/dist/DataTable-DmciIKKM.js +0 -249
  156. package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
  157. package/dist/DriversPage-CH9rjQC-.js +0 -165
  158. package/dist/FinancePage-DEDykSOP.js +0 -158
  159. package/dist/InventoryPage-e0ze63iX.js +0 -253
  160. package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
  161. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  162. package/dist/MetricCard-BY0QrMD4.js +0 -146
  163. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  164. package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
  165. package/dist/ProductsPage-QJfHHpqB.js +0 -169
  166. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  167. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  168. package/dist/TrackingPage-W-jtdM3Y.js +0 -325
  169. package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
  170. package/dist/VehiclesPage-ApKThYeG.js +0 -165
  171. package/dist/WarehousesPage-DCbXXXWE.js +0 -162
  172. package/dist/useAnalytics-DS8n0xgg.js +0 -297
  173. /package/dist/{formatters-CF2S2l1m.js → formatters-FWJDwTFv.js} +0 -0
@@ -1,9 +1,25 @@
1
1
  /**
2
2
  * Marketplace Page
3
+ *
4
+ * Browse third-party logistics service providers and their offerings. Providers
5
+ * are paginated server-side (offset); clicking a provider expands its offerings
6
+ * inline. The Filters control narrows the current page by type, verification and
7
+ * minimum rating.
3
8
  */
4
9
 
5
10
  import { useCallback, useEffect, useMemo, useState } from 'react';
6
- import { Store, Star, MapPin, Clock, Filter, Search } from 'lucide-react';
11
+ import {
12
+ Store,
13
+ Star,
14
+ MapPin,
15
+ Clock,
16
+ Filter,
17
+ Search,
18
+ ChevronDown,
19
+ ChevronUp,
20
+ ChevronLeft,
21
+ ChevronRight,
22
+ } from 'lucide-react';
7
23
  import { PageLayout } from '../components/PageLayout';
8
24
  import {
9
25
  Card,
@@ -14,6 +30,11 @@ import {
14
30
  Badge,
15
31
  Input,
16
32
  Skeleton,
33
+ Select,
34
+ SelectContent,
35
+ SelectItem,
36
+ SelectTrigger,
37
+ SelectValue,
17
38
  } from '../components/ui';
18
39
  import { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';
19
40
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
@@ -86,6 +107,15 @@ const OFFERINGS_QUERY = `
86
107
  }
87
108
  `;
88
109
 
110
+ const PROVIDERS_PAGE_SIZE = 12;
111
+
112
+ const RATING_OPTIONS: { value: string; label: string }[] = [
113
+ { value: '0', label: 'Any rating' },
114
+ { value: '3', label: '3.0+' },
115
+ { value: '4', label: '4.0+' },
116
+ { value: '4.5', label: '4.5+' },
117
+ ];
118
+
89
119
  export default function MarketplacePage() {
90
120
  const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
91
121
  useMoveTheWheels();
@@ -94,6 +124,17 @@ export default function MarketplacePage() {
94
124
  const [search, setSearch] = useState('');
95
125
  const [isLoading, setIsLoading] = useState(false);
96
126
  const [error, setError] = useState<Error | null>(null);
127
+ const [page, setPage] = useState(0);
128
+ const [hasMore, setHasMore] = useState(false);
129
+
130
+ // Inline filters (toggled by the "Filters" button in the header).
131
+ const [showFilters, setShowFilters] = useState(false);
132
+ const [typeFilter, setTypeFilter] = useState('all');
133
+ const [verifiedOnly, setVerifiedOnly] = useState(false);
134
+ const [minRating, setMinRating] = useState('0');
135
+
136
+ // Provider whose offerings are currently expanded.
137
+ const [expandedProviderId, setExpandedProviderId] = useState<string | null>(null);
97
138
 
98
139
  const graphQLOptions = useMemo(
99
140
  () => ({
@@ -115,7 +156,9 @@ export default function MarketplacePage() {
115
156
  executeGraphQL<{ serviceProviders: ApiServiceProvider[] }>(
116
157
  graphQLOptions,
117
158
  PROVIDERS_QUERY,
118
- { pagination: { limit: 200, offset: 0 } }
159
+ {
160
+ pagination: { limit: PROVIDERS_PAGE_SIZE, offset: page * PROVIDERS_PAGE_SIZE },
161
+ }
119
162
  ),
120
163
  executeGraphQL<{ serviceOfferings: ApiServiceOffering[] }>(
121
164
  graphQLOptions,
@@ -127,28 +170,42 @@ export default function MarketplacePage() {
127
170
  ]);
128
171
 
129
172
  setProviders(providersData.serviceProviders);
173
+ setHasMore(providersData.serviceProviders.length === PROVIDERS_PAGE_SIZE);
130
174
  setOfferings(offeringsData.serviceOfferings);
131
175
  } catch (fetchError) {
132
176
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load marketplace'));
133
177
  } finally {
134
178
  setIsLoading(false);
135
179
  }
136
- }, [graphQLOptions]);
180
+ }, [graphQLOptions, page]);
137
181
 
138
182
  useEffect(() => {
139
183
  void load();
140
184
  }, [load]);
141
185
 
186
+ const providerTypes = useMemo(() => {
187
+ const set = new Set<string>();
188
+ providers.forEach((provider) => set.add(provider.type));
189
+ return Array.from(set).sort();
190
+ }, [providers]);
191
+
142
192
  const filteredProviders = useMemo(() => {
143
193
  const term = search.trim().toLowerCase();
144
- if (!term) return providers;
145
- return providers.filter(
146
- (provider) =>
147
- provider.name.toLowerCase().includes(term) ||
148
- (provider.description ?? '').toLowerCase().includes(term) ||
149
- provider.type.toLowerCase().includes(term)
150
- );
151
- }, [providers, search]);
194
+ const rating = Number(minRating);
195
+ return providers.filter((provider) => {
196
+ if (term) {
197
+ const matches =
198
+ provider.name.toLowerCase().includes(term) ||
199
+ (provider.description ?? '').toLowerCase().includes(term) ||
200
+ provider.type.toLowerCase().includes(term);
201
+ if (!matches) return false;
202
+ }
203
+ if (typeFilter !== 'all' && provider.type !== typeFilter) return false;
204
+ if (verifiedOnly && !provider.isVerified) return false;
205
+ if (rating > 0 && (provider.rating ?? 0) < rating) return false;
206
+ return true;
207
+ });
208
+ }, [providers, search, typeFilter, verifiedOnly, minRating]);
152
209
 
153
210
  const filteredOfferings = useMemo(() => {
154
211
  const term = search.trim().toLowerCase();
@@ -161,13 +218,23 @@ export default function MarketplacePage() {
161
218
  );
162
219
  }, [offerings, search]);
163
220
 
221
+ const offeringsByProvider = useCallback(
222
+ (providerId: string) => offerings.filter((offering) => offering.providerId === providerId),
223
+ [offerings]
224
+ );
225
+
226
+ const filtersActive = typeFilter !== 'all' || verifiedOnly || minRating !== '0';
227
+
164
228
  return (
165
229
  <PageLayout
166
230
  title="Marketplace"
167
231
  subtitle="Find logistics service providers"
168
232
  icon={<Store size={24} />}
169
233
  headerActions={
170
- <Button variant="outline">
234
+ <Button
235
+ variant={showFilters || filtersActive ? 'default' : 'outline'}
236
+ onClick={() => setShowFilters((prev) => !prev)}
237
+ >
171
238
  <Filter size={16} /> Filters
172
239
  </Button>
173
240
  }
@@ -179,7 +246,7 @@ export default function MarketplacePage() {
179
246
  </PagePurpose>
180
247
 
181
248
  <Card>
182
- <CardContent className="p-4">
249
+ <CardContent className="p-4 space-y-4">
183
250
  <div className="relative">
184
251
  <Search
185
252
  className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
@@ -192,6 +259,62 @@ export default function MarketplacePage() {
192
259
  onChange={(event) => setSearch(event.target.value)}
193
260
  />
194
261
  </div>
262
+
263
+ {showFilters && (
264
+ <div className="flex flex-wrap items-center gap-3 border-t border-border-seam pt-4">
265
+ <div className="flex items-center gap-2">
266
+ <span className="text-sm font-medium text-text-secondary">Type</span>
267
+ <Select value={typeFilter} onValueChange={setTypeFilter}>
268
+ <SelectTrigger className="w-[160px]">
269
+ <SelectValue placeholder="All types" />
270
+ </SelectTrigger>
271
+ <SelectContent>
272
+ <SelectItem value="all">All types</SelectItem>
273
+ {providerTypes.map((type) => (
274
+ <SelectItem key={type} value={type}>
275
+ {type}
276
+ </SelectItem>
277
+ ))}
278
+ </SelectContent>
279
+ </Select>
280
+ </div>
281
+ <div className="flex items-center gap-2">
282
+ <span className="text-sm font-medium text-text-secondary">Min rating</span>
283
+ <Select value={minRating} onValueChange={setMinRating}>
284
+ <SelectTrigger className="w-[130px]">
285
+ <SelectValue />
286
+ </SelectTrigger>
287
+ <SelectContent>
288
+ {RATING_OPTIONS.map((option) => (
289
+ <SelectItem key={option.value} value={option.value}>
290
+ {option.label}
291
+ </SelectItem>
292
+ ))}
293
+ </SelectContent>
294
+ </Select>
295
+ </div>
296
+ <Button
297
+ variant={verifiedOnly ? 'default' : 'outline'}
298
+ size="sm"
299
+ onClick={() => setVerifiedOnly((prev) => !prev)}
300
+ >
301
+ Verified only
302
+ </Button>
303
+ {filtersActive && (
304
+ <Button
305
+ variant="ghost"
306
+ size="sm"
307
+ onClick={() => {
308
+ setTypeFilter('all');
309
+ setVerifiedOnly(false);
310
+ setMinRating('0');
311
+ }}
312
+ >
313
+ Clear filters
314
+ </Button>
315
+ )}
316
+ </div>
317
+ )}
195
318
  </CardContent>
196
319
  </Card>
197
320
 
@@ -217,11 +340,15 @@ export default function MarketplacePage() {
217
340
  <Card>
218
341
  <CardContent className="p-0">
219
342
  <IllustratedEmptyState
220
- illustration={search.trim() ? 'empty-search' : 'empty-people'}
221
- title={search.trim() ? 'No providers match your search' : 'No providers yet'}
343
+ illustration={search.trim() || filtersActive ? 'empty-search' : 'empty-people'}
344
+ title={
345
+ search.trim() || filtersActive
346
+ ? 'No providers match your filters'
347
+ : 'No providers yet'
348
+ }
222
349
  description={
223
- search.trim()
224
- ? 'Try a different search term to find logistics partners.'
350
+ search.trim() || filtersActive
351
+ ? 'Adjust your search or filters to find logistics partners.'
225
352
  : 'Verified logistics service providers will appear here as they join the marketplace.'
226
353
  }
227
354
  />
@@ -230,42 +357,130 @@ export default function MarketplacePage() {
230
357
  )}
231
358
  {!isLoading &&
232
359
  !error &&
233
- filteredProviders.map((provider) => (
234
- <Card key={provider.id} className="hover:shadow-md transition-shadow">
235
- <CardContent className="p-6">
236
- <div className="flex items-start justify-between">
237
- <div>
238
- <div className="flex items-center gap-2">
239
- <h4 className="font-semibold text-lg">{provider.name}</h4>
240
- {provider.isVerified && <Badge variant="default">Verified</Badge>}
241
- <Badge variant={provider.isActive ? 'default' : 'secondary'}>
242
- {provider.isActive ? 'active' : 'inactive'}
243
- </Badge>
244
- </div>
245
- <p className="text-text-secondary mt-1">{provider.description}</p>
246
- <div className="flex items-center gap-4 mt-3 text-sm text-text-secondary">
247
- <span className="flex items-center gap-1">
248
- <Star size={14} className="text-status-warning-text fill-current" />{' '}
249
- {provider.rating ?? 0}
250
- </span>
251
- <span className="flex items-center gap-1">
252
- <MapPin size={14} /> {provider.type}
253
- </span>
254
- <span className="flex items-center gap-1">
255
- <Clock size={14} /> Listed
256
- </span>
360
+ filteredProviders.map((provider) => {
361
+ const expanded = expandedProviderId === provider.id;
362
+ const providerOfferings = offeringsByProvider(provider.id);
363
+ return (
364
+ <Card key={provider.id}>
365
+ <button
366
+ type="button"
367
+ aria-expanded={expanded}
368
+ className="w-full text-left"
369
+ onClick={() => setExpandedProviderId(expanded ? null : provider.id)}
370
+ >
371
+ <CardContent className="p-6">
372
+ <div className="flex items-start justify-between gap-3">
373
+ <div className="min-w-0">
374
+ <div className="flex items-center gap-2 flex-wrap">
375
+ <h4 className="font-semibold text-lg">{provider.name}</h4>
376
+ {provider.isVerified && <Badge variant="default">Verified</Badge>}
377
+ <Badge variant={provider.isActive ? 'default' : 'secondary'}>
378
+ {provider.isActive ? 'active' : 'inactive'}
379
+ </Badge>
380
+ </div>
381
+ <p className="text-text-secondary mt-1">{provider.description}</p>
382
+ <div className="flex items-center gap-4 mt-3 text-sm text-text-secondary">
383
+ <span className="flex items-center gap-1">
384
+ <Star size={14} className="text-status-warning-text fill-current" />{' '}
385
+ {provider.rating ?? 0}
386
+ </span>
387
+ <span className="flex items-center gap-1">
388
+ <MapPin size={14} /> {provider.type}
389
+ </span>
390
+ <span className="flex items-center gap-1">
391
+ <Clock size={14} /> Listed
392
+ </span>
393
+ </div>
394
+ </div>
395
+ <div className="text-right shrink-0">
396
+ <p className="text-sm text-text-secondary">Services</p>
397
+ <p className="text-lg font-bold">{providerOfferings.length}</p>
398
+ <span className="mt-1 inline-flex items-center gap-1 text-xs text-primary">
399
+ {expanded ? (
400
+ <>
401
+ Hide <ChevronUp size={14} />
402
+ </>
403
+ ) : (
404
+ <>
405
+ View <ChevronDown size={14} />
406
+ </>
407
+ )}
408
+ </span>
409
+ </div>
257
410
  </div>
258
- </div>
259
- <div className="text-right">
260
- <p className="text-sm text-text-secondary">Services</p>
261
- <p className="text-lg font-bold">
262
- {Array.isArray(provider.services) ? provider.services.length : 0}
411
+ </CardContent>
412
+ </button>
413
+ {expanded && (
414
+ <div className="border-t border-border-seam px-6 py-4 space-y-3">
415
+ <p className="text-sm font-medium text-text-secondary">
416
+ Offerings from {provider.name}
263
417
  </p>
418
+ {providerOfferings.length === 0 ? (
419
+ <p className="text-sm text-text-secondary">
420
+ This provider hasn&apos;t listed any offerings yet.
421
+ </p>
422
+ ) : (
423
+ providerOfferings.map((offering) => (
424
+ <div
425
+ key={offering.id}
426
+ className="flex items-center justify-between gap-3 rounded-lg border border-border-seam p-3"
427
+ >
428
+ <div className="min-w-0">
429
+ <p className="font-medium truncate">{offering.name}</p>
430
+ <p className="text-sm text-text-secondary truncate">
431
+ {offering.category ?? 'general'}
432
+ </p>
433
+ </div>
434
+ <div className="text-right shrink-0">
435
+ <p className="font-bold">{formatCurrency(offering.price)}</p>
436
+ <p className="text-xs text-text-secondary">{offering.currency}</p>
437
+ </div>
438
+ </div>
439
+ ))
440
+ )}
264
441
  </div>
265
- </div>
266
- </CardContent>
267
- </Card>
268
- ))}
442
+ )}
443
+ </Card>
444
+ );
445
+ })}
446
+
447
+ {!isLoading && !error && (providers.length > 0 || page > 0) && (
448
+ <div className="flex flex-wrap items-center justify-between gap-3 pt-1">
449
+ <span className="text-sm text-text-secondary">
450
+ Showing {providers.length === 0 ? 0 : page * PROVIDERS_PAGE_SIZE + 1} to{' '}
451
+ {page * PROVIDERS_PAGE_SIZE + providers.length}
452
+ </span>
453
+ <div className="flex items-center gap-1">
454
+ <Button
455
+ variant="outline"
456
+ size="icon"
457
+ className="h-8 w-8"
458
+ aria-label="Previous page"
459
+ onClick={() => {
460
+ setExpandedProviderId(null);
461
+ setPage((p) => Math.max(0, p - 1));
462
+ }}
463
+ disabled={page === 0}
464
+ >
465
+ <ChevronLeft className="h-4 w-4" />
466
+ </Button>
467
+ <span className="px-3 text-sm">Page {page + 1}</span>
468
+ <Button
469
+ variant="outline"
470
+ size="icon"
471
+ className="h-8 w-8"
472
+ aria-label="Next page"
473
+ onClick={() => {
474
+ setExpandedProviderId(null);
475
+ setPage((p) => p + 1);
476
+ }}
477
+ disabled={!hasMore}
478
+ >
479
+ <ChevronRight className="h-4 w-4" />
480
+ </Button>
481
+ </div>
482
+ </div>
483
+ )}
269
484
  </div>
270
485
 
271
486
  {/* Popular Services — the page's single emphasis zone */}