Improve the frontend modals
This commit is contained in:
@@ -8,11 +8,13 @@ import { useSalesAnalytics } from './sales';
|
||||
import { useOrdersDashboard } from './orders';
|
||||
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 { AlertAnalytics } from '../services/alert_analytics';
|
||||
import type { SalesAnalytics } from '../types/sales';
|
||||
import type { OrdersDashboardSummary } from '../types/orders';
|
||||
import type { SustainabilityWidgetData } from '../types/sustainability';
|
||||
|
||||
export interface DashboardStats {
|
||||
// Alert metrics
|
||||
@@ -39,6 +41,10 @@ export interface DashboardStats {
|
||||
productsSoldToday: number;
|
||||
productsSoldTrend: number;
|
||||
|
||||
// Sustainability metrics
|
||||
wasteReductionPercentage?: number;
|
||||
monthlySavingsEur?: number;
|
||||
|
||||
// Data freshness
|
||||
lastUpdated: string;
|
||||
}
|
||||
@@ -48,6 +54,7 @@ interface AggregatedDashboardData {
|
||||
orders?: OrdersDashboardSummary;
|
||||
sales?: SalesAnalytics;
|
||||
inventory?: InventoryDashboardSummary;
|
||||
sustainability?: SustainabilityWidgetData;
|
||||
}
|
||||
|
||||
// Query Keys
|
||||
@@ -175,6 +182,10 @@ function aggregateDashboardStats(data: AggregatedDashboardData): DashboardStats
|
||||
productsSoldToday: sales.productsSold,
|
||||
productsSoldTrend: sales.productsTrend,
|
||||
|
||||
// Sustainability
|
||||
wasteReductionPercentage: data.sustainability?.waste_reduction_percentage,
|
||||
monthlySavingsEur: data.sustainability?.financial_savings_eur,
|
||||
|
||||
// Metadata
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
@@ -199,7 +210,7 @@ export const useDashboardStats = (
|
||||
queryKey: dashboardKeys.stats(tenantId),
|
||||
queryFn: async () => {
|
||||
// Fetch all data in parallel
|
||||
const [alertsData, ordersData, salesData, inventoryData] = await Promise.allSettled([
|
||||
const [alertsData, ordersData, salesData, inventoryData, sustainabilityData] = await Promise.allSettled([
|
||||
getAlertAnalytics(tenantId, 7),
|
||||
// Note: OrdersService methods are static
|
||||
import('../services/orders').then(({ OrdersService }) =>
|
||||
@@ -210,6 +221,7 @@ export const useDashboardStats = (
|
||||
salesService.getSalesAnalytics(tenantId, todayStr, todayStr)
|
||||
),
|
||||
inventoryService.getDashboardSummary(tenantId),
|
||||
getSustainabilityWidgetData(tenantId, 30), // 30 days for monthly savings
|
||||
]);
|
||||
|
||||
// Extract data or use undefined for failed requests
|
||||
@@ -218,6 +230,7 @@ export const useDashboardStats = (
|
||||
orders: ordersData.status === 'fulfilled' ? ordersData.value : undefined,
|
||||
sales: salesData.status === 'fulfilled' ? salesData.value : undefined,
|
||||
inventory: inventoryData.status === 'fulfilled' ? inventoryData.value : undefined,
|
||||
sustainability: sustainabilityData.status === 'fulfilled' ? sustainabilityData.value : undefined,
|
||||
};
|
||||
|
||||
// Log any failures for debugging
|
||||
@@ -233,6 +246,9 @@ export const useDashboardStats = (
|
||||
if (inventoryData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch inventory:', inventoryData.reason);
|
||||
}
|
||||
if (sustainabilityData.status === 'rejected') {
|
||||
console.warn('[Dashboard] Failed to fetch sustainability:', sustainabilityData.reason);
|
||||
}
|
||||
|
||||
return aggregateDashboardStats(aggregatedData);
|
||||
},
|
||||
|
||||
@@ -259,14 +259,12 @@ export const useCreateCustomer = (
|
||||
return useMutation<CustomerResponse, ApiError, CustomerCreate>({
|
||||
mutationFn: (customerData: CustomerCreate) => OrdersService.createCustomer(customerData),
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate customers list for this tenant
|
||||
// Invalidate all customer list queries for this tenant
|
||||
// This will match any query with ['orders', 'customers', 'list', ...]
|
||||
// refetchType: 'active' forces immediate refetch of mounted queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.customers(),
|
||||
predicate: (query) => {
|
||||
const queryKey = query.queryKey as string[];
|
||||
return queryKey.includes('list') &&
|
||||
JSON.stringify(queryKey).includes(variables.tenant_id);
|
||||
},
|
||||
refetchType: 'active',
|
||||
});
|
||||
|
||||
// Add the new customer to cache
|
||||
@@ -278,6 +276,7 @@ export const useCreateCustomer = (
|
||||
// Invalidate dashboard (for customer metrics)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.dashboard(variables.tenant_id),
|
||||
refetchType: 'active',
|
||||
});
|
||||
},
|
||||
...options,
|
||||
@@ -298,14 +297,18 @@ export const useUpdateCustomer = (
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate customers list for this tenant
|
||||
// Invalidate all customer list queries
|
||||
// This will match any query with ['orders', 'customers', 'list', ...]
|
||||
// refetchType: 'active' forces immediate refetch of mounted queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.customers(),
|
||||
predicate: (query) => {
|
||||
const queryKey = query.queryKey as string[];
|
||||
return queryKey.includes('list') &&
|
||||
JSON.stringify(queryKey).includes(variables.tenantId);
|
||||
},
|
||||
refetchType: 'active',
|
||||
});
|
||||
|
||||
// Invalidate dashboard (for customer metrics)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersKeys.dashboard(variables.tenantId),
|
||||
refetchType: 'active',
|
||||
});
|
||||
},
|
||||
...options,
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
RecipeFeasibilityResponse,
|
||||
RecipeStatisticsResponse,
|
||||
RecipeCategoriesResponse,
|
||||
RecipeDeletionSummary,
|
||||
} from '../types/recipes';
|
||||
|
||||
// Query Keys Factory
|
||||
@@ -225,6 +226,46 @@ export const useDeleteRecipe = (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Archive a recipe (soft delete)
|
||||
*/
|
||||
export const useArchiveRecipe = (
|
||||
tenantId: string,
|
||||
options?: UseMutationOptions<RecipeResponse, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<RecipeResponse, ApiError, string>({
|
||||
mutationFn: (recipeId: string) => recipesService.archiveRecipe(tenantId, recipeId),
|
||||
onSuccess: (data) => {
|
||||
// Update individual recipe cache
|
||||
queryClient.setQueryData(recipesKeys.detail(tenantId, data.id), data);
|
||||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: recipesKeys.lists(tenantId) });
|
||||
// Invalidate statistics
|
||||
queryClient.invalidateQueries({ queryKey: recipesKeys.statistics(tenantId) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get deletion summary for a recipe
|
||||
*/
|
||||
export const useRecipeDeletionSummary = (
|
||||
tenantId: string,
|
||||
recipeId: string,
|
||||
options?: Omit<UseQueryOptions<RecipeDeletionSummary, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<RecipeDeletionSummary, ApiError>({
|
||||
queryKey: [...recipesKeys.detail(tenantId, recipeId), 'deletion-summary'],
|
||||
queryFn: () => recipesService.getRecipeDeletionSummary(tenantId, recipeId),
|
||||
enabled: !!(tenantId && recipeId),
|
||||
staleTime: 0, // Always fetch fresh data
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Duplicate a recipe
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
SupplierApproval,
|
||||
SupplierSearchParams,
|
||||
SupplierStatistics,
|
||||
SupplierDeletionSummary,
|
||||
PurchaseOrderCreate,
|
||||
PurchaseOrderUpdate,
|
||||
PurchaseOrderResponse,
|
||||
@@ -324,6 +325,41 @@ export const useUpdateSupplier = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useApproveSupplier = (
|
||||
options?: UseMutationOptions<
|
||||
SupplierResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; approvalData: SupplierApproval }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
SupplierResponse,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; approvalData: SupplierApproval }
|
||||
>({
|
||||
mutationFn: ({ tenantId, supplierId, approvalData }) =>
|
||||
suppliersService.approveSupplier(tenantId, supplierId, approvalData),
|
||||
onSuccess: (data, { tenantId, supplierId }) => {
|
||||
// Update cache with new supplier status
|
||||
queryClient.setQueryData(
|
||||
suppliersKeys.suppliers.detail(tenantId, supplierId),
|
||||
data
|
||||
);
|
||||
|
||||
// Invalidate lists and statistics as approval changes counts
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: suppliersKeys.suppliers.lists()
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: suppliersKeys.suppliers.statistics(tenantId)
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSupplier = (
|
||||
options?: UseMutationOptions<
|
||||
{ message: string },
|
||||
@@ -358,29 +394,28 @@ export const useDeleteSupplier = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useApproveSupplier = (
|
||||
export const useHardDeleteSupplier = (
|
||||
options?: UseMutationOptions<
|
||||
SupplierResponse,
|
||||
SupplierDeletionSummary,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; approval: SupplierApproval }
|
||||
{ tenantId: string; supplierId: string }
|
||||
>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
SupplierResponse,
|
||||
SupplierDeletionSummary,
|
||||
ApiError,
|
||||
{ tenantId: string; supplierId: string; approval: SupplierApproval }
|
||||
{ tenantId: string; supplierId: string }
|
||||
>({
|
||||
mutationFn: ({ tenantId, supplierId, approval }) =>
|
||||
suppliersService.approveSupplier(tenantId, supplierId, approval),
|
||||
onSuccess: (data, { tenantId, supplierId }) => {
|
||||
// Update cache
|
||||
queryClient.setQueryData(
|
||||
suppliersKeys.suppliers.detail(tenantId, supplierId),
|
||||
data
|
||||
);
|
||||
|
||||
mutationFn: ({ tenantId, supplierId }) =>
|
||||
suppliersService.hardDeleteSupplier(tenantId, supplierId),
|
||||
onSuccess: (_, { tenantId, supplierId }) => {
|
||||
// Remove from cache
|
||||
queryClient.removeQueries({
|
||||
queryKey: suppliersKeys.suppliers.detail(tenantId, supplierId)
|
||||
});
|
||||
|
||||
// Invalidate lists and statistics
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: suppliersKeys.suppliers.lists()
|
||||
|
||||
@@ -12,6 +12,7 @@ export const userKeys = {
|
||||
all: ['user'] as const,
|
||||
current: () => [...userKeys.all, 'current'] as const,
|
||||
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
|
||||
activity: (id: string) => [...userKeys.all, 'activity', id] as const,
|
||||
admin: {
|
||||
all: () => [...userKeys.all, 'admin'] as const,
|
||||
list: () => [...userKeys.admin.all(), 'list'] as const,
|
||||
@@ -31,6 +32,19 @@ export const useCurrentUser = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserActivity = (
|
||||
userId: string,
|
||||
options?: Omit<UseQueryOptions<any, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<any, ApiError>({
|
||||
queryKey: userKeys.activity(userId),
|
||||
queryFn: () => userService.getUserActivity(userId),
|
||||
enabled: !!userId,
|
||||
staleTime: 1 * 60 * 1000, // 1 minute for activity data
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAllUsers = (
|
||||
options?: Omit<UseQueryOptions<UserResponse[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
@@ -109,4 +123,4 @@ export const useAdminDeleteUser = (
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user