Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

@@ -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);
}

View File

@@ -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
});
}

View File

@@ -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;

View File

@@ -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;
};