@burdenoff/microfe-movethewheels 2026.1101.3 → 2026.1102.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/components/CustomerDeliveryUpdatesFields.js +57 -0
  2. package/dist/components/CustomerNotificationSettingsCard.js +368 -0
  3. package/dist/components/UpcomingDemandPanel.js +75 -77
  4. package/dist/generated/tracking-quality-operations.d.ts +275 -0
  5. package/dist/generated/wspace-operations.d.ts +3 -0
  6. package/dist/hooks/useCustomers.js +9 -3
  7. package/dist/hooks/useDemandForecast.js +1 -1
  8. package/dist/movethewheels/components/CustomerDeliveryUpdatesFields.d.ts +12 -0
  9. package/dist/movethewheels/components/CustomerNotificationSettingsCard.d.ts +3 -0
  10. package/dist/movethewheels/hooks/useDemandForecast.d.ts +3 -0
  11. package/dist/movethewheels/types/index.d.ts +4 -0
  12. package/dist/movethewheels/utils/customerNotifications.d.ts +66 -0
  13. package/dist/node_modules/lucide-react/dist/esm/icons/bell-ring.js +22 -0
  14. package/dist/pages/CustomerDetailsPage.js +279 -231
  15. package/dist/pages/SettingsPage.js +242 -238
  16. package/dist/utils/customerNotifications.js +83 -0
  17. package/package.json +3 -2
  18. package/src/generated/tracking-quality-operations.ts +304 -0
  19. package/src/generated/wspace-operations.ts +6 -6
  20. package/src/movethewheels/components/CustomerDeliveryUpdatesFields.tsx +67 -0
  21. package/src/movethewheels/components/CustomerNotificationSettingsCard.tsx +465 -0
  22. package/src/movethewheels/components/UpcomingDemandPanel.tsx +71 -29
  23. package/src/movethewheels/hooks/useCustomers.ts +17 -0
  24. package/src/movethewheels/hooks/useDemandForecast.ts +7 -0
  25. package/src/movethewheels/pages/CustomerDetailsPage.tsx +39 -0
  26. package/src/movethewheels/pages/SettingsPage.tsx +221 -208
  27. package/src/movethewheels/types/index.ts +4 -0
  28. package/src/movethewheels/utils/customerNotifications.ts +209 -0
  29. package/src/operations/wspace/movethewheels/queries/forecast.graphql +3 -0
@@ -397,14 +397,14 @@ export type GetMovethewheelsDemandForecastByLaneQueryVariables = Exact<{
397
397
  }>;
398
398
 
399
399
 
400
- export type GetMovethewheelsDemandForecastByLaneQuery = { demandForecastByLane: Array<{ scopeType: Types.DemandForecastScope, scopeKey: string, periodStart: string, periodEnd: string, predictedVolume: number, predictedRevenue: number, actualVolume: number | null, actualRevenue: number | null, confidence: number }> };
400
+ export type GetMovethewheelsDemandForecastByLaneQuery = { demandForecastByLane: Array<{ scopeType: Types.DemandForecastScope, scopeKey: string, scopeLabel: string, periodStart: string, periodEnd: string, predictedVolume: number, predictedRevenue: number, actualVolume: number | null, actualRevenue: number | null, confidence: number }> };
401
401
 
402
402
  export type GetMovethewheelsDemandForecastByCustomerQueryVariables = Exact<{
403
403
  weeks: number;
404
404
  }>;
405
405
 
406
406
 
407
- export type GetMovethewheelsDemandForecastByCustomerQuery = { demandForecastByCustomer: Array<{ scopeType: Types.DemandForecastScope, scopeKey: string, periodStart: string, periodEnd: string, predictedVolume: number, predictedRevenue: number, actualVolume: number | null, actualRevenue: number | null, confidence: number }> };
407
+ export type GetMovethewheelsDemandForecastByCustomerQuery = { demandForecastByCustomer: Array<{ scopeType: Types.DemandForecastScope, scopeKey: string, scopeLabel: string, periodStart: string, periodEnd: string, predictedVolume: number, predictedRevenue: number, actualVolume: number | null, actualRevenue: number | null, confidence: number }> };
408
408
 
