Improve the frontend 2
This commit is contained in:
@@ -10,7 +10,7 @@ import { inventoryService } from '../services/inventory';
|
||||
import { getAlertAnalytics } from '../services/alert_analytics';
|
||||
import { getSustainabilityWidgetData } from '../services/sustainability';
|
||||
import { ApiError } from '../client/apiClient';
|
||||
import type { InventoryDashboardSummary } from '../types/dashboard';
|
||||
import type { InventoryDashboardSummary } from '../types/inventory';
|
||||
import type { AlertAnalytics } from '../services/alert_analytics';
|
||||
import type { SalesAnalytics } from '../types/sales';
|
||||
import type { OrdersDashboardSummary } from '../types/orders';
|
||||
@@ -106,25 +106,12 @@ function calculateTrend(current: number, previous: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate today's sales from sales records
|
||||
* Calculate today's sales from sales records (REMOVED - Professional/Enterprise tier feature)
|
||||
* Basic tier users don't get sales analytics on dashboard
|
||||
*/
|
||||
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),
|
||||
};
|
||||
function calculateTodaySales(): { amount: number; trend: number; productsSold: number; productsTrend: number } {
|
||||
// Return zero values - sales analytics not available for basic tier
|
||||
return { amount: 0, trend: 0, productsSold: 0, productsTrend: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,27 +122,27 @@ function calculateOrdersMetrics(ordersData?: OrdersDashboardSummary): { pending:
|
||||
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;
|
||||
const pendingCount = ordersData.pending_orders || 0;
|
||||
const todayCount = ordersData.total_orders_today || 0;
|
||||
|
||||
return {
|
||||
pending: pendingCount,
|
||||
today: todayCount,
|
||||
trend: calculateTrend(todayCount, yesterdayCount),
|
||||
trend: 0, // Trend calculation removed - needs historical data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate dashboard data from all services
|
||||
* NOTE: Sales analytics removed - Professional/Enterprise tier feature
|
||||
*/
|
||||
function aggregateDashboardStats(data: AggregatedDashboardData): DashboardStats {
|
||||
const sales = calculateTodaySales(data.sales);
|
||||
const sales = calculateTodaySales(); // Returns zeros for basic tier
|
||||
const orders = calculateOrdersMetrics(data.orders);
|
||||
|
||||
const criticalStockCount =
|
||||
(data.inventory?.low_stock_count || 0) +
|
||||
(data.inventory?.out_of_stock_count || 0);
|
||||
(data.inventory?.low_stock_items || 0) +
|
||||
(data.inventory?.out_of_stock_items || 0);
|
||||
|
||||
return {
|
||||
// Alerts
|
||||
@@ -167,20 +154,20 @@ function aggregateDashboardStats(data: AggregatedDashboardData): DashboardStats
|
||||
ordersToday: orders.today,
|
||||
ordersTrend: orders.trend,
|
||||
|
||||
// Sales
|
||||
salesToday: sales.amount,
|
||||
salesTrend: sales.trend,
|
||||
salesCurrency: '€', // Default to EUR for bakery
|
||||
// Sales (REMOVED - not available for basic tier)
|
||||
salesToday: 0,
|
||||
salesTrend: 0,
|
||||
salesCurrency: '€',
|
||||
|
||||
// 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,
|
||||
lowStockCount: data.inventory?.low_stock_items || 0,
|
||||
outOfStockCount: data.inventory?.out_of_stock_items || 0,
|
||||
expiringSoon: data.inventory?.expiring_soon_items || 0,
|
||||
|
||||
// Products
|
||||
productsSoldToday: sales.productsSold,
|
||||
productsSoldTrend: sales.productsTrend,
|
||||
// Products (REMOVED - not available for basic tier)
|
||||
productsSoldToday: 0,
|
||||
productsSoldTrend: 0,
|
||||
|
||||
// Sustainability
|
||||
wasteReductionPercentage: data.sustainability?.waste_reduction_percentage,
|
||||
@@ -209,17 +196,13 @@ export const useDashboardStats = (
|
||||
return useQuery<DashboardStats, ApiError>({
|
||||
queryKey: dashboardKeys.stats(tenantId),
|
||||
queryFn: async () => {
|
||||
// Fetch all data in parallel
|
||||
const [alertsData, ordersData, salesData, inventoryData, sustainabilityData] = await Promise.allSettled([
|
||||
// Fetch all data in parallel (REMOVED sales analytics - Professional/Enterprise tier only)
|
||||
const [alertsData, ordersData, inventoryData, sustainabilityData] = 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),
|
||||
getSustainabilityWidgetData(tenantId, 30), // 30 days for monthly savings
|
||||
]);
|
||||
@@ -228,7 +211,7 @@ export const useDashboardStats = (
|
||||
const aggregatedData: AggregatedDashboardData = {
|
||||
alerts: alertsData.status === 'fulfilled' ? alertsData.value : undefined,
|
||||
orders: ordersData.status === 'fulfilled' ? ordersData.value : undefined,
|
||||
sales: salesData.status === 'fulfilled' ? salesData.value : undefined,
|
||||
sales: undefined, // REMOVED - Professional/Enterprise tier only
|
||||
inventory: inventoryData.status === 'fulfilled' ? inventoryData.value : undefined,
|
||||
sustainability: sustainabilityData.status === 'fulfilled' ? sustainabilityData.value : undefined,
|
||||
};
|
||||
@@ -240,9 +223,6 @@ export const useDashboardStats = (
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { equipmentService } from '../services/equipment';
|
||||
import type { Equipment } from '../types/equipment';
|
||||
import type { Equipment, EquipmentDeletionSummary } from '../types/equipment';
|
||||
|
||||
// Query Keys
|
||||
export const equipmentKeys = {
|
||||
@@ -114,7 +114,7 @@ export function useUpdateEquipment(tenantId: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to delete equipment
|
||||
* Hook to delete equipment (soft delete)
|
||||
*/
|
||||
export function useDeleteEquipment(tenantId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -139,3 +139,46 @@ export function useDeleteEquipment(tenantId: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to hard delete equipment (permanent deletion)
|
||||
*/
|
||||
export function useHardDeleteEquipment(tenantId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (equipmentId: string) =>
|
||||
equipmentService.hardDeleteEquipment(tenantId, equipmentId),
|
||||
onSuccess: (_, equipmentId) => {
|
||||
// Remove from cache
|
||||
queryClient.removeQueries({
|
||||
queryKey: equipmentKeys.detail(tenantId, equipmentId)
|
||||
});
|
||||
|
||||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: equipmentKeys.lists() });
|
||||
|
||||
toast.success('Equipment permanently deleted');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error('Error hard deleting equipment:', error);
|
||||
toast.error(error.response?.data?.detail || 'Error permanently deleting equipment');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get equipment deletion summary
|
||||
*/
|
||||
export function useEquipmentDeletionSummary(
|
||||
tenantId: string,
|
||||
equipmentId: string,
|
||||
options?: { enabled?: boolean }
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...equipmentKeys.detail(tenantId, equipmentId), 'deletion-summary'],
|
||||
queryFn: () => equipmentService.getEquipmentDeletionSummary(tenantId, equipmentId),
|
||||
enabled: !!tenantId && !!equipmentId && (options?.enabled ?? true),
|
||||
staleTime: 0, // Always fetch fresh data for dependency checks
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Subscription hook for checking plan features and limits
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { subscriptionService } from '../services/subscription';
|
||||
import {
|
||||
SUBSCRIPTION_TIERS,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../types/subscription';
|
||||
import { useCurrentTenant } from '../../stores';
|
||||
import { useAuthUser } from '../../stores/auth.store';
|
||||
import { useSubscriptionEvents } from '../../contexts/SubscriptionEventsContext';
|
||||
|
||||
export interface SubscriptionFeature {
|
||||
hasFeature: boolean;
|
||||
@@ -40,9 +41,10 @@ export const useSubscription = () => {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const user = useAuthUser();
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
const { notifySubscriptionChanged } = useSubscriptionEvents();
|
||||
|
||||
// Load subscription data
|
||||
const loadSubscriptionData = useCallback(async () => {
|
||||
@@ -62,6 +64,9 @@ export const useSubscription = () => {
|
||||
features: usageSummary.usage || {},
|
||||
loading: false,
|
||||
});
|
||||
|
||||
// Notify subscribers that subscription data has changed
|
||||
notifySubscriptionChanged();
|
||||
} catch (error) {
|
||||
console.error('Error loading subscription data:', error);
|
||||
setSubscriptionInfo(prev => ({
|
||||
@@ -70,7 +75,7 @@ export const useSubscription = () => {
|
||||
error: 'Failed to load subscription data'
|
||||
}));
|
||||
}
|
||||
}, [tenantId]);
|
||||
}, [tenantId, notifySubscriptionChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscriptionData();
|
||||
@@ -177,4 +182,4 @@ export const useSubscription = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export default useSubscription;
|
||||
export default useSubscription;
|
||||
|
||||
@@ -26,6 +26,9 @@ import type {
|
||||
DeliveryReceiptConfirmation,
|
||||
DeliverySearchParams,
|
||||
PerformanceMetric,
|
||||
SupplierPriceListCreate,
|
||||
SupplierPriceListUpdate,
|
||||
SupplierPriceListResponse,
|
||||
} from '../types/suppliers';
|
||||
|
||||
// Query Keys Factory
|
||||
@@ -228,6 +231,37 @@ export const useDelivery = (
|
||||
});
|
||||
};
|
||||
|
||||
// Supplier Price List Queries
|
||||
export const useSupplierPriceLists = (
|
||||
tenantId: string,
|
||||
supplierId: string,
|
||||
isActive: boolean = true,
|
||||
options?: Omit<UseQueryOptions<SupplierPriceListResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<SupplierPriceListResponse[], ApiError>({
|
||||
queryKey: [...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-lists', isActive],
|
||||
queryFn: () => suppliersService.getSupplierPriceLists(tenantId, supplierId, isActive),
|
||||
enabled: !!tenantId && !!supplierId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSupplierPriceList = (
|
||||
tenantId: string,
|
||||
supplierId: string,
|
||||
priceListId: string,
|
||||
options?: Omit<UseQueryOptions<SupplierPriceListResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<SupplierPriceListResponse, ApiError>({
|
||||
queryKey: [...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-list', priceListId],
|
||||
queryFn: () => suppliersService.getSupplierPriceList(tenantId, supplierId, priceListId),
|
||||
enabled: !!tenantId && !!supplierId && !!priceListId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Performance Queries
|
||||
export const useSupplierPerformanceMetrics = (
|
||||
tenantId: string,
|
||||
@@ -236,7 +270,7 @@ export const useSupplierPerformanceMetrics = (
|
||||
) => {
|
||||
return useQuery<PerformanceMetric[], ApiError>({
|
||||
queryKey: suppliersKeys.performance.metrics(tenantId, supplierId),
|
||||
queryFn: () => suppliersService.getPerformanceMetrics(tenantId, supplierId),
|
||||
queryFn: () => suppliersService.getSupplierPerformanceMetrics(tenantId, supplierId),
|
||||
enabled: !!tenantId && !!supplierId,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
...options,
|
||||
@@ -245,13 +279,13 @@ export const useSupplierPerformanceMetrics = (
|
||||
|
||||
export const usePerformanceAlerts = (
|
||||
tenantId: string,
|
||||
supplierId: string,
|
||||
supplierId?: string,
|
||||
options?: Omit<UseQueryOptions<any[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<any[], ApiError>({
|
||||
queryKey: suppliersKeys.performance.alerts(tenantId, supplierId),
|
||||
queryFn: () => suppliersService.evaluatePerformanceAlerts(tenantId, supplierId),
|
||||
enabled: !!tenantId && !!supplierId,
|
||||
queryFn: () => suppliersService.getPerformanceAlerts(tenantId, supplierId),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
...options,
|
||||
});
|
||||
@@ -607,12 +641,108 @@ export const useConfirmDeliveryReceipt = (
|
||||
});
|
||||
};
|
||||
|
||||
// Supplier Price List Mutations
|
||||
export const useCreateSupplierPriceList = (
|
||||
options?: UseMutationOptions<
|
||||
SupplierPriceListResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; priceListData: SupplierPriceListCreate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
SupplierPriceListResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; priceListData: SupplierPriceListCreate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, supplierId, priceListData }) =>
|
||||
suppliersService.createSupplierPriceList(tenantId, supplierId, priceListData),
|
||||
onSuccess: (data, { tenantId, supplierId }) => {
|
||||
// Add to cache
|
||||
queryClient.setQueryData(
|
||||
[...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-list', data.id],
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate price lists
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-lists']
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSupplierPriceList = (
|
||||
options?: UseMutationOptions<
|
||||
SupplierPriceListResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; priceListId: string; priceListData: SupplierPriceListUpdate }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
SupplierPriceListResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; priceListId: string; priceListData: SupplierPriceListUpdate }
|
||||
>({
|
||||
mutationFn: ({ tenantId, supplierId, priceListId, priceListData }) =>
|
||||
suppliersService.updateSupplierPriceList(tenantId, supplierId, priceListId, priceListData),
|
||||
onSuccess: (data, { tenantId, supplierId, priceListId }) => {
|
||||
// Update cache
|
||||
queryClient.setQueryData(
|
||||
[...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-list', priceListId],
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate price lists
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-lists']
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSupplierPriceList = (
|
||||
options?: UseMutationOptions<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; priceListId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
{ message: string },
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; priceListId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, supplierId, priceListId }) =>
|
||||
suppliersService.deleteSupplierPriceList(tenantId, supplierId, priceListId),
|
||||
onSuccess: (_, { tenantId, supplierId, priceListId }) => {
|
||||
// Remove from cache
|
||||
queryClient.removeQueries({
|
||||
queryKey: [...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-list', priceListId]
|
||||
});
|
||||
|
||||
// Invalidate price lists
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...suppliersKeys.suppliers.detail(tenantId, supplierId), 'price-lists']
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Performance Mutations
|
||||
export const useCalculateSupplierPerformance = (
|
||||
options?: UseMutationOptions<
|
||||
{ message: string; calculation_id: string },
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; request?: PerformanceCalculationRequest }
|
||||
{ tenantId: string; supplierId: string; request?: any }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -620,7 +750,7 @@ export const useCalculateSupplierPerformance = (
|
||||
return useMutation<
|
||||
{ message: string; calculation_id: string },
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; request?: PerformanceCalculationRequest }
|
||||
{ tenantId: string; supplierId: string; request?: any }
|
||||
>({
|
||||
mutationFn: ({ tenantId, supplierId, request }) =>
|
||||
suppliersService.calculateSupplierPerformance(tenantId, supplierId, request),
|
||||
@@ -641,7 +771,7 @@ export const useEvaluatePerformanceAlerts = (
|
||||
options?: UseMutationOptions<
|
||||
{ alerts_generated: number; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string }
|
||||
{ tenantId: string; supplierId?: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -649,7 +779,7 @@ export const useEvaluatePerformanceAlerts = (
|
||||
return useMutation<
|
||||
{ alerts_generated: number; message: string },
|
||||
ApiError,
|
||||
{ tenantId: string }
|
||||
{ tenantId: string; supplierId?: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, supplierId }) => suppliersService.evaluatePerformanceAlerts(tenantId, supplierId),
|
||||
onSuccess: (_, { tenantId }) => {
|
||||
@@ -677,11 +807,7 @@ export const useActiveSuppliersCount = (tenantId: string) => {
|
||||
return statistics?.active_suppliers || 0;
|
||||
};
|
||||
|
||||
export const usePendingOrdersCount = (supplierId?: string) => {
|
||||
const { data: orders } = usePurchaseOrders({
|
||||
supplier_id: supplierId,
|
||||
status: 'pending_approval',
|
||||
limit: 1000
|
||||
});
|
||||
return orders?.data?.length || 0;
|
||||
};
|
||||
export const usePendingOrdersCount = (queryParams?: PurchaseOrderSearchParams) => {
|
||||
const { data: orders } = usePurchaseOrders('', queryParams);
|
||||
return orders?.length || 0;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user