Improve the frontend modals

This commit is contained in:
Urtzi Alfaro
2025-10-27 16:33:26 +01:00
parent 61376b7a9f
commit 858d985c92
143 changed files with 9289 additions and 2306 deletions

View File

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

View File

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

View File

@@ -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
*/

View File

@@ -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()

View File

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