409
409
  export type GetMovethewheelsDemandForecastVarianceQueryVariables = Exact<{
410
410
  scopeType: Types.DemandForecastScope;
@@ -412,7 +412,7 @@ export type GetMovethewheelsDemandForecastVarianceQueryVariables = Exact<{
412
412
  }>;
413
413
 
414
414
 
415
- export type GetMovethewheelsDemandForecastVarianceQuery = { demandForecastVariance: Array<{ scopeType: Types.DemandForecastScope, scopeKey: string, periodStart: string, periodEnd: string, predictedVolume: number, actualVolume: number, volumeVariance: number, predictedRevenue: number, actualRevenue: number, revenueVariance: number }> };
415
+ export type GetMovethewheelsDemandForecastVarianceQuery = { demandForecastVariance: Array<{ scopeType: Types.DemandForecastScope, scopeKey: string, scopeLabel: string, periodStart: string, periodEnd: string, predictedVolume: number, actualVolume: number, volumeVariance: number, predictedRevenue: number, actualRevenue: number, revenueVariance: number }> };
416
416
 
417
417
  export type ListInventoryItemsQueryVariables = Exact<{
418
418
  filter?: Types.InventoryFilterInput | null | undefined;
@@ -549,9 +549,9 @@ export const ListDriversDocument = {"kind":"Document","definitions":[{"kind":"Op
549
549
  export const GetDriverDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDriver"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"driver"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DriverCore"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DriverCore"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MtwDriver"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"employeeId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"licenseNumber"}},{"kind":"Field","name":{"kind":"Name","value":"licenseClass"}},{"kind":"Field","name":{"kind":"Name","value":"licenseExpiry"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"currentLocation"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"vehicleId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetDriverQuery, GetDriverQueryVariables>;
550
550
  export const ListWarehousesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListWarehouses"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WarehouseFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"warehouses"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WarehouseCore"}},{"kind":"Field","name":{"kind":"Name","value":"locations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"locationCode"}},{"kind":"Field","name":{"kind":"Name","value":"zone"}},{"kind":"Field","name":{"kind":"Name","value":"aisle"}},{"kind":"Field","name":{"kind":"Name","value":"rack"}},{"kind":"Field","name":{"kind":"Name","value":"shelf"}},{"kind":"Field","name":{"kind":"Name","value":"bin"}},{"kind":"Field","name":{"kind":"Name","value":"locationType"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WarehouseCore"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MtwWarehouse"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"contactInfo"}},{"kind":"Field","name":{"kind":"Name","value":"operatingHours"}},{"kind":"Field","name":{"kind":"Name","value":"capabilities"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"totalCapacity"}},{"kind":"Field","name":{"kind":"Name","value":"usedCapacity"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<ListWarehousesQuery, ListWarehousesQueryVariables>;
551
551
  export const GetWarehouseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWarehouse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"warehouse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WarehouseCore"}},{"kind":"Field","name":{"kind":"Name","value":"locations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"locationCode"}},{"kind":"Field","name":{"kind":"Name","value":"zone"}},{"kind":"Field","name":{"kind":"Name","value":"aisle"}},{"kind":"Field","name":{"kind":"Name","value":"rack"}},{"kind":"Field","name":{"kind":"Name","value":"shelf"}},{"kind":"Field","name":{"kind":"Name","value":"bin"}},{"kind":"Field","name":{"kind":"Name","value":"locationType"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WarehouseCore"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MtwWarehouse"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"contactInfo"}},{"kind":"Field","name":{"kind":"Name","value":"operatingHours"}},{"kind":"Field","name":{"kind":"Name","value":"capabilities"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"totalCapacity"}},{"kind":"Field","name":{"kind":"Name","value":"usedCapacity"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetWarehouseQuery, GetWarehouseQueryVariables>;
552
- export const GetMovethewheelsDemandForecastByLaneDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMovethewheelsDemandForecastByLane"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"demandForecastByLane"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"weeks"},"value":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopeType"}},{"kind":"Field","name":{"kind":"Name","value":"scopeKey"}},{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"predictedVolume"}},{"kind":"Field","name":{"kind":"Name","value":"predictedRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"actualVolume"}},{"kind":"Field","name":{"kind":"Name","value":"actualRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"confidence"}}]}}]}}]} as unknown as DocumentNode<GetMovethewheelsDemandForecastByLaneQuery, GetMovethewheelsDemandForecastByLaneQueryVariables>;
553
- export const GetMovethewheelsDemandForecastByCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMovethewheelsDemandForecastByCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"demandForecastByCustomer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"weeks"},"value":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopeType"}},{"kind":"Field","name":{"kind":"Name","value":"scopeKey"}},{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"predictedVolume"}},{"kind":"Field","name":{"kind":"Name","value":"predictedRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"actualVolume"}},{"kind":"Field","name":{"kind":"Name","value":"actualRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"confidence"}}]}}]}}]} as unknown as DocumentNode<GetMovethewheelsDemandForecastByCustomerQuery, GetMovethewheelsDemandForecastByCustomerQueryVariables>;
554
- export const GetMovethewheelsDemandForecastVarianceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMovethewheelsDemandForecastVariance"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"scopeType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DemandForecastScope"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"demandForecastVariance"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"scopeType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"scopeType"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopeType"}},{"kind":"Field","name":{"kind":"Name","value":"scopeKey"}},{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"predictedVolume"}},{"kind":"Field","name":{"kind":"Name","value":"actualVolume"}},{"kind":"Field","name":{"kind":"Name","value":"volumeVariance"}},{"kind":"Field","name":{"kind":"Name","value":"predictedRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"actualRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"revenueVariance"}}]}}]}}]} as unknown as DocumentNode<GetMovethewheelsDemandForecastVarianceQuery, GetMovethewheelsDemandForecastVarianceQueryVariables>;
552
+ export const GetMovethewheelsDemandForecastByLaneDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMovethewheelsDemandForecastByLane"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"demandForecastByLane"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"weeks"},"value":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopeType"}},{"kind":"Field","name":{"kind":"Name","value":"scopeKey"}},{"kind":"Field","name":{"kind":"Name","value":"scopeLabel"}},{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"predictedVolume"}},{"kind":"Field","name":{"kind":"Name","value":"predictedRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"actualVolume"}},{"kind":"Field","name":{"kind":"Name","value":"actualRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"confidence"}}]}}]}}]} as unknown as DocumentNode<GetMovethewheelsDemandForecastByLaneQuery, GetMovethewheelsDemandForecastByLaneQueryVariables>;
553
+ export const GetMovethewheelsDemandForecastByCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMovethewheelsDemandForecastByCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"demandForecastByCustomer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"weeks"},"value":{"kind":"Variable","name":{"kind":"Name","value":"weeks"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopeType"}},{"kind":"Field","name":{"kind":"Name","value":"scopeKey"}},{"kind":"Field","name":{"kind":"Name","value":"scopeLabel"}},{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"predictedVolume"}},{"kind":"Field","name":{"kind":"Name","value":"predictedRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"actualVolume"}},{"kind":"Field","name":{"kind":"Name","value":"actualRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"confidence"}}]}}]}}]} as unknown as DocumentNode<GetMovethewheelsDemandForecastByCustomerQuery, GetMovethewheelsDemandForecastByCustomerQueryVariables>;
554
+ export const GetMovethewheelsDemandForecastVarianceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMovethewheelsDemandForecastVariance"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"scopeType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DemandForecastScope"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"demandForecastVariance"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"scopeType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"scopeType"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopeType"}},{"kind":"Field","name":{"kind":"Name","value":"scopeKey"}},{"kind":"Field","name":{"kind":"Name","value":"scopeLabel"}},{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"predictedVolume"}},{"kind":"Field","name":{"kind":"Name","value":"actualVolume"}},{"kind":"Field","name":{"kind":"Name","value":"volumeVariance"}},{"kind":"Field","name":{"kind":"Name","value":"predictedRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"actualRevenue"}},{"kind":"Field","name":{"kind":"Name","value":"revenueVariance"}}]}}]}}]} as unknown as DocumentNode<GetMovethewheelsDemandForecastVarianceQuery, GetMovethewheelsDemandForecastVarianceQueryVariables>;
555
555
  export const ListInventoryItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListInventoryItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"InventoryFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inventoryItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InventoryItemCore"}},{"kind":"Field","name":{"kind":"Name","value":"cargoItem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"category"}}]}},{"kind":"Field","name":{"kind":"Name","value":"warehouse"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InventoryItemCore"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MtwInventoryItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cargoItemId"}},{"kind":"Field","name":{"kind":"Name","value":"warehouseId"}},{"kind":"Field","name":{"kind":"Name","value":"locationId"}},{"kind":"Field","name":{"kind":"Name","value":"onHand"}},{"kind":"Field","name":{"kind":"Name","value":"available"}},{"kind":"Field","name":{"kind":"Name","value":"allocated"}},{"kind":"Field","name":{"kind":"Name","value":"damaged"}},{"kind":"Field","name":{"kind":"Name","value":"unitCost"}},{"kind":"Field","name":{"kind":"Name","value":"reorderPoint"}},{"kind":"Field","name":{"kind":"Name","value":"safetyStock"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<ListInventoryItemsQuery, ListInventoryItemsQueryVariables>;
556
556
  export const GetInventoryStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInventoryStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inventoryStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalOnHand"}},{"kind":"Field","name":{"kind":"Name","value":"totalAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"totalAllocated"}},{"kind":"Field","name":{"kind":"Name","value":"totalDamaged"}},{"kind":"Field","name":{"kind":"Name","value":"lowStockCount"}}]}}]}}]} as unknown as DocumentNode<GetInventoryStatsQuery, GetInventoryStatsQueryVariables>;
