Improve the frontend

This commit is contained in:
Urtzi Alfaro
2025-10-21 19:50:07 +02:00
parent 05da20357d
commit 8d30172483
105 changed files with 14699 additions and 4630 deletions

View 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,
});
};

View 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,
});
};