@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.
- package/dist/components/CustomerDeliveryUpdatesFields.js +57 -0
- package/dist/components/CustomerNotificationSettingsCard.js +368 -0
- package/dist/components/UpcomingDemandPanel.js +75 -77
- package/dist/generated/tracking-quality-operations.d.ts +275 -0
- package/dist/generated/wspace-operations.d.ts +3 -0
- package/dist/hooks/useCustomers.js +9 -3
- package/dist/hooks/useDemandForecast.js +1 -1
- package/dist/movethewheels/components/CustomerDeliveryUpdatesFields.d.ts +12 -0
- package/dist/movethewheels/components/CustomerNotificationSettingsCard.d.ts +3 -0
- package/dist/movethewheels/hooks/useDemandForecast.d.ts +3 -0
- package/dist/movethewheels/types/index.d.ts +4 -0
- package/dist/movethewheels/utils/customerNotifications.d.ts +66 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/bell-ring.js +22 -0
- package/dist/pages/CustomerDetailsPage.js +279 -231
- package/dist/pages/SettingsPage.js +242 -238
- package/dist/utils/customerNotifications.js +83 -0
- package/package.json +3 -2
- package/src/generated/tracking-quality-operations.ts +304 -0
- package/src/generated/wspace-operations.ts +6 -6
- package/src/movethewheels/components/CustomerDeliveryUpdatesFields.tsx +67 -0
- package/src/movethewheels/components/CustomerNotificationSettingsCard.tsx +465 -0
- package/src/movethewheels/components/UpcomingDemandPanel.tsx +71 -29
- package/src/movethewheels/hooks/useCustomers.ts +17 -0
- package/src/movethewheels/hooks/useDemandForecast.ts +7 -0
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +39 -0
- package/src/movethewheels/pages/SettingsPage.tsx +221 -208
- package/src/movethewheels/types/index.ts +4 -0
- package/src/movethewheels/utils/customerNotifications.ts +209 -0
- 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
|
+
}
|