Add frontend imporvements

This commit is contained in:
Urtzi Alfaro
2025-09-09 21:39:12 +02:00
parent 23e088dcb4
commit 2a05048912
16 changed files with 1761 additions and 1233 deletions

View File

@@ -0,0 +1,323 @@
/**
* Forecasting React Query hooks
*/
import {
useMutation,
useQuery,
useQueryClient,
UseQueryOptions,
UseMutationOptions,
useInfiniteQuery,
UseInfiniteQueryOptions,
} from '@tanstack/react-query';
import { forecastingService } from '../services/forecasting';
import {
ForecastRequest,
ForecastResponse,
BatchForecastRequest,
BatchForecastResponse,
ForecastListResponse,
ForecastByIdResponse,
ForecastStatistics,
DeleteForecastResponse,
GetForecastsParams,
ForecastingHealthResponse,
} from '../types/forecasting';
import { ApiError } from '../client/apiClient';
// ================================================================
// QUERY KEYS
// ================================================================
export const forecastingKeys = {
all: ['forecasting'] as const,
lists: () => [...forecastingKeys.all, 'list'] as const,
list: (tenantId: string, filters?: GetForecastsParams) =>
[...forecastingKeys.lists(), tenantId, filters] as const,
details: () => [...forecastingKeys.all, 'detail'] as const,
detail: (tenantId: string, forecastId: string) =>
[...forecastingKeys.details(), tenantId, forecastId] as const,
statistics: (tenantId: string) =>
[...forecastingKeys.all, 'statistics', tenantId] as const,
health: () => [...forecastingKeys.all, 'health'] as const,
} as const;
// ================================================================
// QUERIES
// ================================================================
/**
* Get tenant forecasts with filtering and pagination
*/
export const useTenantForecasts = (
tenantId: string,
params?: GetForecastsParams,
options?: Omit<UseQueryOptions<ForecastListResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<ForecastListResponse, ApiError>({
queryKey: forecastingKeys.list(tenantId, params),
queryFn: () => forecastingService.getTenantForecasts(tenantId, params),
staleTime: 2 * 60 * 1000, // 2 minutes
enabled: !!tenantId,
...options,
});
};
/**
* Get specific forecast by ID
*/
export const useForecastById = (
tenantId: string,
forecastId: string,
options?: Omit<UseQueryOptions<ForecastByIdResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<ForecastByIdResponse, ApiError>({
queryKey: forecastingKeys.detail(tenantId, forecastId),
queryFn: () => forecastingService.getForecastById(tenantId, forecastId),
staleTime: 5 * 60 * 1000, // 5 minutes
enabled: !!tenantId && !!forecastId,
...options,
});
};
/**
* Get forecast statistics for tenant
*/
export const useForecastStatistics = (
tenantId: string,
options?: Omit<UseQueryOptions<ForecastStatistics, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<ForecastStatistics, ApiError>({
queryKey: forecastingKeys.statistics(tenantId),
queryFn: () => forecastingService.getForecastStatistics(tenantId),
staleTime: 5 * 60 * 1000, // 5 minutes
enabled: !!tenantId,
...options,
});
};
/**
* Health check for forecasting service
*/
export const useForecastingHealth = (
options?: Omit<UseQueryOptions<ForecastingHealthResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<ForecastingHealthResponse, ApiError>({
queryKey: forecastingKeys.health(),
queryFn: () => forecastingService.getHealthCheck(),
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
// ================================================================
// INFINITE QUERIES
// ================================================================
/**
* Infinite query for tenant forecasts (for pagination)
*/
export const useInfiniteTenantForecasts = (
tenantId: string,
baseParams?: Omit<GetForecastsParams, 'skip' | 'limit'>,
options?: Omit<UseInfiniteQueryOptions<ForecastListResponse, ApiError>, 'queryKey' | 'queryFn' | 'getNextPageParam'>
) => {
const limit = baseParams?.limit || 20;
return useInfiniteQuery<ForecastListResponse, ApiError>({
queryKey: [...forecastingKeys.list(tenantId, baseParams), 'infinite'],
queryFn: ({ pageParam = 0 }) => {
const params: GetForecastsParams = {
...baseParams,
skip: pageParam as number,
limit,
};
return forecastingService.getTenantForecasts(tenantId, params);
},
getNextPageParam: (lastPage, allPages) => {
const totalFetched = allPages.reduce((sum, page) => sum + page.total_returned, 0);
return lastPage.total_returned === limit ? totalFetched : undefined;
},
staleTime: 2 * 60 * 1000, // 2 minutes
enabled: !!tenantId,
...options,
});
};
// ================================================================
// MUTATIONS
// ================================================================
/**
* Create single forecast mutation
*/
export const useCreateSingleForecast = (
options?: UseMutationOptions<
ForecastResponse,
ApiError,
{ tenantId: string; request: ForecastRequest }
>
) => {
const queryClient = useQueryClient();
return useMutation<
ForecastResponse,
ApiError,
{ tenantId: string; request: ForecastRequest }
>({
mutationFn: ({ tenantId, request }) =>
forecastingService.createSingleForecast(tenantId, request),
onSuccess: (data, variables) => {
// Invalidate and refetch forecasts list
queryClient.invalidateQueries({
queryKey: forecastingKeys.lists(),
});
// Update the specific forecast cache
queryClient.setQueryData(
forecastingKeys.detail(variables.tenantId, data.id),
{
...data,
enhanced_features: true,
repository_integration: true,
} as ForecastByIdResponse
);
// Update statistics
queryClient.invalidateQueries({
queryKey: forecastingKeys.statistics(variables.tenantId),
});
},
...options,
});
};
/**
* Create batch forecast mutation
*/
export const useCreateBatchForecast = (
options?: UseMutationOptions<
BatchForecastResponse,
ApiError,
{ tenantId: string; request: BatchForecastRequest }
>
) => {
const queryClient = useQueryClient();
return useMutation<
BatchForecastResponse,
ApiError,
{ tenantId: string; request: BatchForecastRequest }
>({
mutationFn: ({ tenantId, request }) =>
forecastingService.createBatchForecast(tenantId, request),
onSuccess: (data, variables) => {
// Invalidate forecasts list
queryClient.invalidateQueries({
queryKey: forecastingKeys.lists(),
});
// Cache individual forecasts if available
if (data.forecasts) {
data.forecasts.forEach((forecast) => {
queryClient.setQueryData(
forecastingKeys.detail(variables.tenantId, forecast.id),
{
...forecast,
enhanced_features: true,
repository_integration: true,
} as ForecastByIdResponse
);
});
}
// Update statistics
queryClient.invalidateQueries({
queryKey: forecastingKeys.statistics(variables.tenantId),
});
},
...options,
});
};
/**
* Delete forecast mutation
*/
export const useDeleteForecast = (
options?: UseMutationOptions<
DeleteForecastResponse,
ApiError,
{ tenantId: string; forecastId: string }
>
) => {
const queryClient = useQueryClient();
return useMutation<
DeleteForecastResponse,
ApiError,
{ tenantId: string; forecastId: string }
>({
mutationFn: ({ tenantId, forecastId }) =>
forecastingService.deleteForecast(tenantId, forecastId),
onSuccess: (data, variables) => {
// Remove from cache
queryClient.removeQueries({
queryKey: forecastingKeys.detail(variables.tenantId, variables.forecastId),
});
// Invalidate lists to refresh
queryClient.invalidateQueries({
queryKey: forecastingKeys.lists(),
});
// Update statistics
queryClient.invalidateQueries({
queryKey: forecastingKeys.statistics(variables.tenantId),
});
},
...options,
});
};
// ================================================================
// UTILITY FUNCTIONS
// ================================================================
/**
* Prefetch forecast by ID
*/
export const usePrefetchForecast = () => {
const queryClient = useQueryClient();
return (tenantId: string, forecastId: string) => {
queryClient.prefetchQuery({
queryKey: forecastingKeys.detail(tenantId, forecastId),
queryFn: () => forecastingService.getForecastById(tenantId, forecastId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
};
/**
* Invalidate all forecasting queries for a tenant
*/
export const useInvalidateForecasting = () => {
const queryClient = useQueryClient();
return (tenantId?: string) => {
if (tenantId) {
// Invalidate specific tenant queries
queryClient.invalidateQueries({
queryKey: forecastingKeys.list(tenantId),
});
queryClient.invalidateQueries({
queryKey: forecastingKeys.statistics(tenantId),
});
} else {
// Invalidate all forecasting queries
queryClient.invalidateQueries({
queryKey: forecastingKeys.all,
});
}
};
};

View File

@@ -25,6 +25,7 @@ export { trainingService } from './services/training';
export { alertProcessorService } from './services/alert_processor';
export { suppliersService } from './services/suppliers';
export { OrdersService } from './services/orders';
export { forecastingService } from './services/forecasting';
// Types - Auth
export type {
@@ -295,6 +296,22 @@ export type {
UpdatePlanStatusParams,
} from './types/orders';
// Types - Forecasting
export type {
ForecastRequest,
ForecastResponse,
BatchForecastRequest,
BatchForecastResponse,
ForecastStatistics,
ForecastListResponse,
ForecastByIdResponse,
DeleteForecastResponse,
GetForecastsParams,
ForecastingHealthResponse,
} from './types/forecasting';
export { BusinessType } from './types/forecasting';
// Hooks - Auth
export {
useAuthProfile,
@@ -551,6 +568,21 @@ export {
ordersKeys,
} from './hooks/orders';
// Hooks - Forecasting
export {
useTenantForecasts,
useForecastById,
useForecastStatistics,
useForecastingHealth,
useInfiniteTenantForecasts,
useCreateSingleForecast,
useCreateBatchForecast,
useDeleteForecast,
usePrefetchForecast,
useInvalidateForecasting,
forecastingKeys,
} from './hooks/forecasting';
// Query Key Factories (for advanced usage)
export {
authKeys,
@@ -567,4 +599,5 @@ export {
suppliersKeys,
ordersKeys,
dataImportKeys,
forecastingKeys,
};

View File

@@ -0,0 +1,132 @@
/**
* Forecasting Service
* API calls for forecasting service endpoints
*/
import { apiClient } from '../client/apiClient';
import {
ForecastRequest,
ForecastResponse,
BatchForecastRequest,
BatchForecastResponse,
ForecastListResponse,
ForecastByIdResponse,
ForecastStatistics,
DeleteForecastResponse,
GetForecastsParams,
ForecastingHealthResponse,
} from '../types/forecasting';
export class ForecastingService {
private readonly baseUrl = '/forecasts';
/**
* Generate a single product forecast
* POST /tenants/{tenant_id}/forecasts/single
*/
async createSingleForecast(
tenantId: string,
request: ForecastRequest
): Promise<ForecastResponse> {
return apiClient.post<ForecastResponse, ForecastRequest>(
`/tenants/${tenantId}${this.baseUrl}/single`,
request
);
}
/**
* Generate batch forecasts for multiple products
* POST /tenants/{tenant_id}/forecasts/batch
*/
async createBatchForecast(
tenantId: string,
request: BatchForecastRequest
): Promise<BatchForecastResponse> {
return apiClient.post<BatchForecastResponse, BatchForecastRequest>(
`/tenants/${tenantId}${this.baseUrl}/batch`,
request
);
}
/**
* Get tenant forecasts with filtering and pagination
* GET /tenants/{tenant_id}/forecasts
*/
async getTenantForecasts(
tenantId: string,
params?: GetForecastsParams
): Promise<ForecastListResponse> {
const searchParams = new URLSearchParams();
if (params?.inventory_product_id) {
searchParams.append('inventory_product_id', params.inventory_product_id);
}
if (params?.start_date) {
searchParams.append('start_date', params.start_date);
}
if (params?.end_date) {
searchParams.append('end_date', params.end_date);
}
if (params?.skip !== undefined) {
searchParams.append('skip', params.skip.toString());
}
if (params?.limit !== undefined) {
searchParams.append('limit', params.limit.toString());
}
const queryString = searchParams.toString();
const url = `/tenants/${tenantId}${this.baseUrl}${queryString ? `?${queryString}` : ''}`;
return apiClient.get<ForecastListResponse>(url);
}
/**
* Get specific forecast by ID
* GET /tenants/{tenant_id}/forecasts/{forecast_id}
*/
async getForecastById(
tenantId: string,
forecastId: string
): Promise<ForecastByIdResponse> {
return apiClient.get<ForecastByIdResponse>(
`/tenants/${tenantId}${this.baseUrl}/${forecastId}`
);
}
/**
* Delete a forecast
* DELETE /tenants/{tenant_id}/forecasts/{forecast_id}
*/
async deleteForecast(
tenantId: string,
forecastId: string
): Promise<DeleteForecastResponse> {
return apiClient.delete<DeleteForecastResponse>(
`/tenants/${tenantId}${this.baseUrl}/${forecastId}`
);
}
/**
* Get comprehensive forecast statistics
* GET /tenants/{tenant_id}/forecasts/statistics
*/
async getForecastStatistics(
tenantId: string
): Promise<ForecastStatistics> {
return apiClient.get<ForecastStatistics>(
`/tenants/${tenantId}${this.baseUrl}/statistics`
);
}
/**
* Health check for forecasting service
* GET /health
*/
async getHealthCheck(): Promise<ForecastingHealthResponse> {
return apiClient.get<ForecastingHealthResponse>('/health');
}
}
// Export singleton instance
export const forecastingService = new ForecastingService();
export default forecastingService;

View File

@@ -83,7 +83,7 @@ export class InventoryService {
}
async getLowStockIngredients(tenantId: string): Promise<IngredientResponse[]> {
return apiClient.get<IngredientResponse[]>(`${this.baseUrl}/${tenantId}/ingredients/low-stock`);
return apiClient.get<IngredientResponse[]>(`${this.baseUrl}/${tenantId}/stock/low-stock`);
}
// Stock Management
@@ -104,7 +104,7 @@ export class InventoryService {
queryParams.append('include_unavailable', includeUnavailable.toString());
return apiClient.get<StockResponse[]>(
`${this.baseUrl}/${tenantId}/stock/ingredient/${ingredientId}?${queryParams.toString()}`
`${this.baseUrl}/${tenantId}/ingredients/${ingredientId}/stock?${queryParams.toString()}`
);
}
@@ -218,8 +218,8 @@ export class InventoryService {
if (endDate) queryParams.append('end_date', endDate);
const url = queryParams.toString()
? `${this.baseUrl}/${tenantId}/inventory/analytics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/inventory/analytics`;
? `${this.baseUrl}/${tenantId}/dashboard/analytics?${queryParams.toString()}`
: `${this.baseUrl}/${tenantId}/dashboard/analytics`;
return apiClient.get(url);
}

View File

@@ -0,0 +1,160 @@
/**
* Forecasting API Types
* Mirror of backend forecasting service schemas
*/
// ================================================================
// ENUMS
// ================================================================
export enum BusinessType {
INDIVIDUAL = "individual",
CENTRAL_WORKSHOP = "central_workshop",
}
// ================================================================
// REQUEST TYPES
// ================================================================
export interface ForecastRequest {
inventory_product_id: string;
forecast_date: string; // ISO date string
forecast_days?: number; // Default: 1, Min: 1, Max: 30
location: string;
confidence_level?: number; // Default: 0.8, Min: 0.5, Max: 0.95
}
export interface BatchForecastRequest {
tenant_id: string;
batch_name: string;
inventory_product_ids: string[];
forecast_days?: number; // Default: 7, Min: 1, Max: 30
}
// ================================================================
// RESPONSE TYPES
// ================================================================
export interface ForecastResponse {
id: string;
tenant_id: string;
inventory_product_id: string;
location: string;
forecast_date: string; // ISO datetime string
// Predictions
predicted_demand: number;
confidence_lower: number;
confidence_upper: number;
confidence_level: number;
// Model info
model_id: string;
model_version: string;
algorithm: string;
// Context
business_type: string;
is_holiday: boolean;
is_weekend: boolean;
day_of_week: number;
// External factors
weather_temperature?: number;
weather_precipitation?: number;
weather_description?: string;
traffic_volume?: number;
// Metadata
created_at: string; // ISO datetime string
processing_time_ms?: number;
features_used?: Record<string, any>;
}
export interface BatchForecastResponse {
id: string;
tenant_id: string;
batch_name: string;
status: string;
total_products: number;
completed_products: number;
failed_products: number;
// Timing
requested_at: string; // ISO datetime string
completed_at?: string; // ISO datetime string
processing_time_ms?: number;
// Results
forecasts?: ForecastResponse[];
error_message?: string;
}
export interface ForecastStatistics {
tenant_id: string;
total_forecasts: number;
recent_forecasts: number;
accuracy_metrics: {
average_accuracy: number;
accuracy_trend: number;
};
model_performance: {
most_used_algorithm: string;
average_processing_time: number;
};
enhanced_features: boolean;
repository_integration: boolean;
}
export interface ForecastListResponse {
tenant_id: string;
forecasts: ForecastResponse[];
total_returned: number;
filters: {
inventory_product_id?: string;
start_date?: string; // ISO date string
end_date?: string; // ISO date string
};
pagination: {
skip: number;
limit: number;
};
enhanced_features: boolean;
repository_integration: boolean;
}
export interface ForecastByIdResponse extends ForecastResponse {
enhanced_features: boolean;
repository_integration: boolean;
}
export interface DeleteForecastResponse {
message: string;
forecast_id: string;
enhanced_features: boolean;
repository_integration: boolean;
}
// ================================================================
// QUERY PARAMETERS
// ================================================================
export interface GetForecastsParams {
inventory_product_id?: string;
start_date?: string; // ISO date string
end_date?: string; // ISO date string
skip?: number; // Default: 0
limit?: number; // Default: 100
}
// ================================================================
// HEALTH CHECK
// ================================================================
export interface ForecastingHealthResponse {
status: string;
service: string;
version: string;
features: string[];
timestamp: string;
}