557
557
  export const ListRoutesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListRoutes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"RouteFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"routes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RouteCore"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RouteCore"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MtwRoute"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"shipmentId"}},{"kind":"Field","name":{"kind":"Name","value":"vehicleId"}},{"kind":"Field","name":{"kind":"Name","value":"driverId"}},{"kind":"Field","name":{"kind":"Name","value":"waypoints"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedDistance"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedDuration"}},{"kind":"Field","name":{"kind":"Name","value":"actualDistance"}},{"kind":"Field","name":{"kind":"Name","value":"actualDuration"}},{"kind":"Field","name":{"kind":"Name","value":"fuelCost"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<ListRoutesQuery, ListRoutesQueryVariables>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * A customer's delivery notification preferences (BOFF-4402): whether they
3
+ * get shipment updates by email and by SMS. Used by the new and edit customer
4
+ * forms. The workspace must also switch customer notifications on in Settings.
5
+ */
6
+ import { Checkbox, Label } from './ui';
7
+
8
+ export interface DeliveryUpdatesValue {
9
+ notifyByEmail: boolean;
10
+ notifyBySms: boolean;
11
+ }
12
+
13
+ export function CustomerDeliveryUpdatesFields({
14
+ idPrefix,
15
+ value,
16
+ onChange,
17
+ disabled,
18
+ }: {
19
+ idPrefix: string;
20
+ value: DeliveryUpdatesValue;
21
+ onChange: (next: DeliveryUpdatesValue) => void;
22
+ disabled?: boolean;
23
+ }) {
24
+ return (
25
+ <fieldset
26
+ className="space-y-3"
27
+ disabled={disabled}
28
+ data-testid={`${idPrefix}-delivery-updates`}
29
+ >
30
+ <legend className="text-sm font-medium text-text-primary">Delivery updates</legend>
31
+ <div className="flex items-start gap-2">
32
+ <Checkbox
33
+ id={`${idPrefix}-notify-email`}
34
+ checked={value.notifyByEmail}
35
+ disabled={disabled}
36
+ onCheckedChange={(checked) => onChange({ ...value, notifyByEmail: checked === true })}
37
+ />
38
+ <Label htmlFor={`${idPrefix}-notify-email`} className="text-sm font-normal leading-snug">
39
+ Email this customer about their shipments
40
+ </Label>
41
+ </div>
42
+ <div className="flex items-start gap-2">
43
+ <Checkbox
44
+ id={`${idPrefix}-notify-sms`}
45
+ checked={value.notifyBySms}
46
+ disabled={disabled}
47
+ onCheckedChange={(checked) => onChange({ ...value, notifyBySms: checked === true })}
48
+ />
49
+ <Label htmlFor={`${idPrefix}-notify-sms`} className="text-sm font-normal leading-snug">
50
+ Text this customer about their shipments (only with their consent; the phone number needs
51
+ its country code, e.g. +91…)
52
+ </Label>
53
+ </div>
54
+ <p className="text-xs text-text-secondary">
55
+ Sent only while customer delivery notifications are on in Settings.
56
+ </p>
57
+ </fieldset>
58
+ );
59
+ }
60
+
61
+ /** "Email and SMS", "Email", "SMS" or "Off". */
62
+ export function deliveryUpdatesSummary(value: DeliveryUpdatesValue): string {
63
+ if (value.notifyByEmail && value.notifyBySms) return 'Email and SMS';
64
+ if (value.notifyByEmail) return 'Email';
65
+ if (value.notifyBySms) return 'SMS';
66
+ return 'Off';
67
+ }
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Customer delivery notifications on the Settings page (BOFF-4402).
3
+ *
4
+ * Lets a workspace admin switch customer notifications on, pick email and/or
5
+ * SMS and the shipment events that notify, preview what customers receive,
6
+ * and see the most recent notifications with their delivery status. The
7
+ * service owns validation and delivery; this card only edits the settings.
8
+ *
9
+ * The parent keys this card by workspace, so a workspace switch remounts it.
10
+ * The form stays disabled until the saved settings have actually been read:
11
+ * the defaults are a placeholder, never something to edit and save over.
12
+ */
13
+ import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
14
+ import { BellRing, Loader2, RefreshCw, RotateCcw, Save } from 'lucide-react';
15
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
16
+ import { executeGraphQL } from '../utils/graphqlClient';
17
+ import { formatDateTime } from '../utils/formatters';
18
+ import {
19
+ CUSTOMER_NOTIFICATION_EVENTS,
20
+ MTW_CUSTOMER_NOTIFICATIONS_QUERY,
21
+ MTW_CUSTOMER_NOTIFICATION_PREVIEWS_QUERY,
22
+ MTW_CUSTOMER_NOTIFICATION_SETTINGS_QUERY,
23
+ STATUS_LABELS,
24
+ UPDATE_MTW_CUSTOMER_NOTIFICATION_SETTINGS_MUTATION,
25
+ changedNotificationSettings,
26
+ defaultNotificationSettings,
27
+ eventLabel,
28
+ notificationDraftError,
29
+ toggleEvent,
30
+ type MtwCustomerNotification,
31
+ type MtwCustomerNotificationEvent,
32
+ type MtwCustomerNotificationPreview,
33
+ type MtwCustomerNotificationSettings,
34
+ type MtwCustomerNotificationStatus,
35
+ } from '../utils/customerNotifications';
36
+ import { FormError } from './FormError';
37
+ import { Notice, useNotice } from './Notice';
38
+ import {
39
+ Badge,
40
+ Button,
41
+ Card,
42
+ CardContent,
43
+ CardDescription,
44
+ CardHeader,
45
+ CardTitle,
46
+ Checkbox,
47
+ Label,
48
+ Skeleton,
49
+ Switch,
50
+ } from './ui';
51
+
52
+ const RECENT_LIMIT = 10;
53
+
54
+ type Draft = Pick<
55
+ MtwCustomerNotificationSettings,
56
+ 'enabled' | 'emailEnabled' | 'smsEnabled' | 'events'
57
+ >;
58
+
59
+ const toDraft = (settings: MtwCustomerNotificationSettings): Draft => ({
60
+ enabled: settings.enabled,
61
+ emailEnabled: settings.emailEnabled,
62
+ smsEnabled: settings.smsEnabled,
63
+ events: [...settings.events],
64
+ });
65
+
66
+ const STATUS_VARIANT: Record<
67
+ MtwCustomerNotificationStatus,
68
+ 'default' | 'secondary' | 'outline' | 'destructive'
69
+ > = {
70
+ SENT: 'default',
71
+ PENDING: 'secondary',
72
+ SKIPPED: 'outline',
73
+ FAILED: 'destructive',
74
+ };
75
+
76
+ export function CustomerNotificationSettingsCard({ readOnly }: { readOnly: boolean }) {
77
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
78
+ useMoveTheWheels();
79
+ const graphQLOptions = useMemo(
80
+ () => ({
81
+ apiGatewayUrl,
82
+ authToken,
83
+ workspaceToken,
84
+ workspaceId,
85
+ tenantId,
86
+ actorId: currentUser?.id,
87
+ }),
88
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
89
+ );
90
+
91
+ const [settings, setSettings] = useState<MtwCustomerNotificationSettings>(() =>
92
+ defaultNotificationSettings(workspaceId ?? '')
93
+ );
94
+ const [draft, setDraft] = useState<Draft>(() => toDraft(settings));
95
+ const [isLoaded, setIsLoaded] = useState(false);
96
+ const [loadError, setLoadError] = useState<string | null>(null);
97
+ const [isSaving, setIsSaving] = useState(false);
98
+ const [formError, setFormError] = useState<string | null>(null);
99
+ const { message: notice, notify } = useNotice();
100
+ const [recent, setRecent] = useState<MtwCustomerNotification[]>([]);
101
+ const [recentTotal, setRecentTotal] = useState(0);
102
+ const [recentError, setRecentError] = useState<string | null>(null);
103
+ // Starts true so the empty state does not flash before the first fetch resolves.
104
+ const [isLoadingRecent, setIsLoadingRecent] = useState(true);
105
+ const [previews, setPreviews] = useState<MtwCustomerNotificationPreview[]>([]);
106
+ const [previewEvent, setPreviewEvent] = useState<MtwCustomerNotificationEvent>('BOOKED');
107
+ const [loadAttempt, setLoadAttempt] = useState(0);
108
+ const recentRequest = useRef(0);
109
+
110
+ useEffect(() => {
111
+ if (!graphQLOptions.workspaceId) return;
112
+ let cancelled = false;
113
+ setIsLoaded(false);
114
+ executeGraphQL<{ mtwCustomerNotificationSettings: MtwCustomerNotificationSettings }>(
115
+ graphQLOptions,
116
+ MTW_CUSTOMER_NOTIFICATION_SETTINGS_QUERY
117
+ )
118
+ .then((data) => {
119
+ if (cancelled) return;
120
+ setSettings(data.mtwCustomerNotificationSettings);
121
+ setDraft(toDraft(data.mtwCustomerNotificationSettings));
122
+ setLoadError(null);
123
+ setIsLoaded(true);
124
+ })
125
+ .catch((caught: unknown) => {
126
+ if (!cancelled)
127
+ setLoadError(caught instanceof Error ? caught.message : 'Could not load the settings');
128
+ });
129
+ executeGraphQL<{ mtwCustomerNotificationPreviews: MtwCustomerNotificationPreview[] }>(
130
+ graphQLOptions,
131
+ MTW_CUSTOMER_NOTIFICATION_PREVIEWS_QUERY
132
+ )
133
+ .then((data) => {
134
+ if (!cancelled) setPreviews(data.mtwCustomerNotificationPreviews);
135
+ })
136
+ .catch(() => {
137
+ // Previews are a convenience; the settings still work without them.
138
+ });
139
+ return () => {
140
+ cancelled = true;
141
+ };
142
+ }, [graphQLOptions, loadAttempt]);
143
+
144
+ // Only the newest request may write the log: a slower, older response (or
145
+ // one that lands after unmount) is dropped.
146
+ const loadRecent = useCallback(async () => {
147
+ if (!graphQLOptions.workspaceId) {
148
+ setIsLoadingRecent(false);
149
+ return;
150
+ }
151
+ const request = ++recentRequest.current;
152
+ setIsLoadingRecent(true);
153
+ try {
154
+ const data = await executeGraphQL<{
155
+ mtwCustomerNotifications: { items: MtwCustomerNotification[]; total: number };
156
+ }>(graphQLOptions, MTW_CUSTOMER_NOTIFICATIONS_QUERY, {
157
+ pagination: { limit: RECENT_LIMIT, offset: 0 },
158
+ });
159
+ if (request !== recentRequest.current) return;
160
+ setRecent(data.mtwCustomerNotifications.items);
161
+ setRecentTotal(data.mtwCustomerNotifications.total);
162
+ setRecentError(null);
163
+ } catch (caught) {
164
+ if (request !== recentRequest.current) return;
165
+ setRecentError(caught instanceof Error ? caught.message : 'Could not load notifications');
166
+ } finally {
167
+ if (request === recentRequest.current) setIsLoadingRecent(false);
168
+ }
169
+ }, [graphQLOptions]);
170
+
171
+ useEffect(() => {
172
+ void loadRecent();
173
+ return () => {
174
+ // Invalidate whatever is in flight when the options change or the card unmounts.
175
+ recentRequest.current += 1;
176
+ };
177
+ }, [loadRecent]);
178
+
179
+ const changes = useMemo(() => changedNotificationSettings(settings, draft), [settings, draft]);
180
+ const draftError = notificationDraftError(draft);
181
+ const isDirty = Object.keys(changes).length > 0;
182
+ const canSave = isLoaded && !readOnly && !isSaving && isDirty && !draftError;
183
+ const disabled = readOnly || isSaving || !isLoaded;
184
+
185
+ const update = (next: Partial<Draft>) => {
186
+ setFormError(null);
187
+ setDraft((previous) => ({ ...previous, ...next }));
188
+ };
189
+
190
+ const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
191
+ event.preventDefault();
192
+ if (!canSave) return;
193
+ setIsSaving(true);
194
+ setFormError(null);
195
+ try {
196
+ const data = await executeGraphQL<{
197
+ updateMtwCustomerNotificationSettings: MtwCustomerNotificationSettings;
198
+ }>(
199
+ graphQLOptions,
200
+ UPDATE_MTW_CUSTOMER_NOTIFICATION_SETTINGS_MUTATION,
201
+ { input: changes },
202
+ 'SINGLE_ATTEMPT'
203
+ );
204
+ const saved = data.updateMtwCustomerNotificationSettings;
205
+ setSettings(saved);
206
+ setDraft(toDraft(saved));
207
+ notify(
208
+ saved.enabled
209
+ ? 'Customer notifications are on. Customers hear about shipment changes from now on.'
210
+ : 'Customer notifications are off.'
211
+ );
212
+ } catch (caught) {
213
+ setFormError(caught instanceof Error ? caught.message : 'Could not save the settings');
214
+ } finally {
215
+ setIsSaving(false);
216
+ }
217
+ };
218
+
219
+ const preview = previews.find((item) => item.event === previewEvent);
220
+
221
+ return (
222
+ <Card data-testid="mtw-customer-notifications">
223
+ <CardHeader>
224
+ <div className="flex flex-wrap items-center justify-between gap-3">
225
+ <CardTitle className="flex items-center gap-2 text-base">
226
+ <BellRing size={18} /> Customer delivery notifications
227
+ </CardTitle>
228
+ {isLoaded && (
229
+ <Badge variant={settings.enabled ? 'default' : 'outline'}>
230
+ {settings.enabled ? 'On' : 'Off'}
231
+ </Badge>
232
+ )}
233
+ </div>
234
+ <CardDescription>
235
+ Email or text your customers when their shipment is booked, picked up, out for delivery,
236
+ delivered, running late or could not be delivered. Each customer can opt out on their
237
+ customer record; SMS goes only to customers who agreed to texts.
238
+ </CardDescription>
239
+ </CardHeader>
240
+ <CardContent className="space-y-6">
241
+ {loadError && (
242
+ <div className="space-y-3">
243
+ <FormError
244
+ message={`Could not load the customer notification settings (${loadError}). They cannot be changed until they load.`}
245
+ />
246
+ <Button
247
+ type="button"
248
+ variant="outline"
249
+ size="sm"
250
+ onClick={() => {
251
+ setLoadError(null);
252
+ setLoadAttempt((attempt) => attempt + 1);
253
+ }}
254
+ >
255
+ <RefreshCw size={14} className="mr-2" /> Try again
256
+ </Button>
257
+ </div>
258
+ )}
259
+ <Notice message={notice} />
260
+
261
+ {!isLoaded ? (
262
+ loadError ? null : (
263
+ <div className="space-y-3" aria-busy="true">
264
+ <Skeleton className="h-8 w-full" />
265
+ <Skeleton className="h-24 w-full" />
266
+ </div>
267
+ )
268
+ ) : (
269
+ <form
270
+ className="space-y-6"
271
+ onSubmit={(event) => void onSubmit(event)}
272
+ aria-label="Customer notification settings"
273
+ >
274
+ {readOnly && (
275
+ <p className="rounded-lg border border-border-default bg-surface-secondary px-4 py-3 text-sm text-text-secondary">
276
+ You can view these settings. Ask a workspace admin or the Logistics Director to
277
+ change them.
278
+ </p>
279
+ )}
280
+ <div className="flex items-start justify-between gap-4 rounded-lg border border-border-default px-4 py-3">
281
+ <div className="min-w-0">
282
+ <Label htmlFor="mtw-customer-notify-enabled" className="text-sm font-medium">
283
+ Notify customers about their shipments
284
+ </Label>
285
+ <p className="text-xs text-text-secondary">
286
+ Only changes made after you switch this on notify; customers are never mailed
287
+ about shipments that already happened.
288
+ {settings.enabledAt && settings.enabled
289
+ ? ` On since ${formatDateTime(settings.enabledAt)}.`
290
+ : ''}
291
+ </p>
292
+ </div>
293
+ <Switch
294
+ id="mtw-customer-notify-enabled"
295
+ checked={draft.enabled}
296
+ disabled={disabled}
297
+ onCheckedChange={(checked: boolean) => update({ enabled: checked })}
298
+ />
299
+ </div>
300
+
301
+ <fieldset className="space-y-3" disabled={disabled}>
302
+ <legend className="text-sm font-semibold text-text-primary">Channels</legend>
303
+ <div className="flex flex-wrap gap-6">
304
+ <div className="flex items-center gap-2">
305
+ <Checkbox
306
+ id="mtw-customer-notify-email"
307
+ checked={draft.emailEnabled}
308
+ disabled={disabled}
309
+ onCheckedChange={(checked) => update({ emailEnabled: checked === true })}
310
+ />
311
+ <Label htmlFor="mtw-customer-notify-email" className="text-sm font-normal">
312
+ Email
313
+ </Label>
314
+ </div>
315
+ <div className="flex items-center gap-2">
316
+ <Checkbox
317
+ id="mtw-customer-notify-sms"
318
+ checked={draft.smsEnabled}
319
+ disabled={disabled}
320
+ onCheckedChange={(checked) => update({ smsEnabled: checked === true })}
321
+ />
322
+ <Label htmlFor="mtw-customer-notify-sms" className="text-sm font-normal">
323
+ SMS (phone numbers with a country code, e.g. +91…)
324
+ </Label>
325
+ </div>
326
+ </div>
327
+ <p className="text-xs text-text-secondary">
328
+ Texts are sent through the SMS channel configured for your organization. Without
329
+ one, SMS sends are recorded as failed in the recent notifications below.
330
+ </p>
331
+ </fieldset>
332
+
333
+ <fieldset className="space-y-3" disabled={disabled}>
334
+ <legend className="text-sm font-semibold text-text-primary">Events</legend>
335
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
336
+ {CUSTOMER_NOTIFICATION_EVENTS.map((option) => {
337
+ const id = `mtw-customer-notify-event-${option.value}`;
338
+ return (
339
+ <div key={option.value} className="flex items-start gap-2">
340
+ <Checkbox
341
+ id={id}
342
+ checked={draft.events.includes(option.value)}
343
+ disabled={disabled}
344
+ onCheckedChange={(checked) =>
345
+ update({
346
+ events: toggleEvent(draft.events, option.value, checked === true),
347
+ })
348
+ }
349
+ />
350
+ <Label htmlFor={id} className="text-sm font-normal leading-snug">
351
+ <span className="block font-medium text-text-primary">{option.label}</span>
352
+ <span className="block text-xs text-text-secondary">
353
+ {option.description}
354
+ </span>
355
+ </Label>
356
+ </div>
357
+ );
358
+ })}
359
+ </div>
360
+ </fieldset>
361
+
362
+ {draftError && <p className="text-xs text-status-error-text">{draftError}</p>}
363
+ <FormError message={formError} />
364
+
365
+ <div className="flex flex-wrap items-center justify-end gap-3">
366
+ <Button
367
+ type="button"
368
+ variant="outline"
369
+ disabled={!isDirty || isSaving}
370
+ onClick={() => {
371
+ setFormError(null);
372
+ setDraft(toDraft(settings));
373
+ }}
374
+ >
375
+ <RotateCcw size={16} className="mr-2" /> Discard changes
376
+ </Button>
377
+ <Button type="submit" disabled={!canSave}>
378
+ {isSaving ? (
379
+ <Loader2 size={16} className="mr-2 animate-spin" />
380
+ ) : (
381
+ <Save size={16} className="mr-2" />
382
+ )}
383
+ Save notification settings
384
+ </Button>
385
+ </div>
386
+ </form>
387
+ )}
388
+
389
+ {previews.length > 0 && (
390
+ <div className="space-y-3 border-t border-border-default pt-4">
391
+ <div className="flex flex-wrap items-center gap-2">
392
+ <span className="text-sm font-semibold text-text-primary">Preview</span>
393
+ {CUSTOMER_NOTIFICATION_EVENTS.map((option) => (
394
+ <Button
395
+ key={option.value}
396
+ type="button"
397
+ size="sm"
398
+ variant={previewEvent === option.value ? 'default' : 'outline'}
399
+ onClick={() => setPreviewEvent(option.value)}
400
+ >
401
+ {option.label}
402
+ </Button>
403
+ ))}
404
+ </div>
405
+ {preview && (
406
+ <div
407
+ className="space-y-2 rounded-lg bg-surface-secondary px-4 py-3 text-sm"
408
+ data-testid="mtw-customer-notify-preview"
409
+ >
410
+ <p className="font-medium text-text-primary">{preview.subject}</p>
411
+ <p className="whitespace-pre-line text-text-secondary">{preview.emailText}</p>
412
+ <p className="text-xs text-text-tertiary">SMS: {preview.smsText}</p>
413
+ </div>
414
+ )}
415
+ </div>
416
+ )}
417
+
418
+ <div className="space-y-3 border-t border-border-default pt-4">
419
+ <div className="flex flex-wrap items-center justify-between gap-2">
420
+ <span className="text-sm font-semibold text-text-primary">
421
+ Recent notifications{recentTotal > 0 ? ` (${recentTotal})` : ''}
422
+ </span>
423
+ <Button
424
+ type="button"
425
+ size="sm"
426
+ variant="outline"
427
+ disabled={isLoadingRecent}
428
+ onClick={() => void loadRecent()}
429
+ >
430
+ <RefreshCw size={14} className="mr-2" /> Refresh
431
+ </Button>
432
+ </div>
433
+ {recentError && <FormError message={recentError} />}
434
+ {!recentError && recent.length === 0 && !isLoadingRecent && (
435
+ <p className="text-sm text-text-secondary">
436
+ No customer notifications yet. They appear here as shipments change.
437
+ </p>
438
+ )}
439
+ {recent.length > 0 && (
440
+ <ul className="divide-y divide-border-default" data-testid="mtw-customer-notify-recent">
441
+ {recent.map((item) => (
442
+ <li key={item.id} className="flex flex-wrap items-start justify-between gap-2 py-2">
443
+ <div className="min-w-0">
444
+ <p className="text-sm font-medium text-text-primary">
445
+ {eventLabel(item.event)} · {item.shipmentNumber ?? item.shipmentId}
446
+ </p>
447
+ <p className="text-xs text-text-secondary">
448
+ {item.channel === 'EMAIL' ? 'Email' : 'SMS'}
449
+ {item.recipient ? ` to ${item.recipient}` : ''} ·{' '}
450
+ {formatDateTime(item.sentAt ?? item.createdAt)}
451
+ </p>
452
+ {item.lastError && item.status !== 'SENT' && (
453
+ <p className="text-xs text-text-tertiary">{item.lastError}</p>
454
+ )}
455
+ </div>
456
+ <Badge variant={STATUS_VARIANT[item.status]}>{STATUS_LABELS[item.status]}</Badge>
457
+ </li>
458
+ ))}
459
+ </ul>
460
+ )}
461
+ </div>
462
+ </CardContent>
463
+ </Card>
464
+ );
465
+ }