Improve the frontend
This commit is contained in:
246
frontend/src/api/hooks/dashboard.ts
Normal file
246
frontend/src/api/hooks/dashboard.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Dashboard React Query hooks
|
||||
* Aggregates data from multiple services for dashboard metrics
|
||||
*/
|
||||
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useSalesAnalytics } from './sales';
|
||||
import { useOrdersDashboard } from './orders';
|
||||
import { inventoryService } from '../services/inventory';
|
||||
import { getAlertAnalytics } from '../services/alert_analytics';
|
||||
import { ApiError } from '../client/apiClient';
|
||||
import type { InventoryDashboardSummary } from '../types/dashboard';
|
||||
import type { AlertAnalytics } from '../services/alert_analytics';
|
||||
import type { SalesAnalytics } from '../types/sales';
|
||||
import type { OrdersDashboardSummary } from '../types/orders';
|
||||
|
||||
export interface DashboardStats {
|
||||
// Alert metrics
|
||||
activeAlerts: number;
|
||||
criticalAlerts: number;
|
||||
|
||||
// Order metrics
|
||||
pendingOrders: number;
|
||||
ordersToday: number;
|
||||
ordersTrend: number; // percentage change
|
||||
|
||||
// Sales metrics
|
||||
salesToday: number;
|
||||
salesTrend: number; // percentage change
|
||||
salesCurrency: string;
|
||||
|
||||
// Inventory metrics
|
||||
criticalStock: number;
|
||||
lowStockCount: number;
|
||||
outOfStockCount: number;
|
||||
expiringSoon: number;
|
||||
|
||||
// Production metrics
|
||||
productsSoldToday: number;
|
||||
productsSoldTrend: number;
|
||||
|
||||
// Data freshness
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface AggregatedDashboardData {
|
||||
alerts?: AlertAnalytics;
|
||||
orders?: OrdersDashboardSummary;
|
||||
sales?: SalesAnalytics;
|
||||
inventory?: InventoryDashboardSummary;
|
||||
}
|
||||
|
||||
// Query Keys
|
||||
export const dashboardKeys = {
|
||||
all: ['dashboard'] as const,
|
||||
stats: (tenantId: string) => [...dashboardKeys.all, 'stats', tenantId] as const,
|
||||
inventory: (tenantId: string) => [...dashboardKeys.all, 'inventory', tenantId] as const,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Fetch inventory dashboard summary
|
||||
*/
|
||||
export const useInventoryDashboard = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<InventoryDashboardSummary, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<InventoryDashboardSummary, ApiError>({
|
||||
queryKey: dashboardKeys.inventory(tenantId),
|
||||
queryFn: () => inventoryService.getDashboardSummary(tenantId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch alert analytics
|
||||
*/
|
||||
export const useAlertAnalytics = (
|
||||
tenantId: string,
|
||||
days: number = 7,
|
||||
options?: Omit<UseQueryOptions<AlertAnalytics, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<AlertAnalytics, ApiError>({
|
||||
queryKey: ['alerts', 'analytics', tenantId, days],
|
||||
queryFn: () => getAlertAnalytics(tenantId, days),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate percentage change between two values
|
||||
*/
|
||||
function calculateTrend(current: number, previous: number): number {
|
||||
if (previous === 0) return current > 0 ? 100 : 0;
|
||||
return Math.round(((current - previous) / previous) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate today's sales from sales records
|
||||
*/
|
||||
function calculateTodaySales(salesData?: SalesAnalytics): { amount: number; trend: number; productsSold: number; productsTrend: number } {
|
||||
if (!salesData) {
|
||||
return { amount: 0, trend: 0, productsSold: 0, productsTrend: 0 };
|
||||
}
|
||||
|
||||
// Sales data should have today's revenue and comparison
|
||||
const todayRevenue = salesData.total_revenue || 0;
|
||||
const previousRevenue = salesData.previous_period_revenue || 0;
|
||||
const todayUnits = salesData.total_units_sold || 0;
|
||||
const previousUnits = salesData.previous_period_units_sold || 0;
|
||||
|
||||
return {
|
||||
amount: todayRevenue,
|
||||
trend: calculateTrend(todayRevenue, previousRevenue),
|
||||
productsSold: todayUnits,
|
||||
productsTrend: calculateTrend(todayUnits, previousUnits),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate orders metrics
|
||||
*/
|
||||
function calculateOrdersMetrics(ordersData?: OrdersDashboardSummary): { pending: number; today: number; trend: number } {
|
||||
if (!ordersData) {
|
||||
return { pending: 0, today: 0, trend: 0 };
|
||||
}
|
||||
|
||||
const pendingCount = ordersData.pending_orders_count || 0;
|
||||
const todayCount = ordersData.orders_today_count || 0;
|
||||
const yesterdayCount = ordersData.orders_yesterday_count || 0;
|
||||
|
||||
return {
|
||||
pending: pendingCount,
|
||||
today: todayCount,
|
||||
trend: calculateTrend(todayCount, yesterdayCount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate dashboard data from all services
|
||||
*/
|
||||
function aggregateDashboardStats(data: AggregatedDashboardData): DashboardStats {
|
||||
const sales = calculateTodaySales(data.sales);
|
||||
const orders = calculateOrdersMetrics(data.orders);
|
||||
|
||||
const criticalStockCount =
|
||||
(data.inventory?.low_stock_count || 0) +
|
||||
(data.inventory?.out_of_stock_count || 0);
|
||||
|
||||
return {
|
||||
// Alerts
|
||||
activeAlerts: data.alerts?.activeAlerts || 0,
|
||||
criticalAlerts: data.alerts?.totalAlerts || 0,
|
||||
|
||||
// Orders
|
||||
pendingOrders: orders.pending,
|
||||
ordersToday: orders.today,
|
||||
ordersTrend: orders.trend,
|
||||
|
||||
// Sales
|
||||
salesToday: sales.amount,
|
||||
salesTrend: sales.trend,
|
||||
salesCurrency: '€', // Default to EUR for bakery
|
||||
|
||||
// Inventory
|
||||
criticalStock: criticalStockCount,
|
||||
lowStockCount: data.inventory?.low_stock_count || 0,
|
||||
outOfStockCount: data.inventory?.out_of_stock_count || 0,
|
||||
expiringSoon: data.inventory?.expiring_soon_count || 0,
|
||||
|
||||
// Products
|
||||
productsSoldToday: sales.productsSold,
|
||||
productsSoldTrend: sales.productsTrend,
|
||||
|
||||
// Metadata
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main hook to fetch aggregated dashboard statistics
|
||||
* Combines data from multiple services into a single cohesive dashboard view
|
||||
*/
|
||||
export const useDashboardStats = (
|
||||
tenantId: string,
|
||||
options?: Omit<UseQueryOptions<DashboardStats, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
// Get today's date range for sales
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||
|
||||
return useQuery<DashboardStats, ApiError>({
|
||||
queryKey: dashboardKeys.stats(tenantId),
|
||||
queryFn: async () => {
|
||||
// Fetch all data in parallel
|
||||
const [alertsData, ordersData, salesData, inventoryData] = await Promise.allSettled([
|
||||
getAlertAnalytics(tenantId, 7),
|
||||
// Note: OrdersService methods are static
|
||||
import('../services/orders').then(({ OrdersService }) =>
|
||||
OrdersService.getDashboardSummary(tenantId)
|
||||
),
|
||||
// Fetch today's sales with comparison to yesterday
|
||||
import('../services/sales').then(({ salesService }) =>
|
||||
salesService.getSalesAnalytics(tenantId, todayStr, todayStr)
|
||||
),
|
||||
inventoryService.getDashboardSummary(tenantId),
|
||||
]);
|
||||
|
||||
// Extract data or use undefined for failed requests
|
||||
const aggregatedData: AggregatedDashboardData = {
|
||||
alerts: alertsData.status === 'fulfilled' ? alertsData.value : undefined,
|
||||
orders: ordersData.status === 'fulfilled' ? ordersData.value : undefined,
|
||||
sales: salesData.status === 'fulfilled' ? salesData.value : undefined,
|
||||
inventory: inventoryData.status === 'fulfilled' ? inventoryData.value : undefined,
|
||||
};
|
||||
|
||||
// Log any failures for debugging
|
||||
if (alertsData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch alerts:', alertsData.reason);
|
||||
}
|
||||
if (ordersData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch orders:', ordersData.reason);
|
||||
}
|
||||
if (salesData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch sales:', salesData.reason);
|
||||
}
|
||||
if (inventoryData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch inventory:', inventoryData.reason);
|
||||
}
|
||||
|
||||
return aggregateDashboardStats(aggregatedData);
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
refetchInterval: 60 * 1000, // Auto-refresh every minute
|
||||
retry: 2, // Retry failed requests twice
|
||||
retryDelay: 1000, // Wait 1s between retries
|
||||
...options,
|
||||
});
|
||||
};
|
||||
259
frontend/src/api/hooks/purchase-orders.ts
Normal file
259
frontend/src/api/hooks/purchase-orders.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Purchase Orders React Query hooks
|
||||
* Handles data fetching and mutations for purchase orders
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { ApiError } from '../client/apiClient';
|
||||
import type {
|
||||
PurchaseOrderSummary,
|
||||
PurchaseOrderDetail,
|
||||
PurchaseOrderSearchParams,
|
||||
PurchaseOrderUpdateData,
|
||||
PurchaseOrderStatus
|
||||
} from '../services/purchase_orders';
|
||||
import {
|
||||
listPurchaseOrders,
|
||||
getPurchaseOrder,
|
||||
getPendingApprovalPurchaseOrders,
|
||||
getPurchaseOrdersByStatus,
|
||||
updatePurchaseOrder,
|
||||
approvePurchaseOrder,
|
||||
rejectPurchaseOrder,
|
||||
bulkApprovePurchaseOrders,
|
||||
deletePurchaseOrder
|
||||
} from '../services/purchase_orders';
|
||||
|
||||
// Query Keys
|
||||
export const purchaseOrderKeys = {
|
||||
all: ['purchase-orders'] as const,
|
||||
lists: () => [...purchaseOrderKeys.all, 'list'] as const,
|
||||
list: (tenantId: string, params?: PurchaseOrderSearchParams) =>
|
||||
[...purchaseOrderKeys.lists(), tenantId, params] as const,
|
||||
details: () => [...purchaseOrderKeys.all, 'detail'] as const,
|
||||
detail: (tenantId: string, poId: string) =>
|
||||
[...purchaseOrderKeys.details(), tenantId, poId] as const,
|
||||
byStatus: (tenantId: string, status: PurchaseOrderStatus) =>
|
||||
[...purchaseOrderKeys.lists(), tenantId, 'status', status] as const,
|
||||
pendingApproval: (tenantId: string) =>
|
||||
[...purchaseOrderKeys.lists(), tenantId, 'pending-approval'] as const,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Hook to list purchase orders with optional filters
|
||||
*/
|
||||
export const usePurchaseOrders = (
|
||||
tenantId: string,
|
||||
params?: PurchaseOrderSearchParams,
|
||||
options?: Omit<UseQueryOptions<PurchaseOrderSummary[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PurchaseOrderSummary[], ApiError>({
|
||||
queryKey: purchaseOrderKeys.list(tenantId, params),
|
||||
queryFn: () => listPurchaseOrders(tenantId, params),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get pending approval purchase orders
|
||||
*/
|
||||
export const usePendingApprovalPurchaseOrders = (
|
||||
tenantId: string,
|
||||
limit: number = 50,
|
||||
options?: Omit<UseQueryOptions<PurchaseOrderSummary[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PurchaseOrderSummary[], ApiError>({
|
||||
queryKey: purchaseOrderKeys.pendingApproval(tenantId),
|
||||
queryFn: () => getPendingApprovalPurchaseOrders(tenantId, limit),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 15 * 1000, // 15 seconds - more frequent for pending approvals
|
||||
refetchInterval: 30 * 1000, // Auto-refetch every 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get purchase orders by status
|
||||
*/
|
||||
export const usePurchaseOrdersByStatus = (
|
||||
tenantId: string,
|
||||
status: PurchaseOrderStatus,
|
||||
limit: number = 50,
|
||||
options?: Omit<UseQueryOptions<PurchaseOrderSummary[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PurchaseOrderSummary[], ApiError>({
|
||||
queryKey: purchaseOrderKeys.byStatus(tenantId, status),
|
||||
queryFn: () => getPurchaseOrdersByStatus(tenantId, status, limit),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get a single purchase order detail
|
||||
*/
|
||||
export const usePurchaseOrder = (
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
options?: Omit<UseQueryOptions<PurchaseOrderDetail, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<PurchaseOrderDetail, ApiError>({
|
||||
queryKey: purchaseOrderKeys.detail(tenantId, poId),
|
||||
queryFn: () => getPurchaseOrder(tenantId, poId),
|
||||
enabled: !!tenantId && !!poId,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to update a purchase order
|
||||
*/
|
||||
export const useUpdatePurchaseOrder = (
|
||||
options?: UseMutationOptions<
|
||||
PurchaseOrderDetail,
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string; data: PurchaseOrderUpdateData }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
PurchaseOrderDetail,
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string; data: PurchaseOrderUpdateData }
|
||||
>({
|
||||
mutationFn: ({ tenantId, poId, data }) => updatePurchaseOrder(tenantId, poId, data),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate and refetch related queries
|
||||
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.lists() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: purchaseOrderKeys.detail(variables.tenantId, variables.poId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to approve a purchase order
|
||||
*/
|
||||
export const useApprovePurchaseOrder = (
|
||||
options?: UseMutationOptions<
|
||||
PurchaseOrderDetail,
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string; notes?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
PurchaseOrderDetail,
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string; notes?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, poId, notes }) => approvePurchaseOrder(tenantId, poId, notes),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate pending approvals list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: purchaseOrderKeys.pendingApproval(variables.tenantId)
|
||||
});
|
||||
// Invalidate all lists
|
||||
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.lists() });
|
||||
// Invalidate detail
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: purchaseOrderKeys.detail(variables.tenantId, variables.poId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to reject a purchase order
|
||||
*/
|
||||
export const useRejectPurchaseOrder = (
|
||||
options?: UseMutationOptions<
|
||||
PurchaseOrderDetail,
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string; reason: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
PurchaseOrderDetail,
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string; reason: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, poId, reason }) => rejectPurchaseOrder(tenantId, poId, reason),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate pending approvals list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: purchaseOrderKeys.pendingApproval(variables.tenantId)
|
||||
});
|
||||
// Invalidate all lists
|
||||
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.lists() });
|
||||
// Invalidate detail
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: purchaseOrderKeys.detail(variables.tenantId, variables.poId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to bulk approve purchase orders
|
||||
*/
|
||||
export const useBulkApprovePurchaseOrders = (
|
||||
options?: UseMutationOptions<
|
||||
PurchaseOrderDetail[],
|
||||
ApiError,
|
||||
{ tenantId: string; poIds: string[]; notes?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
PurchaseOrderDetail[],
|
||||
ApiError,
|
||||
{ tenantId: string; poIds: string[]; notes?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, poIds, notes }) => bulkApprovePurchaseOrders(tenantId, poIds, notes),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all PO queries
|
||||
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.all });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to delete a purchase order
|
||||
*/
|
||||
export const useDeletePurchaseOrder = (
|
||||
options?: UseMutationOptions<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; poId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, poId }) => deletePurchaseOrder(tenantId, poId),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate all PO queries
|
||||
queryClient.invalidateQueries({ queryKey: purchaseOrderKeys.all });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
125
frontend/src/api/services/alert_analytics.ts
Normal file
125
frontend/src/api/services/alert_analytics.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Alert Analytics API Client
|
||||
* Handles all API calls for alert analytics and interaction tracking
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export interface AlertTrendData {
|
||||
date: string;
|
||||
count: number;
|
||||
urgentCount: number;
|
||||
highCount: number;
|
||||
mediumCount: number;
|
||||
lowCount: number;
|
||||
}
|
||||
|
||||
export interface AlertCategory {
|
||||
category: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface AlertAnalytics {
|
||||
trends: AlertTrendData[];
|
||||
averageResponseTime: number;
|
||||
topCategories: AlertCategory[];
|
||||
totalAlerts: number;
|
||||
resolvedAlerts: number;
|
||||
activeAlerts: number;
|
||||
resolutionRate: number;
|
||||
predictedDailyAverage: number;
|
||||
busiestDay: string;
|
||||
}
|
||||
|
||||
export interface AlertInteraction {
|
||||
alert_id: string;
|
||||
interaction_type: 'acknowledged' | 'resolved' | 'snoozed' | 'dismissed';
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface InteractionResponse {
|
||||
id: string;
|
||||
alert_id: string;
|
||||
interaction_type: string;
|
||||
interacted_at: string;
|
||||
response_time_seconds: number;
|
||||
}
|
||||
|
||||
export interface BatchInteractionResponse {
|
||||
created_count: number;
|
||||
interactions: Array<{
|
||||
id: string;
|
||||
alert_id: string;
|
||||
interaction_type: string;
|
||||
interacted_at: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a single alert interaction
|
||||
*/
|
||||
export async function trackAlertInteraction(
|
||||
tenantId: string,
|
||||
alertId: string,
|
||||
interactionType: 'acknowledged' | 'resolved' | 'snoozed' | 'dismissed',
|
||||
metadata?: Record<string, any>
|
||||
): Promise<InteractionResponse> {
|
||||
return apiClient.post<InteractionResponse>(
|
||||
`/tenants/${tenantId}/alerts/${alertId}/interactions`,
|
||||
{
|
||||
alert_id: alertId,
|
||||
interaction_type: interactionType,
|
||||
metadata
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track multiple alert interactions in batch
|
||||
*/
|
||||
export async function trackAlertInteractionsBatch(
|
||||
tenantId: string,
|
||||
interactions: AlertInteraction[]
|
||||
): Promise<BatchInteractionResponse> {
|
||||
return apiClient.post<BatchInteractionResponse>(
|
||||
`/tenants/${tenantId}/alerts/interactions/batch`,
|
||||
{
|
||||
interactions
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive alert analytics
|
||||
*/
|
||||
export async function getAlertAnalytics(
|
||||
tenantId: string,
|
||||
days: number = 7
|
||||
): Promise<AlertAnalytics> {
|
||||
console.log('[getAlertAnalytics] Calling API:', `/tenants/${tenantId}/alerts/analytics`, 'with days:', days);
|
||||
const data = await apiClient.get<AlertAnalytics>(
|
||||
`/tenants/${tenantId}/alerts/analytics`,
|
||||
{
|
||||
params: { days }
|
||||
}
|
||||
);
|
||||
console.log('[getAlertAnalytics] Received data:', data);
|
||||
console.log('[getAlertAnalytics] Data type:', typeof data);
|
||||
return data; // apiClient.get() already returns data, not response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alert trends only
|
||||
*/
|
||||
export async function getAlertTrends(
|
||||
tenantId: string,
|
||||
days: number = 7
|
||||
): Promise<AlertTrendData[]> {
|
||||
return apiClient.get<AlertTrendData[]>(
|
||||
`/tenants/${tenantId}/alerts/analytics/trends`,
|
||||
{
|
||||
params: { days }
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export class OrdersService {
|
||||
*/
|
||||
static async createOrder(orderData: OrderCreate): Promise<OrderResponse> {
|
||||
const { tenant_id, ...data } = orderData;
|
||||
return apiClient.post<OrderResponse>(`/tenants/${tenant_id}/orders/orders`, data);
|
||||
return apiClient.post<OrderResponse>(`/tenants/${tenant_id}/orders`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ export class OrdersService {
|
||||
* GET /tenants/{tenant_id}/orders/{order_id}
|
||||
*/
|
||||
static async getOrder(tenantId: string, orderId: string): Promise<OrderResponse> {
|
||||
return apiClient.get<OrderResponse>(`/tenants/${tenantId}/orders/orders/${orderId}`);
|
||||
return apiClient.get<OrderResponse>(`/tenants/${tenantId}/orders/${orderId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +117,7 @@ export class OrdersService {
|
||||
queryParams.append('end_date', end_date);
|
||||
}
|
||||
|
||||
return apiClient.get<OrderResponse[]>(`/tenants/${tenant_id}/orders/orders?${queryParams.toString()}`);
|
||||
return apiClient.get<OrderResponse[]>(`/tenants/${tenant_id}/orders?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -381,7 +381,7 @@ export class ProductionService {
|
||||
}
|
||||
|
||||
async getProductionRequirements(tenantId: string, date: string): Promise<any> {
|
||||
return apiClient.get(`${this.baseUrl}/${tenantId}/production/dashboard/requirements/${date}`);
|
||||
return apiClient.get(`${this.baseUrl}/${tenantId}/production/dashboard/requirements?date=${date}`);
|
||||
}
|
||||
|
||||
async getCapacityOverview(tenantId: string, date?: string): Promise<any> {
|
||||
|
||||
239
frontend/src/api/services/purchase_orders.ts
Normal file
239
frontend/src/api/services/purchase_orders.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Purchase Orders API Client
|
||||
* Handles all API calls for purchase orders in the suppliers service
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export type PurchaseOrderStatus =
|
||||
| 'DRAFT'
|
||||
| 'PENDING_APPROVAL'
|
||||
| 'APPROVED'
|
||||
| 'SENT_TO_SUPPLIER'
|
||||
| 'CONFIRMED'
|
||||
| 'RECEIVED'
|
||||
| 'COMPLETED'
|
||||
| 'CANCELLED'
|
||||
| 'DISPUTED';
|
||||
|
||||
export type PurchaseOrderPriority = 'urgent' | 'high' | 'normal' | 'low';
|
||||
|
||||
export interface PurchaseOrderItem {
|
||||
id: string;
|
||||
inventory_product_id: string;
|
||||
product_code?: string;
|
||||
ordered_quantity: number;
|
||||
unit_of_measure: string;
|
||||
unit_price: string; // Decimal as string
|
||||
line_total: string; // Decimal as string
|
||||
received_quantity: number;
|
||||
remaining_quantity: number;
|
||||
quality_requirements?: string;
|
||||
item_notes?: string;
|
||||
}
|
||||
|
||||
export interface SupplierSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
supplier_code: string;
|
||||
supplier_type: string;
|
||||
status: string;
|
||||
contact_person?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderSummary {
|
||||
id: string;
|
||||
po_number: string;
|
||||
supplier_id: string;
|
||||
supplier_name?: string;
|
||||
status: PurchaseOrderStatus;
|
||||
priority: PurchaseOrderPriority;
|
||||
order_date: string;
|
||||
required_delivery_date?: string;
|
||||
total_amount: string; // Decimal as string
|
||||
currency: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderDetail extends PurchaseOrderSummary {
|
||||
reference_number?: string;
|
||||
estimated_delivery_date?: string;
|
||||
|
||||
// Financial information
|
||||
subtotal: string;
|
||||
tax_amount: string;
|
||||
shipping_cost: string;
|
||||
discount_amount: string;
|
||||
|
||||
// Delivery information
|
||||
delivery_address?: string;
|
||||
delivery_instructions?: string;
|
||||
delivery_contact?: string;
|
||||
delivery_phone?: string;
|
||||
|
||||
// Approval workflow
|
||||
requires_approval: boolean;
|
||||
approved_by?: string;
|
||||
approved_at?: string;
|
||||
rejection_reason?: string;
|
||||
|
||||
// Communication tracking
|
||||
sent_to_supplier_at?: string;
|
||||
supplier_confirmation_date?: string;
|
||||
supplier_reference?: string;
|
||||
|
||||
// Additional information
|
||||
notes?: string;
|
||||
internal_notes?: string;
|
||||
terms_and_conditions?: string;
|
||||
|
||||
// Audit fields
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
|
||||
// Related data
|
||||
supplier?: SupplierSummary;
|
||||
items?: PurchaseOrderItem[];
|
||||
}
|
||||
|
||||
export interface PurchaseOrderSearchParams {
|
||||
supplier_id?: string;
|
||||
status?: PurchaseOrderStatus;
|
||||
priority?: PurchaseOrderPriority;
|
||||
date_from?: string; // YYYY-MM-DD
|
||||
date_to?: string; // YYYY-MM-DD
|
||||
search_term?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderUpdateData {
|
||||
status?: PurchaseOrderStatus;
|
||||
priority?: PurchaseOrderPriority;
|
||||
notes?: string;
|
||||
rejection_reason?: string;
|
||||
internal_notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of purchase orders with optional filters
|
||||
*/
|
||||
export async function listPurchaseOrders(
|
||||
tenantId: string,
|
||||
params?: PurchaseOrderSearchParams
|
||||
): Promise<PurchaseOrderSummary[]> {
|
||||
return apiClient.get<PurchaseOrderSummary[]>(
|
||||
`/tenants/${tenantId}/purchase-orders`,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get purchase orders by status
|
||||
*/
|
||||
export async function getPurchaseOrdersByStatus(
|
||||
tenantId: string,
|
||||
status: PurchaseOrderStatus,
|
||||
limit: number = 50
|
||||
): Promise<PurchaseOrderSummary[]> {
|
||||
return listPurchaseOrders(tenantId, { status, limit });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending approval purchase orders
|
||||
*/
|
||||
export async function getPendingApprovalPurchaseOrders(
|
||||
tenantId: string,
|
||||
limit: number = 50
|
||||
): Promise<PurchaseOrderSummary[]> {
|
||||
return getPurchaseOrdersByStatus(tenantId, 'PENDING_APPROVAL', limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single purchase order by ID with full details
|
||||
*/
|
||||
export async function getPurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.get<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update purchase order
|
||||
*/
|
||||
export async function updatePurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
data: PurchaseOrderUpdateData
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.put<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a purchase order
|
||||
*/
|
||||
export async function approvePurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
notes?: string
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.post<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}/approve`,
|
||||
{
|
||||
action: 'approve',
|
||||
notes: notes || 'Approved from dashboard'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a purchase order
|
||||
*/
|
||||
export async function rejectPurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string,
|
||||
reason: string
|
||||
): Promise<PurchaseOrderDetail> {
|
||||
return apiClient.post<PurchaseOrderDetail>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}/approve`,
|
||||
{
|
||||
action: 'reject',
|
||||
notes: reason
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk approve purchase orders
|
||||
*/
|
||||
export async function bulkApprovePurchaseOrders(
|
||||
tenantId: string,
|
||||
poIds: string[],
|
||||
notes?: string
|
||||
): Promise<PurchaseOrderDetail[]> {
|
||||
const approvalPromises = poIds.map(poId =>
|
||||
approvePurchaseOrder(tenantId, poId, notes)
|
||||
);
|
||||
return Promise.all(approvalPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete purchase order
|
||||
*/
|
||||
export async function deletePurchaseOrder(
|
||||
tenantId: string,
|
||||
poId: string
|
||||
): Promise<{ message: string }> {
|
||||
return apiClient.delete<{ message: string }>(
|
||||
`/tenants/${tenantId}/purchase-orders/${poId}`
|
||||
);
|
||||
}
|
||||
@@ -104,6 +104,19 @@ class SuppliersService {
|
||||
);
|
||||
}
|
||||
|
||||
async getSupplierProducts(
|
||||
tenantId: string,
|
||||
supplierId: string,
|
||||
isActive: boolean = true
|
||||
): Promise<Array<{ inventory_product_id: string }>> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('is_active', isActive.toString());
|
||||
|
||||
return apiClient.get<Array<{ inventory_product_id: string }>>(
|
||||
`${this.baseUrl}/${tenantId}/suppliers/suppliers/${supplierId}/products?${params.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ATOMIC: Purchase Orders CRUD
|
||||
// Backend: services/suppliers/app/api/purchase_orders.py
|
||||
|
||||
Reference in New Issue
Block a user