@burdenoff/microfe-movethewheels 2026.1026.3 → 2026.1030.1

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 (29) hide show
  1. package/dist/MoveTheWheelsRoot.js +130 -122
  2. package/dist/MoveTheWheelsRoutes.js +55 -51
  3. package/dist/components/ProofOfDeliveryCard.js +1 -1
  4. package/dist/components/UpcomingDemandPanel.js +1 -1
  5. package/dist/components/marketplace/MarketplaceDialogs.js +524 -0
  6. package/dist/generated/wspace-types.d.ts +30737 -8505
  7. package/dist/hooks/useMarketplace.js +265 -0
  8. package/dist/movethewheels/components/marketplace/MarketplaceDialogs.d.ts +18 -0
  9. package/dist/movethewheels/hooks/useMarketplace.d.ts +121 -0
  10. package/dist/movethewheels/pages/MarketplacePage.d.ts +1 -0
  11. package/dist/node_modules/lucide-react/dist/esm/icons/badge-check.js +11 -0
  12. package/dist/node_modules/lucide-react/dist/esm/icons/chevron-down.js +8 -0
  13. package/dist/node_modules/lucide-react/dist/esm/icons/chevron-up.js +8 -0
  14. package/dist/node_modules/lucide-react/dist/esm/icons/pencil.js +11 -0
  15. package/dist/node_modules/lucide-react/dist/esm/icons/store.js +26 -0
  16. package/dist/pages/ContainersPage.js +3 -3
  17. package/dist/pages/MarketplacePage.js +503 -0
  18. package/dist/pages/TrackingPage.js +111 -56
  19. package/package.json +2 -1
  20. package/src/generated/wspace-types.ts +34017 -8803
  21. package/src/movethewheels/MoveTheWheelsRoot.tsx +8 -0
  22. package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
  23. package/src/movethewheels/components/ProofOfDeliveryCard.tsx +1 -1
  24. package/src/movethewheels/components/UpcomingDemandPanel.tsx +2 -2
  25. package/src/movethewheels/components/marketplace/MarketplaceDialogs.tsx +633 -0
  26. package/src/movethewheels/hooks/useMarketplace.ts +526 -0
  27. package/src/movethewheels/pages/ContainersPage.tsx +3 -3
  28. package/src/movethewheels/pages/MarketplacePage.tsx +571 -0
  29. package/src/movethewheels/pages/TrackingPage.tsx +54 -9
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Marketplace hooks (BOFF-3988, MOV-UC-13).
3
+ *
4
+ * The workspace's directory of vetted service providers and their offerings,
5
+ * served by wspace-movethewheels-svc's marketplace module. A provider becomes a
6
+ * shipment's carrier through `assignShipmentServiceProvider`.
7
+ */
8
+ import { useCallback, useEffect, useMemo, useState } from 'react';
9
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
10
+ import { executeGraphQL } from '../utils/graphqlClient';
11
+
12
+ export const PROVIDER_TYPES = [
13
+ 'CARRIER',
14
+ 'FREIGHT_FORWARDER',
15
+ 'THIRD_PARTY_LOGISTICS',
16
+ 'WAREHOUSE',
17
+ 'CUSTOMS_BROKER',
18
+ 'LAST_MILE',
19
+ 'COURIER',
20
+ 'OTHER',
21
+ ] as const;
22
+ export type ProviderType = (typeof PROVIDER_TYPES)[number];
23
+
24
+ export const OFFERING_CATEGORIES = [
25
+ 'FULL_TRUCKLOAD',
26
+ 'LESS_THAN_TRUCKLOAD',
27
+ 'LAST_MILE',
28
+ 'EXPRESS',
29
+ 'OCEAN_FREIGHT',
30
+ 'AIR_FREIGHT',
31
+ 'RAIL_FREIGHT',
32
+ 'WAREHOUSING',
33
+ 'CUSTOMS_CLEARANCE',
34
+ 'COLD_CHAIN',
35
+ 'OTHER',
36
+ ] as const;
37
+ export type OfferingCategory = (typeof OFFERING_CATEGORIES)[number];
38
+
39
+ export const AVAILABILITIES = ['AVAILABLE', 'LIMITED', 'UNAVAILABLE'] as const;
40
+ export type Availability = (typeof AVAILABILITIES)[number];
41
+
42
+ /** "THIRD_PARTY_LOGISTICS" → "Third party logistics". */
43
+ export function humanizeEnum(value: string): string {
44
+ const words = value.toLowerCase().split('_');
45
+ const text = words.join(' ');
46
+ return text.charAt(0).toUpperCase() + text.slice(1);
47
+ }
48
+
49
+ export interface ProviderContact {
50
+ contactName?: string;
51
+ email?: string;
52
+ phone?: string;
53
+ website?: string;
54
+ }
55
+
56
+ export interface ServiceOffering {
57
+ id: string;
58
+ providerId: string;
59
+ name: string;
60
+ description: string | null;
61
+ category: OfferingCategory;
62
+ price: number | null;
63
+ currency: string;
64
+ pricingUnit: string | null;
65
+ transitDays: number | null;
66
+ availability: Availability;
67
+ isActive: boolean;
68
+ }
69
+
70
+ export interface ServiceProvider {
71
+ id: string;
72
+ name: string;
73
+ type: ProviderType;
74
+ description: string | null;
75
+ rating: number | null;
76
+ contact: ProviderContact;
77
+ services: string[];
78
+ serviceAreas: string[];
79
+ isVerified: boolean;
80
+ isActive: boolean;
81
+ offeringsCount: number;
82
+ createdAt: string;
83
+ }
84
+
85
+ export interface MarketplaceStats {
86
+ providers: number;
87
+ verifiedProviders: number;
88
+ activeProviders: number;
89
+ offerings: number;
90
+ avgRating: number | null;
91
+ }
92
+
93
+ export interface ProviderFilters {
94
+ search?: string;
95
+ type?: ProviderType;
96
+ isVerified?: boolean;
97
+ }
98
+
99
+ export interface ProviderInput {
100
+ name: string;
101
+ type: ProviderType;
102
+ description: string | null;
103
+ rating: number | null;
104
+ contactInfo: ProviderContact | null;
105
+ services: string[];
106
+ serviceAreas: string[];
107
+ isVerified: boolean;
108
+ isActive: boolean;
109
+ }
110
+
111
+ export interface OfferingInput {
112
+ name: string;
113
+ category: OfferingCategory;
114
+ description: string | null;
115
+ price: number | null;
116
+ currency: string;
117
+ pricingUnit: string | null;
118
+ transitDays: number | null;
119
+ availability: Availability;
120
+ isActive: boolean;
121
+ }
122
+
123
+ interface ApiProvider {
124
+ id: string;
125
+ name: string;
126
+ type: ProviderType;
127
+ description?: string | null;
128
+ rating?: number | null;
129
+ contactInfo?: unknown;
130
+ services: string[];
131
+ serviceAreas: string[];
132
+ isVerified: boolean;
133
+ isActive: boolean;
134
+ offeringsCount: number;
135
+ createdAt: string;
136
+ }
137
+
138
+ const PROVIDER_FIELDS = `
139
+ id
140
+ name
141
+ type
142
+ description
143
+ rating
144
+ contactInfo
145
+ services
146
+ serviceAreas
147
+ isVerified
148
+ isActive
149
+ offeringsCount
150
+ createdAt
151
+ `;
152
+
153
+ const OFFERING_FIELDS = `
154
+ id
155
+ providerId
156
+ name
157
+ description
158
+ category
159
+ price
160
+ currency
161
+ pricingUnit
162
+ transitDays
163
+ availability
164
+ isActive
165
+ `;
166
+
167
+ const LIST_PROVIDERS_QUERY = `
168
+ query ListMtwServiceProviders($filter: MtwServiceProviderFilterInput, $pagination: PaginationInput) {
169
+ serviceProviders(filter: $filter, pagination: $pagination) {
170
+ ${PROVIDER_FIELDS}
171
+ }
172
+ serviceProvidersCount(filter: $filter)
173
+ }
174
+ `;
175
+
176
+ const MARKETPLACE_STATS_QUERY = `
177
+ query MtwMarketplaceStats {
178
+ marketplaceStats {
179
+ providers
180
+ verifiedProviders
181
+ activeProviders
182
+ offerings
183
+ avgRating
184
+ }
185
+ }
186
+ `;
187
+
188
+ const LIST_OFFERINGS_QUERY = `
189
+ query ListMtwServiceOfferings($filter: MtwServiceOfferingFilterInput, $pagination: PaginationInput) {
190
+ serviceOfferings(filter: $filter, pagination: $pagination) {
191
+ ${OFFERING_FIELDS}
192
+ }
193
+ }
194
+ `;
195
+
196
+ const PROVIDER_SHIPMENTS_QUERY = `
197
+ query MtwProviderShipments($filter: ShipmentFilterInput, $pagination: PaginationInput) {
198
+ shipments(filter: $filter, pagination: $pagination) {
199
+ id
200
+ shipmentNumber
201
+ status
202
+ }
203
+ shipmentsCount(filter: $filter)
204
+ }
205
+ `;
206
+
207
+ const CREATE_PROVIDER_MUTATION = `
208
+ mutation CreateMtwServiceProvider($input: MtwCreateServiceProviderInput!) {
209
+ createServiceProvider(input: $input) {
210
+ ${PROVIDER_FIELDS}
211
+ }
212
+ }
213
+ `;
214
+
215
+ const UPDATE_PROVIDER_MUTATION = `
216
+ mutation UpdateMtwServiceProvider($id: ID!, $input: MtwUpdateServiceProviderInput!) {
217
+ updateServiceProvider(id: $id, input: $input) {
218
+ ${PROVIDER_FIELDS}
219
+ }
220
+ }
221
+ `;
222
+
223
+ const DELETE_PROVIDER_MUTATION = `
224
+ mutation DeleteMtwServiceProvider($id: ID!) {
225
+ deleteServiceProvider(id: $id)
226
+ }
227
+ `;
228
+
229
+ const CREATE_OFFERING_MUTATION = `
230
+ mutation CreateMtwServiceOffering($input: MtwCreateServiceOfferingInput!) {
231
+ createServiceOffering(input: $input) {
232
+ ${OFFERING_FIELDS}
233
+ }
234
+ }
235
+ `;
236
+
237
+ const UPDATE_OFFERING_MUTATION = `
238
+ mutation UpdateMtwServiceOffering($id: ID!, $input: MtwUpdateServiceOfferingInput!) {
239
+ updateServiceOffering(id: $id, input: $input) {
240
+ ${OFFERING_FIELDS}
241
+ }
242
+ }
243
+ `;
244
+
245
+ const DELETE_OFFERING_MUTATION = `
246
+ mutation DeleteMtwServiceOffering($id: ID!) {
247
+ deleteServiceOffering(id: $id)
248
+ }
249
+ `;
250
+
251
+ const ASSIGN_SHIPMENT_MUTATION = `
252
+ mutation AssignMtwShipmentServiceProvider($shipmentId: ID!, $providerId: ID) {
253
+ assignShipmentServiceProvider(shipmentId: $shipmentId, providerId: $providerId) {
254
+ id
255
+ shipmentNumber
256
+ carrierId
257
+ }
258
+ }
259
+ `;
260
+
261
+ function readContact(value: unknown): ProviderContact {
262
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
263
+ const record = value as Record<string, unknown>;
264
+ const pick = (key: string) =>
265
+ typeof record[key] === 'string' && (record[key] as string).trim()
266
+ ? (record[key] as string)
267
+ : undefined;
268
+ return {
269
+ contactName: pick('contactName'),
270
+ email: pick('email'),
271
+ phone: pick('phone'),
272
+ website: pick('website'),
273
+ };
274
+ }
275
+
276
+ function mapProvider(provider: ApiProvider): ServiceProvider {
277
+ return {
278
+ id: provider.id,
279
+ name: provider.name,
280
+ type: provider.type,
281
+ description: provider.description ?? null,
282
+ rating: provider.rating ?? null,
283
+ contact: readContact(provider.contactInfo),
284
+ services: provider.services,
285
+ serviceAreas: provider.serviceAreas,
286
+ isVerified: provider.isVerified,
287
+ isActive: provider.isActive,
288
+ offeringsCount: provider.offeringsCount,
289
+ createdAt: provider.createdAt,
290
+ };
291
+ }
292
+
293
+ function useGraphQLOptions() {
294
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
295
+ useMoveTheWheels();
296
+ return useMemo(
297
+ () => ({
298
+ apiGatewayUrl,
299
+ authToken,
300
+ workspaceToken,
301
+ workspaceId,
302
+ tenantId,
303
+ actorId: currentUser?.id,
304
+ }),
305
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
306
+ );
307
+ }
308
+
309
+ function toError(error: unknown, fallback: string): Error {
310
+ return error instanceof Error ? error : new Error(fallback);
311
+ }
312
+
313
+ export function useServiceProviders(options: {
314
+ filters: ProviderFilters;
315
+ limit: number;
316
+ offset: number;
317
+ }) {
318
+ const graphQLOptions = useGraphQLOptions();
319
+ const { limit, offset } = options;
320
+ const { search, type, isVerified } = options.filters;
321
+ const [providers, setProviders] = useState<ServiceProvider[]>([]);
322
+ const [total, setTotal] = useState(0);
323
+ const [isLoading, setIsLoading] = useState(false);
324
+ const [error, setError] = useState<Error | null>(null);
325
+
326
+ const load = useCallback(async () => {
327
+ setIsLoading(true);
328
+ setError(null);
329
+ try {
330
+ const data = await executeGraphQL<{
331
+ serviceProviders: ApiProvider[];
332
+ serviceProvidersCount: number;
333
+ }>(graphQLOptions, LIST_PROVIDERS_QUERY, {
334
+ filter: {
335
+ search: search?.trim() || undefined,
336
+ type,
337
+ isVerified,
338
+ },
339
+ pagination: { limit, offset },
340
+ });
341
+ setProviders(data.serviceProviders.map(mapProvider));
342
+ setTotal(data.serviceProvidersCount);
343
+ } catch (loadError) {
344
+ setError(toError(loadError, 'Failed to load service providers'));
345
+ } finally {
346
+ setIsLoading(false);
347
+ }
348
+ }, [graphQLOptions, search, type, isVerified, limit, offset]);
349
+
350
+ useEffect(() => {
351
+ void load();
352
+ }, [load]);
353
+
354
+ return { providers, total, isLoading, error, refetch: load };
355
+ }
356
+
357
+ export function useMarketplaceStats(refreshKey: number) {
358
+ const graphQLOptions = useGraphQLOptions();
359
+ const [stats, setStats] = useState<MarketplaceStats | null>(null);
360
+
361
+ useEffect(() => {
362
+ let cancelled = false;
363
+ executeGraphQL<{ marketplaceStats: MarketplaceStats }>(graphQLOptions, MARKETPLACE_STATS_QUERY)
364
+ .then((data) => {
365
+ if (!cancelled) setStats(data.marketplaceStats);
366
+ })
367
+ .catch(() => {
368
+ // Tiles fall back to "—"; the directory itself reports load errors.
369
+ if (!cancelled) setStats(null);
370
+ });
371
+ return () => {
372
+ cancelled = true;
373
+ };
374
+ }, [graphQLOptions, refreshKey]);
375
+
376
+ return stats;
377
+ }
378
+
379
+ export function useProviderOfferings(providerId: string | null, refreshKey: number) {
380
+ const graphQLOptions = useGraphQLOptions();
381
+ const [offerings, setOfferings] = useState<ServiceOffering[]>([]);
382
+ const [isLoading, setIsLoading] = useState(false);
383
+ const [error, setError] = useState<Error | null>(null);
384
+
385
+ useEffect(() => {
386
+ if (!providerId) {
387
+ setOfferings([]);
388
+ return;
389
+ }
390
+ let cancelled = false;
391
+ setIsLoading(true);
392
+ setError(null);
393
+ executeGraphQL<{ serviceOfferings: ServiceOffering[] }>(graphQLOptions, LIST_OFFERINGS_QUERY, {
394
+ filter: { providerId },
395
+ pagination: { limit: 100, offset: 0 },
396
+ })
397
+ .then((data) => {
398
+ if (!cancelled) setOfferings(data.serviceOfferings);
399
+ })
400
+ .catch((loadError: unknown) => {
401
+ if (!cancelled) setError(toError(loadError, 'Failed to load offerings'));
402
+ })
403
+ .finally(() => {
404
+ if (!cancelled) setIsLoading(false);
405
+ });
406
+ return () => {
407
+ cancelled = true;
408
+ };
409
+ }, [graphQLOptions, providerId, refreshKey]);
410
+
411
+ return { offerings, isLoading, error };
412
+ }
413
+
414
+ export interface ShipmentOption {
415
+ id: string;
416
+ shipmentNumber: string;
417
+ status: string;
418
+ }
419
+
420
+ /** Shipments, optionally only those whose carrier is the given provider. */
421
+ export function useShipmentOptions(options: {
422
+ search?: string;
423
+ carrierId?: string;
424
+ limit: number;
425
+ }) {
426
+ const graphQLOptions = useGraphQLOptions();
427
+ const { search, carrierId, limit } = options;
428
+ const [shipments, setShipments] = useState<ShipmentOption[]>([]);
429
+ const [total, setTotal] = useState(0);
430
+ const [isLoading, setIsLoading] = useState(false);
431
+
432
+ const load = useCallback(async () => {
433
+ setIsLoading(true);
434
+ try {
435
+ const data = await executeGraphQL<{ shipments: ShipmentOption[]; shipmentsCount: number }>(
436
+ graphQLOptions,
437
+ PROVIDER_SHIPMENTS_QUERY,
438
+ {
439
+ filter: { search: search?.trim() || undefined, carrierId },
440
+ pagination: { limit, offset: 0 },
441
+ }
442
+ );
443
+ setShipments(data.shipments);
444
+ setTotal(data.shipmentsCount);
445
+ } catch {
446
+ setShipments([]);
447
+ setTotal(0);
448
+ } finally {
449
+ setIsLoading(false);
450
+ }
451
+ }, [graphQLOptions, search, carrierId, limit]);
452
+
453
+ useEffect(() => {
454
+ void load();
455
+ }, [load]);
456
+
457
+ return { shipments, total, isLoading, refetch: load };
458
+ }
459
+
460
+ export function useMarketplaceActions() {
461
+ const graphQLOptions = useGraphQLOptions();
462
+
463
+ return useMemo(
464
+ () => ({
465
+ createProvider: async (input: ProviderInput) => {
466
+ const data = await executeGraphQL<{ createServiceProvider: ApiProvider }>(
467
+ graphQLOptions,
468
+ CREATE_PROVIDER_MUTATION,
469
+ { input }
470
+ );
471
+ return mapProvider(data.createServiceProvider);
472
+ },
473
+ updateProvider: async (id: string, input: Partial<ProviderInput>) => {
474
+ const data = await executeGraphQL<{ updateServiceProvider: ApiProvider }>(
475
+ graphQLOptions,
476
+ UPDATE_PROVIDER_MUTATION,
477
+ { id, input }
478
+ );
479
+ return mapProvider(data.updateServiceProvider);
480
+ },
481
+ deleteProvider: async (id: string) => {
482
+ const data = await executeGraphQL<{ deleteServiceProvider: boolean }>(
483
+ graphQLOptions,
484
+ DELETE_PROVIDER_MUTATION,
485
+ { id }
486
+ );
487
+ return data.deleteServiceProvider;
488
+ },
489
+ createOffering: async (providerId: string, input: OfferingInput) => {
490
+ const data = await executeGraphQL<{ createServiceOffering: ServiceOffering }>(
491
+ graphQLOptions,
492
+ CREATE_OFFERING_MUTATION,
493
+ { input: { ...input, providerId } }
494
+ );
495
+ return data.createServiceOffering;
496
+ },
497
+ updateOffering: async (id: string, input: Partial<OfferingInput>) => {
498
+ const data = await executeGraphQL<{ updateServiceOffering: ServiceOffering }>(
499
+ graphQLOptions,
500
+ UPDATE_OFFERING_MUTATION,
501
+ { id, input }
502
+ );
503
+ return data.updateServiceOffering;
504
+ },
505
+ deleteOffering: async (id: string) => {
506
+ const data = await executeGraphQL<{ deleteServiceOffering: boolean }>(
507
+ graphQLOptions,
508
+ DELETE_OFFERING_MUTATION,
509
+ { id }
510
+ );
511
+ return data.deleteServiceOffering;
512
+ },
513
+ assignShipment: async (shipmentId: string, providerId: string | null) => {
514
+ const data = await executeGraphQL<{
515
+ assignShipmentServiceProvider: {
516
+ id: string;
517
+ shipmentNumber: string;
518
+ carrierId: string | null;
519
+ };
520
+ }>(graphQLOptions, ASSIGN_SHIPMENT_MUTATION, { shipmentId, providerId });
521
+ return data.assignShipmentServiceProvider;
522
+ },
523
+ }),
524
+ [graphQLOptions]
525
+ );
526
+ }
@@ -142,7 +142,7 @@ function ContainerWorkbench({ access }: { access: ReturnType<typeof useShipmentP
142
142
  actual empty return and recorded return acceptance.
143
143
  </p>
144
144
  {actionError && (
145
- <p role="alert" className="mb-4 rounded-md border border-red-400 p-3">
145
+ <p role="alert" className="mb-4 rounded-md border border-status-error-border p-3">
146
146
  {actionError}
147
147
  </p>
148
148
  )}
@@ -178,7 +178,7 @@ function ContainerWorkbench({ access }: { access: ReturnType<typeof useShipmentP
178
178
  setActionError(null);
179
179
  setNotice(null);
180
180
  }}
181
- className={`w-full rounded-md border p-3 text-left ${row.id === data.containerId ? 'border-blue-500' : ''}`}
181
+ className={`w-full rounded-md border p-3 text-left ${row.id === data.containerId ? 'border-action-primary-border' : ''}`}
182
182
  >
183
183
  <span className="font-semibold">{row.containerNumber}</span>
184
184
  <br />
@@ -269,7 +269,7 @@ function ContainerWorkbench({ access }: { access: ReturnType<typeof useShipmentP
269
269
  type="button"
270
270
  aria-pressed={row.id === data.cycleId}
271
271
  disabled={busy}
272
- className={`w-full rounded-md border p-3 text-left ${row.id === data.cycleId ? 'border-blue-500' : ''}`}
272
+ className={`w-full rounded-md border p-3 text-left ${row.id === data.cycleId ? 'border-action-primary-border' : ''}`}
273
273
  onClick={() => {
274
274
  data.selectCycle(row.id);
275
275
  setActionError(null);