@burdenoff/microfe-movethewheels 2026.804.1 → 2026.822.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/CarbonFootprintPanel.js +108 -0
- package/dist/components/UpcomingDemandPanel.js +184 -0
- package/dist/components/index.js +1 -0
- package/dist/generated/wspace-operations.d.ts +147 -0
- package/dist/generated/wspace-types.d.ts +63 -0
- package/dist/hooks/useAnalytics.js +45 -4
- package/dist/hooks/useDemandForecast.js +54 -0
- package/dist/index.js +29 -28
- package/dist/movethewheels/components/CarbonFootprintPanel.d.ts +7 -0
- package/dist/movethewheels/components/UpcomingDemandPanel.d.ts +13 -0
- package/dist/movethewheels/components/index.d.ts +1 -0
- package/dist/movethewheels/hooks/index.d.ts +1 -1
- package/dist/movethewheels/hooks/useAnalytics.d.ts +22 -0
- package/dist/movethewheels/hooks/useDemandForecast.d.ts +39 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/leaf.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/trending-down.js +11 -0
- package/dist/pages/AnalyticsPage.js +82 -69
- package/package.json +2 -2
- package/src/generated/wspace-operations.ts +177 -0
- package/src/generated/wspace-types.ts +74 -0
- package/src/movethewheels/components/CarbonFootprintPanel.tsx +133 -0
- package/src/movethewheels/components/UpcomingDemandPanel.tsx +237 -0
- package/src/movethewheels/components/index.ts +3 -0
- package/src/movethewheels/hooks/index.ts +2 -0
- package/src/movethewheels/hooks/useAnalytics.ts +86 -0
- package/src/movethewheels/hooks/useDemandForecast.ts +161 -0
- package/src/movethewheels/pages/AnalyticsPage.tsx +27 -0
- package/src/operations/wspace/movethewheels/queries/forecast.graphql +47 -0
|
@@ -462,6 +462,28 @@ export type GetWarehouseQueryVariables = Types.Exact<{
|
|
|
462
462
|
|
|
463
463
|
export type GetWarehouseQuery = { __typename?: 'Query', warehouse?: { __typename?: 'Warehouse', id: string, name: string, code: string, type: Types.WarehouseType, address: Record<string, unknown>, contactInfo?: Record<string, unknown> | null, operatingHours?: Record<string, unknown> | null, capabilities?: Record<string, unknown> | null, status: string, totalCapacity?: number | null, usedCapacity?: number | null, createdAt: string, updatedAt: string, locations: Array<{ __typename?: 'WarehouseLocation', id: string, locationCode: string, zone?: string | null, aisle?: string | null, rack?: string | null, shelf?: string | null, bin?: string | null, locationType?: string | null, capacity?: number | null, status: string }> } | null };
|
|
464
464
|
|
|
465
|
+
export type GetMovethewheelsDemandForecastByLaneQueryVariables = Types.Exact<{
|
|
466
|
+
weeks: Types.Scalars['Int']['input'];
|
|
467
|
+
}>;
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
export type GetMovethewheelsDemandForecastByLaneQuery = { __typename?: 'Query', demandForecastByLane: Array<{ __typename?: 'MtwDemandForecastPoint', scopeType: Types.DemandForecastScope, scopeKey: string, periodStart: string, periodEnd: string, predictedVolume: number, predictedRevenue: number, actualVolume?: number | null, actualRevenue?: number | null, confidence: number }> };
|
|
471
|
+
|
|
472
|
+
export type GetMovethewheelsDemandForecastByCustomerQueryVariables = Types.Exact<{
|
|
473
|
+
weeks: Types.Scalars['Int']['input'];
|
|
474
|
+
}>;
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
export type GetMovethewheelsDemandForecastByCustomerQuery = { __typename?: 'Query', demandForecastByCustomer: Array<{ __typename?: 'MtwDemandForecastPoint', scopeType: Types.DemandForecastScope, scopeKey: string, periodStart: string, periodEnd: string, predictedVolume: number, predictedRevenue: number, actualVolume?: number | null, actualRevenue?: number | null, confidence: number }> };
|
|
478
|
+
|
|
479
|
+
export type GetMovethewheelsDemandForecastVarianceQueryVariables = Types.Exact<{
|
|
480
|
+
scopeType: Types.DemandForecastScope;
|
|
481
|
+
limit: Types.Scalars['Int']['input'];
|
|
482
|
+
}>;
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
export type GetMovethewheelsDemandForecastVarianceQuery = { __typename?: 'Query', demandForecastVariance: Array<{ __typename?: 'MtwDemandForecastVariance', scopeType: Types.DemandForecastScope, scopeKey: string, periodStart: string, periodEnd: string, predictedVolume: number, actualVolume: number, volumeVariance: number, predictedRevenue: number, actualRevenue: number, revenueVariance: number }> };
|
|
486
|
+
|
|
465
487
|
export type ListInventoryItemsQueryVariables = Types.Exact<{
|
|
466
488
|
filter?: Types.InputMaybe<Types.InventoryFilterInput>;
|
|
467
489
|
pagination?: Types.InputMaybe<Types.PaginationInput>;
|
|
@@ -2918,6 +2940,161 @@ export type GetWarehouseQueryHookResult = ReturnType<typeof useGetWarehouseQuery
|
|
|
2918
2940
|
export type GetWarehouseLazyQueryHookResult = ReturnType<typeof useGetWarehouseLazyQuery>;
|
|
2919
2941
|
export type GetWarehouseSuspenseQueryHookResult = ReturnType<typeof useGetWarehouseSuspenseQuery>;
|
|
2920
2942
|
export type GetWarehouseQueryResult = Apollo.QueryResult<Types.GetWarehouseQuery, Types.GetWarehouseQueryVariables>;
|
|
2943
|
+
export const GetMovethewheelsDemandForecastByLaneDocument = gql`
|
|
2944
|
+
query GetMovethewheelsDemandForecastByLane($weeks: Int!) {
|
|
2945
|
+
demandForecastByLane(weeks: $weeks) {
|
|
2946
|
+
scopeType
|
|
2947
|
+
scopeKey
|
|
2948
|
+
periodStart
|
|
2949
|
+
periodEnd
|
|
2950
|
+
predictedVolume
|
|
2951
|
+
predictedRevenue
|
|
2952
|
+
actualVolume
|
|
2953
|
+
actualRevenue
|
|
2954
|
+
confidence
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
`;
|
|
2958
|
+
|
|
2959
|
+
/**
|
|
2960
|
+
* __useGetMovethewheelsDemandForecastByLaneQuery__
|
|
2961
|
+
*
|
|
2962
|
+
* To run a query within a React component, call `useGetMovethewheelsDemandForecastByLaneQuery` and pass it any options that fit your needs.
|
|
2963
|
+
* When your component renders, `useGetMovethewheelsDemandForecastByLaneQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
2964
|
+
* you can use to render your UI.
|
|
2965
|
+
*
|
|
2966
|
+
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
2967
|
+
*
|
|
2968
|
+
* @example
|
|
2969
|
+
* const { data, loading, error } = useGetMovethewheelsDemandForecastByLaneQuery({
|
|
2970
|
+
* variables: {
|
|
2971
|
+
* weeks: // value for 'weeks'
|
|
2972
|
+
* },
|
|
2973
|
+
* });
|
|
2974
|
+
*/
|
|
2975
|
+
export function useGetMovethewheelsDemandForecastByLaneQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables> & ({ variables: Types.GetMovethewheelsDemandForecastByLaneQueryVariables; skip?: boolean; } | { skip: boolean; }) ) {
|
|
2976
|
+
const options = {...defaultOptions, ...baseOptions}
|
|
2977
|
+
return ApolloReactHooks.useQuery<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>(GetMovethewheelsDemandForecastByLaneDocument, options);
|
|
2978
|
+
}
|
|
2979
|
+
export function useGetMovethewheelsDemandForecastByLaneLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>) {
|
|
2980
|
+
const options = {...defaultOptions, ...baseOptions}
|
|
2981
|
+
return ApolloReactHooks.useLazyQuery<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>(GetMovethewheelsDemandForecastByLaneDocument, options);
|
|
2982
|
+
}
|
|
2983
|
+
// @ts-ignore
|
|
2984
|
+
export function useGetMovethewheelsDemandForecastByLaneSuspenseQuery(baseOptions?: ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>;
|
|
2985
|
+
export function useGetMovethewheelsDemandForecastByLaneSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<Types.GetMovethewheelsDemandForecastByLaneQuery | undefined, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>;
|
|
2986
|
+
export function useGetMovethewheelsDemandForecastByLaneSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>) {
|
|
2987
|
+
const options = baseOptions === ApolloReactHooks.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
|
|
2988
|
+
return ApolloReactHooks.useSuspenseQuery<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>(GetMovethewheelsDemandForecastByLaneDocument, options);
|
|
2989
|
+
}
|
|
2990
|
+
export type GetMovethewheelsDemandForecastByLaneQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastByLaneQuery>;
|
|
2991
|
+
export type GetMovethewheelsDemandForecastByLaneLazyQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastByLaneLazyQuery>;
|
|
2992
|
+
export type GetMovethewheelsDemandForecastByLaneSuspenseQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastByLaneSuspenseQuery>;
|
|
2993
|
+
export type GetMovethewheelsDemandForecastByLaneQueryResult = Apollo.QueryResult<Types.GetMovethewheelsDemandForecastByLaneQuery, Types.GetMovethewheelsDemandForecastByLaneQueryVariables>;
|
|
2994
|
+
export const GetMovethewheelsDemandForecastByCustomerDocument = gql`
|
|
2995
|
+
query GetMovethewheelsDemandForecastByCustomer($weeks: Int!) {
|
|
2996
|
+
demandForecastByCustomer(weeks: $weeks) {
|
|
2997
|
+
scopeType
|
|
2998
|
+
scopeKey
|
|
2999
|
+
periodStart
|
|
3000
|
+
periodEnd
|
|
3001
|
+
predictedVolume
|
|
3002
|
+
predictedRevenue
|
|
3003
|
+
actualVolume
|
|
3004
|
+
actualRevenue
|
|
3005
|
+
confidence
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
`;
|
|
3009
|
+
|
|
3010
|
+
/**
|
|
3011
|
+
* __useGetMovethewheelsDemandForecastByCustomerQuery__
|
|
3012
|
+
*
|
|
3013
|
+
* To run a query within a React component, call `useGetMovethewheelsDemandForecastByCustomerQuery` and pass it any options that fit your needs.
|
|
3014
|
+
* When your component renders, `useGetMovethewheelsDemandForecastByCustomerQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
3015
|
+
* you can use to render your UI.
|
|
3016
|
+
*
|
|
3017
|
+
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
3018
|
+
*
|
|
3019
|
+
* @example
|
|
3020
|
+
* const { data, loading, error } = useGetMovethewheelsDemandForecastByCustomerQuery({
|
|
3021
|
+
* variables: {
|
|
3022
|
+
* weeks: // value for 'weeks'
|
|
3023
|
+
* },
|
|
3024
|
+
* });
|
|
3025
|
+
*/
|
|
3026
|
+
export function useGetMovethewheelsDemandForecastByCustomerQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables> & ({ variables: Types.GetMovethewheelsDemandForecastByCustomerQueryVariables; skip?: boolean; } | { skip: boolean; }) ) {
|
|
3027
|
+
const options = {...defaultOptions, ...baseOptions}
|
|
3028
|
+
return ApolloReactHooks.useQuery<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>(GetMovethewheelsDemandForecastByCustomerDocument, options);
|
|
3029
|
+
}
|
|
3030
|
+
export function useGetMovethewheelsDemandForecastByCustomerLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>) {
|
|
3031
|
+
const options = {...defaultOptions, ...baseOptions}
|
|
3032
|
+
return ApolloReactHooks.useLazyQuery<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>(GetMovethewheelsDemandForecastByCustomerDocument, options);
|
|
3033
|
+
}
|
|
3034
|
+
// @ts-ignore
|
|
3035
|
+
export function useGetMovethewheelsDemandForecastByCustomerSuspenseQuery(baseOptions?: ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>;
|
|
3036
|
+
export function useGetMovethewheelsDemandForecastByCustomerSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<Types.GetMovethewheelsDemandForecastByCustomerQuery | undefined, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>;
|
|
3037
|
+
export function useGetMovethewheelsDemandForecastByCustomerSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>) {
|
|
3038
|
+
const options = baseOptions === ApolloReactHooks.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
|
|
3039
|
+
return ApolloReactHooks.useSuspenseQuery<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>(GetMovethewheelsDemandForecastByCustomerDocument, options);
|
|
3040
|
+
}
|
|
3041
|
+
export type GetMovethewheelsDemandForecastByCustomerQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastByCustomerQuery>;
|
|
3042
|
+
export type GetMovethewheelsDemandForecastByCustomerLazyQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastByCustomerLazyQuery>;
|
|
3043
|
+
export type GetMovethewheelsDemandForecastByCustomerSuspenseQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastByCustomerSuspenseQuery>;
|
|
3044
|
+
export type GetMovethewheelsDemandForecastByCustomerQueryResult = Apollo.QueryResult<Types.GetMovethewheelsDemandForecastByCustomerQuery, Types.GetMovethewheelsDemandForecastByCustomerQueryVariables>;
|
|
3045
|
+
export const GetMovethewheelsDemandForecastVarianceDocument = gql`
|
|
3046
|
+
query GetMovethewheelsDemandForecastVariance($scopeType: DemandForecastScope!, $limit: Int!) {
|
|
3047
|
+
demandForecastVariance(scopeType: $scopeType, limit: $limit) {
|
|
3048
|
+
scopeType
|
|
3049
|
+
scopeKey
|
|
3050
|
+
periodStart
|
|
3051
|
+
periodEnd
|
|
3052
|
+
predictedVolume
|
|
3053
|
+
actualVolume
|
|
3054
|
+
volumeVariance
|
|
3055
|
+
predictedRevenue
|
|
3056
|
+
actualRevenue
|
|
3057
|
+
revenueVariance
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
`;
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* __useGetMovethewheelsDemandForecastVarianceQuery__
|
|
3064
|
+
*
|
|
3065
|
+
* To run a query within a React component, call `useGetMovethewheelsDemandForecastVarianceQuery` and pass it any options that fit your needs.
|
|
3066
|
+
* When your component renders, `useGetMovethewheelsDemandForecastVarianceQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
3067
|
+
* you can use to render your UI.
|
|
3068
|
+
*
|
|
3069
|
+
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
3070
|
+
*
|
|
3071
|
+
* @example
|
|
3072
|
+
* const { data, loading, error } = useGetMovethewheelsDemandForecastVarianceQuery({
|
|
3073
|
+
* variables: {
|
|
3074
|
+
* scopeType: // value for 'scopeType'
|
|
3075
|
+
* limit: // value for 'limit'
|
|
3076
|
+
* },
|
|
3077
|
+
* });
|
|
3078
|
+
*/
|
|
3079
|
+
export function useGetMovethewheelsDemandForecastVarianceQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables> & ({ variables: Types.GetMovethewheelsDemandForecastVarianceQueryVariables; skip?: boolean; } | { skip: boolean; }) ) {
|
|
3080
|
+
const options = {...defaultOptions, ...baseOptions}
|
|
3081
|
+
return ApolloReactHooks.useQuery<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>(GetMovethewheelsDemandForecastVarianceDocument, options);
|
|
3082
|
+
}
|
|
3083
|
+
export function useGetMovethewheelsDemandForecastVarianceLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>) {
|
|
3084
|
+
const options = {...defaultOptions, ...baseOptions}
|
|
3085
|
+
return ApolloReactHooks.useLazyQuery<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>(GetMovethewheelsDemandForecastVarianceDocument, options);
|
|
3086
|
+
}
|
|
3087
|
+
// @ts-ignore
|
|
3088
|
+
export function useGetMovethewheelsDemandForecastVarianceSuspenseQuery(baseOptions?: ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>;
|
|
3089
|
+
export function useGetMovethewheelsDemandForecastVarianceSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<Types.GetMovethewheelsDemandForecastVarianceQuery | undefined, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>;
|
|
3090
|
+
export function useGetMovethewheelsDemandForecastVarianceSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>) {
|
|
3091
|
+
const options = baseOptions === ApolloReactHooks.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
|
|
3092
|
+
return ApolloReactHooks.useSuspenseQuery<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>(GetMovethewheelsDemandForecastVarianceDocument, options);
|
|
3093
|
+
}
|
|
3094
|
+
export type GetMovethewheelsDemandForecastVarianceQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastVarianceQuery>;
|
|
3095
|
+
export type GetMovethewheelsDemandForecastVarianceLazyQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastVarianceLazyQuery>;
|
|
3096
|
+
export type GetMovethewheelsDemandForecastVarianceSuspenseQueryHookResult = ReturnType<typeof useGetMovethewheelsDemandForecastVarianceSuspenseQuery>;
|
|
3097
|
+
export type GetMovethewheelsDemandForecastVarianceQueryResult = Apollo.QueryResult<Types.GetMovethewheelsDemandForecastVarianceQuery, Types.GetMovethewheelsDemandForecastVarianceQueryVariables>;
|
|
2921
3098
|
export const ListInventoryItemsDocument = gql`
|
|
2922
3099
|
query ListInventoryItems($filter: InventoryFilterInput, $pagination: PaginationInput) {
|
|
2923
3100
|
inventoryItems(filter: $filter, pagination: $pagination) {
|
|
@@ -9831,6 +9831,11 @@ export enum DeliveryChannelType {
|
|
|
9831
9831
|
Webhook = 'WEBHOOK'
|
|
9832
9832
|
}
|
|
9833
9833
|
|
|
9834
|
+
export enum DemandForecastScope {
|
|
9835
|
+
Customer = 'CUSTOMER',
|
|
9836
|
+
Lane = 'LANE'
|
|
9837
|
+
}
|
|
9838
|
+
|
|
9834
9839
|
export type DependencyHealth = {
|
|
9835
9840
|
__typename?: 'DependencyHealth';
|
|
9836
9841
|
message: Scalars['String']['output'];
|
|
@@ -20581,6 +20586,33 @@ export type MovethewheelsReportFilterInput = {
|
|
|
20581
20586
|
type?: InputMaybe<Scalars['String']['input']>;
|
|
20582
20587
|
};
|
|
20583
20588
|
|
|
20589
|
+
export type MtwDemandForecastPoint = {
|
|
20590
|
+
__typename?: 'MtwDemandForecastPoint';
|
|
20591
|
+
actualRevenue?: Maybe<Scalars['Float']['output']>;
|
|
20592
|
+
actualVolume?: Maybe<Scalars['Int']['output']>;
|
|
20593
|
+
confidence: Scalars['Float']['output'];
|
|
20594
|
+
periodEnd: Scalars['DateTime']['output'];
|
|
20595
|
+
periodStart: Scalars['DateTime']['output'];
|
|
20596
|
+
predictedRevenue: Scalars['Float']['output'];
|
|
20597
|
+
predictedVolume: Scalars['Int']['output'];
|
|
20598
|
+
scopeKey: Scalars['String']['output'];
|
|
20599
|
+
scopeType: DemandForecastScope;
|
|
20600
|
+
};
|
|
20601
|
+
|
|
20602
|
+
export type MtwDemandForecastVariance = {
|
|
20603
|
+
__typename?: 'MtwDemandForecastVariance';
|
|
20604
|
+
actualRevenue: Scalars['Float']['output'];
|
|
20605
|
+
actualVolume: Scalars['Int']['output'];
|
|
20606
|
+
periodEnd: Scalars['DateTime']['output'];
|
|
20607
|
+
periodStart: Scalars['DateTime']['output'];
|
|
20608
|
+
predictedRevenue: Scalars['Float']['output'];
|
|
20609
|
+
predictedVolume: Scalars['Int']['output'];
|
|
20610
|
+
revenueVariance: Scalars['Float']['output'];
|
|
20611
|
+
scopeKey: Scalars['String']['output'];
|
|
20612
|
+
scopeType: DemandForecastScope;
|
|
20613
|
+
volumeVariance: Scalars['Int']['output'];
|
|
20614
|
+
};
|
|
20615
|
+
|
|
20584
20616
|
/**
|
|
20585
20617
|
* Base Mutation type with placeholder field.
|
|
20586
20618
|
* Extended by individual mutation files.
|
|
@@ -22335,6 +22367,7 @@ export type Mutation = {
|
|
|
22335
22367
|
/** Reactivate an archived workspace (sets status to ACTIVE) */
|
|
22336
22368
|
reactivateWorkspace: Workspace;
|
|
22337
22369
|
rebuildKnowledgeBaseIndex: Scalars['Boolean']['output'];
|
|
22370
|
+
recalculateDemandForecasts: Scalars['Int']['output'];
|
|
22338
22371
|
/**
|
|
22339
22372
|
* Recalculate resource usage for the current workspace.
|
|
22340
22373
|
* Workspace ID is read from gateway context (x-workspace-id header).
|
|
@@ -32325,6 +32358,15 @@ export type MutationRebuildKnowledgeBaseIndexArgs = {
|
|
|
32325
32358
|
};
|
|
32326
32359
|
|
|
32327
32360
|
|
|
32361
|
+
/**
|
|
32362
|
+
* Base Mutation type with placeholder field.
|
|
32363
|
+
* Extended by individual mutation files.
|
|
32364
|
+
*/
|
|
32365
|
+
export type MutationRecalculateDemandForecastsArgs = {
|
|
32366
|
+
weeksOfHistory?: InputMaybe<Scalars['Int']['input']>;
|
|
32367
|
+
};
|
|
32368
|
+
|
|
32369
|
+
|
|
32328
32370
|
/**
|
|
32329
32371
|
* Base Mutation type with placeholder field.
|
|
32330
32372
|
* Extended by individual mutation files.
|
|
@@ -41214,6 +41256,10 @@ export type Query = {
|
|
|
41214
41256
|
deadLetterQueueEntry?: Maybe<DeadLetterQueue>;
|
|
41215
41257
|
decryptSecret?: Maybe<DecryptedSecret>;
|
|
41216
41258
|
defaultVibeDeck?: Maybe<VibeDeck>;
|
|
41259
|
+
/** Demand forecasting / S&OP v1 (BOFF-4393). Heuristic next-week forecasts. */
|
|
41260
|
+
demandForecastByCustomer: Array<MtwDemandForecastPoint>;
|
|
41261
|
+
demandForecastByLane: Array<MtwDemandForecastPoint>;
|
|
41262
|
+
demandForecastVariance: Array<MtwDemandForecastVariance>;
|
|
41217
41263
|
/** Check the health status of all system dependencies */
|
|
41218
41264
|
dependenciesHealth: HealthCheckResponse;
|
|
41219
41265
|
/**
|
|
@@ -43672,6 +43718,34 @@ export type QueryDecryptSecretArgs = {
|
|
|
43672
43718
|
};
|
|
43673
43719
|
|
|
43674
43720
|
|
|
43721
|
+
/**
|
|
43722
|
+
* Base Query type with placeholder field.
|
|
43723
|
+
* Extended by individual query files.
|
|
43724
|
+
*/
|
|
43725
|
+
export type QueryDemandForecastByCustomerArgs = {
|
|
43726
|
+
weeks?: InputMaybe<Scalars['Int']['input']>;
|
|
43727
|
+
};
|
|
43728
|
+
|
|
43729
|
+
|
|
43730
|
+
/**
|
|
43731
|
+
* Base Query type with placeholder field.
|
|
43732
|
+
* Extended by individual query files.
|
|
43733
|
+
*/
|
|
43734
|
+
export type QueryDemandForecastByLaneArgs = {
|
|
43735
|
+
weeks?: InputMaybe<Scalars['Int']['input']>;
|
|
43736
|
+
};
|
|
43737
|
+
|
|
43738
|
+
|
|
43739
|
+
/**
|
|
43740
|
+
* Base Query type with placeholder field.
|
|
43741
|
+
* Extended by individual query files.
|
|
43742
|
+
*/
|
|
43743
|
+
export type QueryDemandForecastVarianceArgs = {
|
|
43744
|
+
limit?: InputMaybe<Scalars['Int']['input']>;
|
|
43745
|
+
scopeType: DemandForecastScope;
|
|
43746
|
+
};
|
|
43747
|
+
|
|
43748
|
+
|
|
43675
43749
|
/**
|
|
43676
43750
|
* Base Query type with placeholder field.
|
|
43677
43751
|
* Extended by individual query files.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carbon footprint panel (BOFF-4391).
|
|
3
|
+
*
|
|
4
|
+
* Surfaces workspace-level CO2e totals and per-vehicle intensity. Data comes
|
|
5
|
+
* from Query.emissionsSummary via useEmissionsSummary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Leaf, Truck } from 'lucide-react';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from './ui';
|
|
10
|
+
import { MetricCard, MetricGrid } from './MetricCard';
|
|
11
|
+
import { useEmissionsSummary } from '../hooks/useAnalytics';
|
|
12
|
+
|
|
13
|
+
function formatKg(value: number): string {
|
|
14
|
+
if (value >= 1000) {
|
|
15
|
+
return `${(value / 1000).toFixed(2)} t`;
|
|
16
|
+
}
|
|
17
|
+
return `${value.toFixed(1)} kg`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatKm(value: number): string {
|
|
21
|
+
if (value >= 1000) {
|
|
22
|
+
return `${(value / 1000).toFixed(1)}k km`;
|
|
23
|
+
}
|
|
24
|
+
return `${value.toFixed(1)} km`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CarbonFootprintPanel() {
|
|
28
|
+
const { summary, isLoading, error } = useEmissionsSummary();
|
|
29
|
+
|
|
30
|
+
if (error) {
|
|
31
|
+
return (
|
|
32
|
+
<Card className="border-status-error-border">
|
|
33
|
+
<CardContent className="p-6 text-center">
|
|
34
|
+
<p className="text-sm text-status-error-text">Couldn't load emissions data</p>
|
|
35
|
+
</CardContent>
|
|
36
|
+
</Card>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isLoading) {
|
|
41
|
+
return (
|
|
42
|
+
<Card>
|
|
43
|
+
<CardHeader>
|
|
44
|
+
<CardTitle className="flex items-center gap-2">
|
|
45
|
+
<Leaf size={18} className="text-status-success-text" />
|
|
46
|
+
Carbon footprint
|
|
47
|
+
</CardTitle>
|
|
48
|
+
</CardHeader>
|
|
49
|
+
<CardContent>
|
|
50
|
+
<MetricGrid columns={3}>
|
|
51
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
52
|
+
<div key={i} className="h-24 rounded-xl bg-bg-sunken motion-safe:animate-pulse" />
|
|
53
|
+
))}
|
|
54
|
+
</MetricGrid>
|
|
55
|
+
</CardContent>
|
|
56
|
+
</Card>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hasData = summary.totalDistanceKm > 0;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle className="flex items-center gap-2">
|
|
66
|
+
<Leaf size={18} className="text-status-success-text" />
|
|
67
|
+
Carbon footprint
|
|
68
|
+
</CardTitle>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent className="space-y-6">
|
|
71
|
+
<MetricGrid columns={3}>
|
|
72
|
+
<MetricCard
|
|
73
|
+
label="Total CO2e"
|
|
74
|
+
value={formatKg(summary.totalCo2eKg)}
|
|
75
|
+
icon={<Leaf size={20} />}
|
|
76
|
+
accent="success"
|
|
77
|
+
/>
|
|
78
|
+
<MetricCard
|
|
79
|
+
label="Total distance"
|
|
80
|
+
value={formatKm(summary.totalDistanceKm)}
|
|
81
|
+
icon={<Truck size={20} />}
|
|
82
|
+
accent="blue"
|
|
83
|
+
/>
|
|
84
|
+
<MetricCard
|
|
85
|
+
label="Avg intensity"
|
|
86
|
+
value={`${(summary.averageCo2eKgPerKm * 1000).toFixed(1)} g/km`}
|
|
87
|
+
icon={<Leaf size={20} />}
|
|
88
|
+
accent="warning"
|
|
89
|
+
/>
|
|
90
|
+
</MetricGrid>
|
|
91
|
+
|
|
92
|
+
{hasData && summary.byVehicle.length > 0 && (
|
|
93
|
+
<div className="space-y-3">
|
|
94
|
+
<h4 className="text-sm font-medium text-text-secondary">By vehicle</h4>
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
{summary.byVehicle
|
|
97
|
+
.filter((v) => v.totalDistanceKm > 0 || v.co2eKg > 0)
|
|
98
|
+
.slice(0, 5)
|
|
99
|
+
.map((vehicle) => (
|
|
100
|
+
<div
|
|
101
|
+
key={vehicle.vehicleId ?? 'unassigned'}
|
|
102
|
+
className="flex items-center justify-between rounded-lg bg-bg-sunken px-3 py-2"
|
|
103
|
+
>
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Truck size={16} className="text-text-secondary" />
|
|
106
|
+
<span className="text-sm font-medium text-text-primary">
|
|
107
|
+
{vehicle.vehicleNumber ?? 'Unassigned'}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="text-right">
|
|
111
|
+
<p className="text-sm font-medium text-text-primary">
|
|
112
|
+
{formatKg(vehicle.co2eKg)}
|
|
113
|
+
</p>
|
|
114
|
+
<p className="text-xs text-text-secondary">
|
|
115
|
+
{formatKm(vehicle.totalDistanceKm)}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{!hasData && (
|
|
125
|
+
<p className="text-sm text-text-secondary">
|
|
126
|
+
No route distance recorded yet. Assign vehicles and distances to routes to see emissions
|
|
127
|
+
estimates.
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
</CardContent>
|
|
131
|
+
</Card>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpcomingDemandPanel
|
|
3
|
+
*
|
|
4
|
+
* Surfaces next-week demand forecasts by lane / customer alongside the
|
|
5
|
+
* most recent closed-period variance (BOFF-4393). Pure read-side view
|
|
6
|
+
* over `wspace-movethewheels-svc`'s DemandForecast surface.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Calendar, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from './ui';
|
|
12
|
+
|
|
13
|
+
import type { ForecastPoint, ForecastVariance } from '../hooks/useDemandForecast';
|
|
14
|
+
|
|
15
|
+
interface UpcomingDemandPanelProps {
|
|
16
|
+
laneForecast: ForecastPoint[];
|
|
17
|
+
customerForecast: ForecastPoint[];
|
|
18
|
+
laneVariance: ForecastVariance[];
|
|
19
|
+
customerVariance: ForecastVariance[];
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
onRetry: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const formatWeek = (iso: string): string => {
|
|
26
|
+
const date = new Date(iso);
|
|
27
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
28
|
+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const topByVolume = (rows: ForecastPoint[], weekStart: string): ForecastPoint[] => {
|
|
32
|
+
const filtered = rows.filter((row) => row.periodStart === weekStart);
|
|
33
|
+
return [...filtered].sort((a, b) => b.predictedVolume - a.predictedVolume).slice(0, 3);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const topByRevenue = (rows: ForecastPoint[], weekStart: string): ForecastPoint[] => {
|
|
37
|
+
const filtered = rows.filter((row) => row.periodStart === weekStart);
|
|
38
|
+
return [...filtered].sort((a, b) => b.predictedRevenue - a.predictedRevenue).slice(0, 3);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const totalVolume = (rows: ForecastPoint[], weekStart: string): number =>
|
|
42
|
+
rows
|
|
43
|
+
.filter((row) => row.periodStart === weekStart)
|
|
44
|
+
.reduce((sum, row) => sum + row.predictedVolume, 0);
|
|
45
|
+
|
|
46
|
+
const formatConfidence = (confidence: number): string => {
|
|
47
|
+
if (confidence <= 0) return 'Not enough history';
|
|
48
|
+
return `${Math.round(confidence * 100)}% confidence`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function UpcomingDemandPanel({
|
|
52
|
+
laneForecast,
|
|
53
|
+
customerForecast,
|
|
54
|
+
laneVariance,
|
|
55
|
+
customerVariance,
|
|
56
|
+
isLoading,
|
|
57
|
+
error,
|
|
58
|
+
onRetry,
|
|
59
|
+
}: UpcomingDemandPanelProps) {
|
|
60
|
+
if (error && !isLoading) {
|
|
61
|
+
return (
|
|
62
|
+
<Card className="border-status-error-border">
|
|
63
|
+
<CardContent className="p-6 text-center space-y-3">
|
|
64
|
+
<p className="text-status-error-text font-medium">Couldn't load demand forecast</p>
|
|
65
|
+
<p className="text-sm text-text-secondary">{error.message}</p>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={onRetry}
|
|
69
|
+
className="text-sm font-medium text-primary hover:underline"
|
|
70
|
+
>
|
|
71
|
+
Retry
|
|
72
|
+
</button>
|
|
73
|
+
</CardContent>
|
|
74
|
+
</Card>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isLoading) {
|
|
79
|
+
return (
|
|
80
|
+
<Card>
|
|
81
|
+
<CardHeader>
|
|
82
|
+
<CardTitle>Upcoming demand</CardTitle>
|
|
83
|
+
</CardHeader>
|
|
84
|
+
<CardContent className="space-y-4">
|
|
85
|
+
<Skeleton className="h-16 w-full" />
|
|
86
|
+
<Skeleton className="h-16 w-full" />
|
|
87
|
+
<Skeleton className="h-16 w-full" />
|
|
88
|
+
</CardContent>
|
|
89
|
+
</Card>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const nextWeekStart = laneForecast[0]?.periodStart ?? customerForecast[0]?.periodStart ?? null;
|
|
94
|
+
|
|
95
|
+
if (!nextWeekStart || laneForecast.length === 0) {
|
|
96
|
+
return (
|
|
97
|
+
<Card>
|
|
98
|
+
<CardHeader>
|
|
99
|
+
<CardTitle className="flex items-center gap-2">
|
|
100
|
+
<Calendar size={18} aria-hidden="true" />
|
|
101
|
+
Upcoming demand
|
|
102
|
+
</CardTitle>
|
|
103
|
+
</CardHeader>
|
|
104
|
+
<CardContent>
|
|
105
|
+
<div className="flex items-start gap-2 rounded-lg border border-border/60 bg-muted/40 p-4 text-sm text-text-secondary">
|
|
106
|
+
<AlertCircle size={16} aria-hidden="true" className="mt-0.5 shrink-0" />
|
|
107
|
+
<p>
|
|
108
|
+
Not enough history to forecast next week yet. Once you have at least 4 weeks of
|
|
109
|
+
completed orders, this panel will surface demand by lane and customer.
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
</CardContent>
|
|
113
|
+
</Card>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const topLanes = topByVolume(laneForecast, nextWeekStart);
|
|
118
|
+
const topLanesByRevenue = topByRevenue(laneForecast, nextWeekStart);
|
|
119
|
+
const topCustomers = topByVolume(customerForecast, nextWeekStart);
|
|
120
|
+
const total = totalVolume(laneForecast, nextWeekStart);
|
|
121
|
+
|
|
122
|
+
// Most-recent variance row per scope key — show the lane that overshot /
|
|
123
|
+
// undershot the most in absolute volume terms.
|
|
124
|
+
const mostRecentVariance = [...laneVariance, ...customerVariance]
|
|
125
|
+
.sort((a, b) => Math.abs(b.volumeVariance) - Math.abs(a.volumeVariance))
|
|
126
|
+
.slice(0, 3);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Card>
|
|
130
|
+
<CardHeader>
|
|
131
|
+
<CardTitle className="flex items-center gap-2">
|
|
132
|
+
<Calendar size={18} aria-hidden="true" />
|
|
133
|
+
Upcoming demand — week of {formatWeek(nextWeekStart)}
|
|
134
|
+
</CardTitle>
|
|
135
|
+
</CardHeader>
|
|
136
|
+
<CardContent className="space-y-4">
|
|
137
|
+
<div className="flex items-baseline justify-between rounded-lg border border-border/60 bg-muted/40 px-4 py-3">
|
|
138
|
+
<div>
|
|
139
|
+
<p className="text-xs uppercase tracking-wide text-text-secondary">
|
|
140
|
+
Predicted jobs (all lanes)
|
|
141
|
+
</p>
|
|
142
|
+
<p className="text-2xl font-bold">{total}</p>
|
|
143
|
+
</div>
|
|
144
|
+
<p className="text-xs text-text-secondary">
|
|
145
|
+
{topLanes[0] ? formatConfidence(topLanes[0].confidence) : ''}
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
150
|
+
<ForecastList title="Top lanes (volume)" rows={topLanes} />
|
|
151
|
+
<ForecastList title="Top lanes (revenue)" rows={topLanesByRevenue} />
|
|
152
|
+
<ForecastList title="Top customers (volume)" rows={topCustomers} />
|
|
153
|
+
<VarianceList rows={mostRecentVariance} />
|
|
154
|
+
</div>
|
|
155
|
+
</CardContent>
|
|
156
|
+
</Card>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface ForecastListProps {
|
|
161
|
+
title: string;
|
|
162
|
+
rows: ForecastPoint[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ForecastList({ title, rows }: ForecastListProps) {
|
|
166
|
+
if (rows.length === 0) {
|
|
167
|
+
return (
|
|
168
|
+
<div className="rounded-lg border border-border/60 p-4">
|
|
169
|
+
<p className="text-sm font-medium mb-2">{title}</p>
|
|
170
|
+
<p className="text-xs text-text-secondary">No forecasts yet for this week.</p>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return (
|
|
175
|
+
<div className="rounded-lg border border-border/60 p-4">
|
|
176
|
+
<p className="text-sm font-medium mb-3">{title}</p>
|
|
177
|
+
<ul className="space-y-2">
|
|
178
|
+
{rows.map((row) => (
|
|
179
|
+
<li
|
|
180
|
+
key={`${row.scopeType}-${row.scopeKey}`}
|
|
181
|
+
className="flex items-center justify-between text-sm"
|
|
182
|
+
>
|
|
183
|
+
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
184
|
+
<span className="ml-2 shrink-0 text-text-secondary">
|
|
185
|
+
{row.predictedVolume} jobs ·{' '}
|
|
186
|
+
{new Intl.NumberFormat(undefined, {
|
|
187
|
+
style: 'currency',
|
|
188
|
+
currency: 'USD',
|
|
189
|
+
maximumFractionDigits: 0,
|
|
190
|
+
}).format(row.predictedRevenue)}
|
|
191
|
+
</span>
|
|
192
|
+
</li>
|
|
193
|
+
))}
|
|
194
|
+
</ul>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface VarianceListProps {
|
|
200
|
+
rows: ForecastVariance[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function VarianceList({ rows }: VarianceListProps) {
|
|
204
|
+
return (
|
|
205
|
+
<div className="rounded-lg border border-border/60 p-4">
|
|
206
|
+
<p className="text-sm font-medium mb-3">Recent forecast variance</p>
|
|
207
|
+
{rows.length === 0 ? (
|
|
208
|
+
<p className="text-xs text-text-secondary">No closed periods to compare yet.</p>
|
|
209
|
+
) : (
|
|
210
|
+
<ul className="space-y-2">
|
|
211
|
+
{rows.map((row) => {
|
|
212
|
+
const isOvershoot = row.volumeVariance > 0;
|
|
213
|
+
const Icon = isOvershoot ? TrendingUp : TrendingDown;
|
|
214
|
+
const tone = isOvershoot
|
|
215
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
216
|
+
: 'text-rose-600 dark:text-rose-400';
|
|
217
|
+
return (
|
|
218
|
+
<li
|
|
219
|
+
key={`${row.scopeType}-${row.scopeKey}-${row.periodStart}`}
|
|
220
|
+
className="flex items-center justify-between text-sm"
|
|
221
|
+
>
|
|
222
|
+
<span className="flex items-center gap-2 truncate">
|
|
223
|
+
<Icon size={14} className={tone} aria-hidden="true" />
|
|
224
|
+
<span className="truncate font-medium">{row.scopeKey}</span>
|
|
225
|
+
</span>
|
|
226
|
+
<span className={`ml-2 shrink-0 ${tone}`}>
|
|
227
|
+
{isOvershoot ? '+' : ''}
|
|
228
|
+
{row.volumeVariance} jobs
|
|
229
|
+
</span>
|
|
230
|
+
</li>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
</ul>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|