diff --git a/frontend/src/api/hooks/orders.ts b/frontend/src/api/hooks/orders.ts new file mode 100644 index 00000000..4118373d --- /dev/null +++ b/frontend/src/api/hooks/orders.ts @@ -0,0 +1,332 @@ +/** + * Orders React Query hooks + */ +import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query'; +import { OrdersService } from '../services/orders'; +import { + OrderResponse, + OrderCreate, + OrderUpdate, + CustomerResponse, + CustomerCreate, + CustomerUpdate, + OrdersDashboardSummary, + DemandRequirements, + BusinessModelDetection, + ServiceStatus, + GetOrdersParams, + GetCustomersParams, + UpdateOrderStatusParams, + GetDemandRequirementsParams, +} from '../types/orders'; +import { ApiError } from '../client/apiClient'; + +// Query Keys +export const ordersKeys = { + all: ['orders'] as const, + + // Orders + orders: () => [...ordersKeys.all, 'orders'] as const, + ordersList: (params: GetOrdersParams) => [...ordersKeys.orders(), 'list', params] as const, + order: (tenantId: string, orderId: string) => [...ordersKeys.orders(), 'detail', tenantId, orderId] as const, + + // Customers + customers: () => [...ordersKeys.all, 'customers'] as const, + customersList: (params: GetCustomersParams) => [...ordersKeys.customers(), 'list', params] as const, + customer: (tenantId: string, customerId: string) => [...ordersKeys.customers(), 'detail', tenantId, customerId] as const, + + // Dashboard & Analytics + dashboard: (tenantId: string) => [...ordersKeys.all, 'dashboard', tenantId] as const, + demandRequirements: (params: GetDemandRequirementsParams) => [...ordersKeys.all, 'demand', params] as const, + businessModel: (tenantId: string) => [...ordersKeys.all, 'business-model', tenantId] as const, + + // Status + status: (tenantId: string) => [...ordersKeys.all, 'status', tenantId] as const, +} as const; + +// ===== Order Queries ===== + +export const useOrders = ( + params: GetOrdersParams, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.ordersList(params), + queryFn: () => OrdersService.getOrders(params), + staleTime: 2 * 60 * 1000, // 2 minutes + ...options, + }); +}; + +export const useOrder = ( + tenantId: string, + orderId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.order(tenantId, orderId), + queryFn: () => OrdersService.getOrder(tenantId, orderId), + staleTime: 1 * 60 * 1000, // 1 minute + enabled: !!tenantId && !!orderId, + ...options, + }); +}; + +// ===== Customer Queries ===== + +export const useCustomers = ( + params: GetCustomersParams, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.customersList(params), + queryFn: () => OrdersService.getCustomers(params), + staleTime: 5 * 60 * 1000, // 5 minutes + ...options, + }); +}; + +export const useCustomer = ( + tenantId: string, + customerId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.customer(tenantId, customerId), + queryFn: () => OrdersService.getCustomer(tenantId, customerId), + staleTime: 5 * 60 * 1000, // 5 minutes + enabled: !!tenantId && !!customerId, + ...options, + }); +}; + +// ===== Dashboard & Analytics Queries ===== + +export const useOrdersDashboard = ( + tenantId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.dashboard(tenantId), + queryFn: () => OrdersService.getDashboardSummary(tenantId), + staleTime: 1 * 60 * 1000, // 1 minute + enabled: !!tenantId, + ...options, + }); +}; + +export const useDemandRequirements = ( + params: GetDemandRequirementsParams, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.demandRequirements(params), + queryFn: () => OrdersService.getDemandRequirements(params), + staleTime: 30 * 60 * 1000, // 30 minutes + enabled: !!params.tenant_id && !!params.target_date, + ...options, + }); +}; + +export const useBusinessModelDetection = ( + tenantId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.businessModel(tenantId), + queryFn: () => OrdersService.detectBusinessModel(tenantId), + staleTime: 60 * 60 * 1000, // 1 hour + enabled: !!tenantId, + ...options, + }); +}; + +export const useOrdersServiceStatus = ( + tenantId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: ordersKeys.status(tenantId), + queryFn: () => OrdersService.getServiceStatus(tenantId), + staleTime: 30 * 1000, // 30 seconds + enabled: !!tenantId, + ...options, + }); +}; + +// ===== Order Mutations ===== + +export const useCreateOrder = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (orderData: OrderCreate) => OrdersService.createOrder(orderData), + onSuccess: (data, variables) => { + // Invalidate orders list for this tenant + queryClient.invalidateQueries({ + queryKey: ordersKeys.orders(), + predicate: (query) => { + const queryKey = query.queryKey as string[]; + return queryKey.includes('list') && + JSON.stringify(queryKey).includes(variables.tenant_id); + }, + }); + + // Invalidate dashboard + queryClient.invalidateQueries({ + queryKey: ordersKeys.dashboard(variables.tenant_id), + }); + + // Add the new order to cache + queryClient.setQueryData( + ordersKeys.order(variables.tenant_id, data.id), + data + ); + }, + ...options, + }); +}; + +export const useUpdateOrderStatus = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (params: UpdateOrderStatusParams) => OrdersService.updateOrderStatus(params), + onSuccess: (data, variables) => { + // Update the specific order in cache + queryClient.setQueryData( + ordersKeys.order(variables.tenant_id, variables.order_id), + data + ); + + // Invalidate orders list for this tenant + queryClient.invalidateQueries({ + queryKey: ordersKeys.orders(), + predicate: (query) => { + const queryKey = query.queryKey as string[]; + return queryKey.includes('list') && + JSON.stringify(queryKey).includes(variables.tenant_id); + }, + }); + + // Invalidate dashboard + queryClient.invalidateQueries({ + queryKey: ordersKeys.dashboard(variables.tenant_id), + }); + }, + ...options, + }); +}; + +// ===== Customer Mutations ===== + +export const useCreateCustomer = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (customerData: CustomerCreate) => OrdersService.createCustomer(customerData), + onSuccess: (data, variables) => { + // Invalidate customers list for this tenant + queryClient.invalidateQueries({ + queryKey: ordersKeys.customers(), + predicate: (query) => { + const queryKey = query.queryKey as string[]; + return queryKey.includes('list') && + JSON.stringify(queryKey).includes(variables.tenant_id); + }, + }); + + // Add the new customer to cache + queryClient.setQueryData( + ordersKeys.customer(variables.tenant_id, data.id), + data + ); + + // Invalidate dashboard (for customer metrics) + queryClient.invalidateQueries({ + queryKey: ordersKeys.dashboard(variables.tenant_id), + }); + }, + ...options, + }); +}; + +export const useUpdateCustomer = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ tenantId, customerId, data }) => OrdersService.updateCustomer(tenantId, customerId, data), + onSuccess: (data, variables) => { + // Update the specific customer in cache + queryClient.setQueryData( + ordersKeys.customer(variables.tenantId, variables.customerId), + data + ); + + // Invalidate customers list for this tenant + queryClient.invalidateQueries({ + queryKey: ordersKeys.customers(), + predicate: (query) => { + const queryKey = query.queryKey as string[]; + return queryKey.includes('list') && + JSON.stringify(queryKey).includes(variables.tenantId); + }, + }); + }, + ...options, + }); +}; + +// ===== Utility Functions ===== + +export const useInvalidateOrders = () => { + const queryClient = useQueryClient(); + + return { + invalidateAllOrders: (tenantId?: string) => { + if (tenantId) { + queryClient.invalidateQueries({ + queryKey: ordersKeys.all, + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(tenantId); + }, + }); + } else { + queryClient.invalidateQueries({ queryKey: ordersKeys.all }); + } + }, + invalidateOrdersList: (tenantId: string) => { + queryClient.invalidateQueries({ + queryKey: ordersKeys.orders(), + predicate: (query) => { + const queryKey = query.queryKey as string[]; + return queryKey.includes('list') && + JSON.stringify(queryKey).includes(tenantId); + }, + }); + }, + invalidateCustomersList: (tenantId: string) => { + queryClient.invalidateQueries({ + queryKey: ordersKeys.customers(), + predicate: (query) => { + const queryKey = query.queryKey as string[]; + return queryKey.includes('list') && + JSON.stringify(queryKey).includes(tenantId); + }, + }); + }, + invalidateDashboard: (tenantId: string) => { + queryClient.invalidateQueries({ + queryKey: ordersKeys.dashboard(tenantId), + }); + }, + }; +}; \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f57d9292..85303148 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -24,6 +24,7 @@ export { foodSafetyService } from './services/foodSafety'; export { trainingService } from './services/training'; export { alertProcessorService } from './services/alert_processor'; export { suppliersService } from './services/suppliers'; +export { OrdersService } from './services/orders'; // Types - Auth export type { @@ -234,6 +235,42 @@ export { PerformanceMetricType, } from './types/suppliers'; +// Types - Orders +export type { + CustomerType, + DeliveryMethod, + PaymentTerms as OrdersPaymentTerms, + PaymentMethod, + PaymentStatus, + CustomerSegment, + PriorityLevel, + OrderType, + OrderStatus, + OrderSource, + SalesChannel, + BusinessModel, + CustomerBase, + CustomerCreate, + CustomerUpdate, + CustomerResponse, + OrderItemBase, + OrderItemCreate, + OrderItemUpdate, + OrderItemResponse, + OrderBase, + OrderCreate, + OrderUpdate, + OrderResponse, + OrdersDashboardSummary, + DemandRequirements, + BusinessModelDetection, + ServiceStatus, + GetOrdersParams, + GetCustomersParams, + UpdateOrderStatusParams, + GetDemandRequirementsParams, +} from './types/orders'; + // Hooks - Auth export { useAuthProfile, @@ -460,6 +497,24 @@ export { suppliersKeys, } from './hooks/suppliers'; +// Hooks - Orders +export { + useOrders, + useOrder, + useCustomers, + useCustomer, + useOrdersDashboard, + useDemandRequirements, + useBusinessModelDetection, + useOrdersServiceStatus, + useCreateOrder, + useUpdateOrderStatus, + useCreateCustomer, + useUpdateCustomer, + useInvalidateOrders, + ordersKeys, +} from './hooks/orders'; + // Query Key Factories (for advanced usage) export { authKeys, @@ -474,5 +529,6 @@ export { trainingKeys, alertProcessorKeys, suppliersKeys, + ordersKeys, dataImportKeys, }; \ No newline at end of file diff --git a/frontend/src/api/services/orders.ts b/frontend/src/api/services/orders.ts new file mode 100644 index 00000000..5e7da533 --- /dev/null +++ b/frontend/src/api/services/orders.ts @@ -0,0 +1,173 @@ +/** + * Orders Service - API endpoints for Orders Service + * + * This service mirrors the backend API endpoints defined in: + * services/orders/app/api/orders.py + */ + +import { apiClient } from '../client/apiClient'; +import { + OrderResponse, + OrderCreate, + OrderUpdate, + CustomerResponse, + CustomerCreate, + CustomerUpdate, + OrdersDashboardSummary, + DemandRequirements, + BusinessModelDetection, + ServiceStatus, + GetOrdersParams, + GetCustomersParams, + UpdateOrderStatusParams, + GetDemandRequirementsParams, +} from '../types/orders'; + +export class OrdersService { + // ===== Dashboard and Analytics Endpoints ===== + + /** + * Get comprehensive dashboard summary for orders + * GET /tenants/{tenant_id}/orders/dashboard-summary + */ + static async getDashboardSummary(tenantId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/orders/dashboard-summary`); + } + + /** + * Get demand requirements for production planning + * GET /tenants/{tenant_id}/orders/demand-requirements + */ + static async getDemandRequirements(params: GetDemandRequirementsParams): Promise { + const { tenant_id, target_date } = params; + return apiClient.get( + `/tenants/${tenant_id}/orders/demand-requirements?target_date=${target_date}` + ); + } + + // ===== Order Management Endpoints ===== + + /** + * Create a new customer order + * POST /tenants/{tenant_id}/orders + */ + static async createOrder(orderData: OrderCreate): Promise { + const { tenant_id, ...data } = orderData; + return apiClient.post(`/tenants/${tenant_id}/orders`, data); + } + + /** + * Get order details with items + * GET /tenants/{tenant_id}/orders/{order_id} + */ + static async getOrder(tenantId: string, orderId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/orders/${orderId}`); + } + + /** + * Get orders with filtering and pagination + * GET /tenants/{tenant_id}/orders + */ + static async getOrders(params: GetOrdersParams): Promise { + const { tenant_id, status_filter, start_date, end_date, skip = 0, limit = 100 } = params; + + const queryParams = new URLSearchParams({ + skip: skip.toString(), + limit: limit.toString(), + }); + + if (status_filter) { + queryParams.append('status_filter', status_filter); + } + if (start_date) { + queryParams.append('start_date', start_date); + } + if (end_date) { + queryParams.append('end_date', end_date); + } + + return apiClient.get(`/tenants/${tenant_id}/orders?${queryParams.toString()}`); + } + + /** + * Update order status + * PUT /tenants/{tenant_id}/orders/{order_id}/status + */ + static async updateOrderStatus(params: UpdateOrderStatusParams): Promise { + const { tenant_id, order_id, new_status, reason } = params; + + const queryParams = new URLSearchParams(); + if (reason) { + queryParams.append('reason', reason); + } + + const url = `/tenants/${tenant_id}/orders/${order_id}/status${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; + + return apiClient.put(url, { status: new_status }); + } + + // ===== Customer Management Endpoints ===== + + /** + * Create a new customer + * POST /tenants/{tenant_id}/customers + */ + static async createCustomer(customerData: CustomerCreate): Promise { + const { tenant_id, ...data } = customerData; + return apiClient.post(`/tenants/${tenant_id}/customers`, data); + } + + /** + * Get customers with filtering and pagination + * GET /tenants/{tenant_id}/customers + */ + static async getCustomers(params: GetCustomersParams): Promise { + const { tenant_id, active_only = true, skip = 0, limit = 100 } = params; + + const queryParams = new URLSearchParams({ + active_only: active_only.toString(), + skip: skip.toString(), + limit: limit.toString(), + }); + + return apiClient.get(`/tenants/${tenant_id}/customers?${queryParams.toString()}`); + } + + /** + * Get customer details + * GET /tenants/{tenant_id}/customers/{customer_id} + */ + static async getCustomer(tenantId: string, customerId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/customers/${customerId}`); + } + + /** + * Update customer details + * PUT /tenants/{tenant_id}/customers/{customer_id} + */ + static async updateCustomer(tenantId: string, customerId: string, customerData: CustomerUpdate): Promise { + return apiClient.put(`/tenants/${tenantId}/customers/${customerId}`, customerData); + } + + // ===== Business Intelligence Endpoints ===== + + /** + * Detect business model based on order patterns + * GET /tenants/{tenant_id}/orders/business-model + */ + static async detectBusinessModel(tenantId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/orders/business-model`); + } + + // ===== Health and Status Endpoints ===== + + /** + * Get orders service status + * GET /tenants/{tenant_id}/orders/status + */ + static async getServiceStatus(tenantId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/orders/status`); + } +} + +export default OrdersService; \ No newline at end of file diff --git a/frontend/src/api/types/orders.ts b/frontend/src/api/types/orders.ts new file mode 100644 index 00000000..1005f403 --- /dev/null +++ b/frontend/src/api/types/orders.ts @@ -0,0 +1,283 @@ +/** + * TypeScript types for Orders Service + * Based on backend schemas in services/orders/app/schemas/order_schemas.py + */ + +export type CustomerType = 'individual' | 'business' | 'central_bakery'; +export type DeliveryMethod = 'delivery' | 'pickup'; +export type PaymentTerms = 'immediate' | 'net_30' | 'net_60'; +export type PaymentMethod = 'cash' | 'card' | 'bank_transfer' | 'account'; +export type PaymentStatus = 'pending' | 'partial' | 'paid' | 'failed' | 'refunded'; +export type CustomerSegment = 'vip' | 'regular' | 'wholesale'; +export type PriorityLevel = 'high' | 'normal' | 'low'; +export type OrderType = 'standard' | 'rush' | 'recurring' | 'special'; +export type OrderStatus = 'pending' | 'confirmed' | 'in_production' | 'ready' | 'out_for_delivery' | 'delivered' | 'cancelled' | 'failed'; +export type OrderSource = 'manual' | 'online' | 'phone' | 'app' | 'api'; +export type SalesChannel = 'direct' | 'wholesale' | 'retail'; +export type BusinessModel = 'individual_bakery' | 'central_bakery'; + +// ===== Customer Types ===== + +export interface CustomerBase { + name: string; + business_name?: string; + customer_type: CustomerType; + email?: string; + phone?: string; + address_line1?: string; + address_line2?: string; + city?: string; + state?: string; + postal_code?: string; + country: string; + is_active: boolean; + preferred_delivery_method: DeliveryMethod; + payment_terms: PaymentTerms; + credit_limit?: number; + discount_percentage: number; + customer_segment: CustomerSegment; + priority_level: PriorityLevel; + special_instructions?: string; + delivery_preferences?: Record; + product_preferences?: Record; +} + +export interface CustomerCreate extends CustomerBase { + customer_code: string; + tenant_id: string; +} + +export interface CustomerUpdate extends Partial> { + country?: string; + is_active?: boolean; + preferred_delivery_method?: DeliveryMethod; + payment_terms?: PaymentTerms; + discount_percentage?: number; + customer_segment?: CustomerSegment; + priority_level?: PriorityLevel; +} + +export interface CustomerResponse extends CustomerBase { + id: string; + tenant_id: string; + customer_code: string; + total_orders: number; + total_spent: number; + average_order_value: number; + last_order_date?: string; + created_at: string; + updated_at: string; +} + +// ===== Order Item Types ===== + +export interface OrderItemBase { + product_id: string; + product_name: string; + product_sku?: string; + product_category?: string; + quantity: number; + unit_of_measure: string; + weight?: number; + unit_price: number; + line_discount: number; + product_specifications?: Record; + customization_details?: string; + special_instructions?: string; + recipe_id?: string; +} + +export interface OrderItemCreate extends OrderItemBase {} + +export interface OrderItemUpdate { + quantity?: number; + unit_price?: number; + line_discount?: number; + product_specifications?: Record; + customization_details?: string; + special_instructions?: string; +} + +export interface OrderItemResponse extends OrderItemBase { + id: string; + order_id: string; + line_total: number; + status: string; + created_at: string; + updated_at: string; +} + +// ===== Order Types ===== + +export interface OrderBase { + customer_id: string; + order_type: OrderType; + priority: PriorityLevel; + requested_delivery_date: string; + delivery_method: DeliveryMethod; + delivery_address?: Record; + delivery_instructions?: string; + delivery_window_start?: string; + delivery_window_end?: string; + discount_percentage: number; + delivery_fee: number; + payment_method?: PaymentMethod; + payment_terms: PaymentTerms; + special_instructions?: string; + custom_requirements?: Record; + allergen_warnings?: Record; + order_source: OrderSource; + sales_channel: SalesChannel; + order_origin?: string; + communication_preferences?: Record; +} + +export interface OrderCreate extends OrderBase { + tenant_id: string; + items: OrderItemCreate[]; +} + +export interface OrderUpdate { + status?: OrderStatus; + priority?: PriorityLevel; + requested_delivery_date?: string; + confirmed_delivery_date?: string; + delivery_method?: DeliveryMethod; + delivery_address?: Record; + delivery_instructions?: string; + delivery_window_start?: string; + delivery_window_end?: string; + payment_method?: PaymentMethod; + payment_status?: PaymentStatus; + special_instructions?: string; + custom_requirements?: Record; + allergen_warnings?: Record; +} + +export interface OrderResponse extends OrderBase { + id: string; + tenant_id: string; + order_number: string; + status: OrderStatus; + order_date: string; + confirmed_delivery_date?: string; + actual_delivery_date?: string; + subtotal: number; + discount_amount: number; + tax_amount: number; + total_amount: number; + payment_status: PaymentStatus; + business_model?: string; + estimated_business_model?: string; + production_batch_id?: string; + quality_score?: number; + customer_rating?: number; + created_at: string; + updated_at: string; + items: OrderItemResponse[]; +} + +// ===== Dashboard and Analytics Types ===== + +export interface OrdersDashboardSummary { + // Current period metrics + total_orders_today: number; + total_orders_this_week: number; + total_orders_this_month: number; + + // Revenue metrics + revenue_today: number; + revenue_this_week: number; + revenue_this_month: number; + + // Order status breakdown + pending_orders: number; + confirmed_orders: number; + in_production_orders: number; + ready_orders: number; + delivered_orders: number; + + // Customer metrics + total_customers: number; + new_customers_this_month: number; + repeat_customers_rate: number; + + // Performance metrics + average_order_value: number; + order_fulfillment_rate: number; + on_time_delivery_rate: number; + + // Business model detection + business_model?: string; + business_model_confidence?: number; + + // Recent activity + recent_orders: OrderResponse[]; + high_priority_orders: OrderResponse[]; +} + +export interface DemandRequirements { + date: string; + tenant_id: string; + + // Product demand breakdown + product_demands: Record[]; + + // Aggregate metrics + total_orders: number; + total_quantity: number; + total_value: number; + + // Business context + business_model?: string; + rush_orders_count: number; + special_requirements: string[]; + + // Timing requirements + earliest_delivery: string; + latest_delivery: string; + average_lead_time_hours: number; +} + +export interface BusinessModelDetection { + business_model: string; + confidence: string; + detected_at: string; +} + +export interface ServiceStatus { + service: string; + status: string; + timestamp: string; + tenant_id: string; +} + +// ===== Query Parameters Types ===== + +export interface GetOrdersParams { + tenant_id: string; + status_filter?: string; + start_date?: string; + end_date?: string; + skip?: number; + limit?: number; +} + +export interface GetCustomersParams { + tenant_id: string; + active_only?: boolean; + skip?: number; + limit?: number; +} + +export interface UpdateOrderStatusParams { + tenant_id: string; + order_id: string; + new_status: OrderStatus; + reason?: string; +} + +export interface GetDemandRequirementsParams { + tenant_id: string; + target_date: string; +} \ No newline at end of file