Start integrating the onboarding flow with backend 6

This commit is contained in:
Urtzi Alfaro
2025-09-05 17:49:48 +02:00
parent 236c3a32ae
commit 069954981a
131 changed files with 5217 additions and 22838 deletions

View File

@@ -1,620 +0,0 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
export type AlertType = 'info' | 'success' | 'warning' | 'error' | 'critical';
export type AlertCategory = 'system' | 'production' | 'inventory' | 'quality' | 'financial' | 'maintenance' | 'safety';
export type AlertPriority = 'low' | 'medium' | 'high' | 'urgent';
export type AlertStatus = 'unread' | 'read' | 'acknowledged' | 'resolved' | 'dismissed';
export interface Alert {
id: string;
type: AlertType;
category: AlertCategory;
priority: AlertPriority;
status: AlertStatus;
title: string;
message: string;
description?: string;
source: string; // service or component that generated the alert
sourceId?: string; // specific entity ID
timestamp: string;
resolvedAt?: string;
acknowledgedAt?: string;
acknowledgedBy?: string;
resolvedBy?: string;
metadata?: Record<string, any>;
actions?: AlertAction[];
tags?: string[];
expiresAt?: string;
isRecurring?: boolean;
recurringPattern?: string;
relatedAlerts?: string[];
}
export interface AlertAction {
id: string;
label: string;
type: 'button' | 'link' | 'api_call';
variant?: 'primary' | 'secondary' | 'danger';
action: string; // URL or action identifier
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
payload?: Record<string, any>;
requiresConfirmation?: boolean;
confirmationMessage?: string;
}
export interface AlertRule {
id: string;
name: string;
description: string;
isEnabled: boolean;
category: AlertCategory;
conditions: AlertCondition[];
actions: AlertRuleAction[];
cooldownMinutes?: number;
maxFrequency?: {
count: number;
period: 'hour' | 'day' | 'week';
};
recipients?: string[];
schedule?: {
timezone: string;
activeHours?: {
start: string; // HH:mm
end: string; // HH:mm
};
activeDays?: number[]; // 0-6, Sunday = 0
};
}
export interface AlertCondition {
field: string;
operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'in' | 'not_in';
value: any;
logic?: 'and' | 'or';
}
export interface AlertRuleAction {
type: 'create_alert' | 'send_email' | 'send_sms' | 'webhook' | 'auto_resolve';
config: Record<string, any>;
}
export interface AlertsState {
// Data
alerts: Alert[];
rules: AlertRule[];
// Filters and pagination
filters: {
status?: AlertStatus[];
type?: AlertType[];
category?: AlertCategory[];
priority?: AlertPriority[];
dateRange?: {
start: string;
end: string;
};
search?: string;
};
sortBy: 'timestamp' | 'priority' | 'status' | 'type';
sortOrder: 'asc' | 'desc';
currentPage: number;
itemsPerPage: number;
// Loading states
loading: {
alerts: boolean;
rules: boolean;
actions: boolean;
};
// Error states
errors: {
alerts: string | null;
rules: string | null;
actions: string | null;
};
// Settings
settings: {
autoRefreshInterval: number; // seconds
soundEnabled: boolean;
desktopNotifications: boolean;
maxAlertsToKeep: number;
autoAcknowledgeResolved: boolean;
};
// Actions - Alerts
loadAlerts: (filters?: any) => Promise<void>;
createAlert: (alert: Omit<Alert, 'id' | 'timestamp' | 'status'>) => Promise<void>;
updateAlert: (id: string, updates: Partial<Alert>) => Promise<void>;
deleteAlert: (id: string) => Promise<void>;
acknowledgeAlert: (id: string) => Promise<void>;
resolveAlert: (id: string, resolution?: string) => Promise<void>;
dismissAlert: (id: string) => Promise<void>;
bulkUpdateAlerts: (ids: string[], updates: Partial<Alert>) => Promise<void>;
// Actions - Rules
loadRules: () => Promise<void>;
createRule: (rule: Omit<AlertRule, 'id'>) => Promise<void>;
updateRule: (id: string, updates: Partial<AlertRule>) => Promise<void>;
deleteRule: (id: string) => Promise<void>;
toggleRule: (id: string) => Promise<void>;
// Actions - Filters and UI
setFilters: (filters: Partial<AlertsState['filters']>) => void;
clearFilters: () => void;
setSorting: (sortBy: AlertsState['sortBy'], sortOrder: AlertsState['sortOrder']) => void;
setPage: (page: number) => void;
setItemsPerPage: (count: number) => void;
// Actions - Settings
updateSettings: (settings: Partial<AlertsState['settings']>) => void;
// Utilities
getUnreadCount: () => number;
getCriticalCount: () => number;
getAlertsByCategory: () => Record<AlertCategory, Alert[]>;
clearErrors: () => void;
reset: () => void;
}
// Mock data
const mockRules: AlertRule[] = [
{
id: 'rule-1',
name: 'Stock Bajo',
description: 'Alerta cuando el inventario está por debajo del mínimo',
isEnabled: true,
category: 'inventory',
conditions: [
{
field: 'current_stock',
operator: 'lt',
value: 'minimum_stock',
},
],
actions: [
{
type: 'create_alert',
config: {
type: 'warning',
priority: 'medium',
title: 'Stock bajo detectado',
},
},
],
recipients: ['inventory@bakery.com'],
},
];
const mockAlerts: Alert[] = [
{
id: 'alert-1',
type: 'warning',
category: 'inventory',
priority: 'medium',
status: 'unread',
title: 'Stock bajo: Harina de trigo',
message: 'El inventario de harina de trigo está por debajo del mínimo (5kg restantes)',
source: 'inventory-service',
sourceId: 'ingredient-1',
timestamp: new Date().toISOString(),
metadata: {
currentStock: 5,
minimumStock: 20,
unit: 'kg',
},
actions: [
{
id: 'action-1',
label: 'Generar orden de compra',
type: 'api_call',
variant: 'primary',
action: '/api/procurement/orders',
method: 'POST',
payload: { ingredientId: 'ingredient-1' },
},
{
id: 'action-2',
label: 'Ver detalles de inventario',
type: 'link',
action: '/inventory/ingredients/ingredient-1',
},
],
tags: ['inventory', 'ingredients'],
},
{
id: 'alert-2',
type: 'error',
category: 'production',
priority: 'high',
status: 'unread',
title: 'Fallo en lote de producción',
message: 'El lote #B123456 ha fallado en el control de calidad',
source: 'production-service',
sourceId: 'batch-123456',
timestamp: new Date(Date.now() - 1800000).toISOString(), // 30 minutes ago
metadata: {
batchNumber: 'B123456',
recipe: 'Pan de molde',
stage: 'quality_check',
},
actions: [
{
id: 'action-3',
label: 'Ver lote',
type: 'link',
action: '/production/batches/batch-123456',
},
{
id: 'action-4',
label: 'Reprocesar',
type: 'api_call',
variant: 'primary',
action: '/api/production/batches/batch-123456/reprocess',
method: 'POST',
requiresConfirmation: true,
confirmationMessage: '¿Estás seguro de que quieres reprocesar este lote?',
},
],
tags: ['production', 'quality'],
},
{
id: 'alert-3',
type: 'info',
category: 'system',
priority: 'low',
status: 'read',
title: 'Mantenimiento programado',
message: 'Mantenimiento del sistema programado para el domingo a las 02:00',
source: 'system',
timestamp: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
metadata: {
maintenanceDate: '2024-01-21T02:00:00Z',
duration: 120, // minutes
},
tags: ['maintenance', 'scheduled'],
},
];
export const useAlertsStore = create<AlertsState>()(
persist(
(set, get) => ({
// Initial state
alerts: [],
rules: [],
filters: {},
sortBy: 'timestamp',
sortOrder: 'desc',
currentPage: 1,
itemsPerPage: 25,
loading: {
alerts: false,
rules: false,
actions: false,
},
errors: {
alerts: null,
rules: null,
actions: null,
},
settings: {
autoRefreshInterval: 30,
soundEnabled: true,
desktopNotifications: true,
maxAlertsToKeep: 1000,
autoAcknowledgeResolved: true,
},
// Alert actions
loadAlerts: async (filters?: any) => {
set((state) => ({ loading: { ...state.loading, alerts: true } }));
try {
await new Promise(resolve => setTimeout(resolve, 500));
set({
alerts: mockAlerts,
loading: { ...get().loading, alerts: false },
errors: { ...get().errors, alerts: null },
});
} catch (error) {
set({
loading: { ...get().loading, alerts: false },
errors: { ...get().errors, alerts: 'Failed to load alerts' },
});
}
},
createAlert: async (alert: Omit<Alert, 'id' | 'timestamp' | 'status'>) => {
const newAlert: Alert = {
...alert,
id: `alert-${Date.now()}`,
timestamp: new Date().toISOString(),
status: 'unread',
};
set((state) => ({
alerts: [newAlert, ...state.alerts],
}));
// Show desktop notification if enabled
const { settings } = get();
if (settings.desktopNotifications && 'Notification' in window && Notification.permission === 'granted') {
new Notification(newAlert.title, {
body: newAlert.message,
icon: '/favicon.ico',
});
}
},
updateAlert: async (id: string, updates: Partial<Alert>) => {
set((state) => ({
alerts: state.alerts.map(alert =>
alert.id === id ? { ...alert, ...updates } : alert
),
}));
},
deleteAlert: async (id: string) => {
set((state) => ({
alerts: state.alerts.filter(alert => alert.id !== id),
}));
},
acknowledgeAlert: async (id: string) => {
await get().updateAlert(id, {
status: 'acknowledged',
acknowledgedAt: new Date().toISOString(),
acknowledgedBy: 'current-user', // Would get from auth store
});
},
resolveAlert: async (id: string, resolution?: string) => {
await get().updateAlert(id, {
status: 'resolved',
resolvedAt: new Date().toISOString(),
resolvedBy: 'current-user', // Would get from auth store
metadata: {
...get().alerts.find(a => a.id === id)?.metadata,
resolution,
},
});
},
dismissAlert: async (id: string) => {
await get().updateAlert(id, {
status: 'dismissed',
});
},
bulkUpdateAlerts: async (ids: string[], updates: Partial<Alert>) => {
set((state) => ({
alerts: state.alerts.map(alert =>
ids.includes(alert.id) ? { ...alert, ...updates } : alert
),
}));
},
// Rule actions
loadRules: async () => {
set((state) => ({ loading: { ...state.loading, rules: true } }));
try {
await new Promise(resolve => setTimeout(resolve, 300));
set({
rules: mockRules,
loading: { ...get().loading, rules: false },
errors: { ...get().errors, rules: null },
});
} catch (error) {
set({
loading: { ...get().loading, rules: false },
errors: { ...get().errors, rules: 'Failed to load rules' },
});
}
},
createRule: async (rule: Omit<AlertRule, 'id'>) => {
const newRule: AlertRule = {
...rule,
id: `rule-${Date.now()}`,
};
set((state) => ({
rules: [...state.rules, newRule],
}));
},
updateRule: async (id: string, updates: Partial<AlertRule>) => {
set((state) => ({
rules: state.rules.map(rule =>
rule.id === id ? { ...rule, ...updates } : rule
),
}));
},
deleteRule: async (id: string) => {
set((state) => ({
rules: state.rules.filter(rule => rule.id !== id),
}));
},
toggleRule: async (id: string) => {
const rule = get().rules.find(r => r.id === id);
if (rule) {
await get().updateRule(id, { isEnabled: !rule.isEnabled });
}
},
// Filter and UI actions
setFilters: (filters: Partial<AlertsState['filters']>) => {
set((state) => ({
filters: { ...state.filters, ...filters },
currentPage: 1, // Reset to first page when filters change
}));
},
clearFilters: () => {
set({
filters: {},
currentPage: 1,
});
},
setSorting: (sortBy: AlertsState['sortBy'], sortOrder: AlertsState['sortOrder']) => {
set({ sortBy, sortOrder });
},
setPage: (currentPage: number) => {
set({ currentPage });
},
setItemsPerPage: (itemsPerPage: number) => {
set({ itemsPerPage, currentPage: 1 });
},
// Settings actions
updateSettings: (settingsUpdate: Partial<AlertsState['settings']>) => {
set((state) => ({
settings: { ...state.settings, ...settingsUpdate },
}));
},
// Utilities
getUnreadCount: (): number => {
return get().alerts.filter(alert => alert.status === 'unread').length;
},
getCriticalCount: (): number => {
return get().alerts.filter(alert =>
alert.status === 'unread' && (alert.priority === 'urgent' || alert.type === 'critical')
).length;
},
getAlertsByCategory: (): Record<AlertCategory, Alert[]> => {
const alerts = get().alerts;
const categories: AlertCategory[] = ['system', 'production', 'inventory', 'quality', 'financial', 'maintenance', 'safety'];
return categories.reduce((acc, category) => {
acc[category] = alerts.filter(alert => alert.category === category);
return acc;
}, {} as Record<AlertCategory, Alert[]>);
},
clearErrors: () => {
set({
errors: {
alerts: null,
rules: null,
actions: null,
},
});
},
reset: () => {
set({
alerts: [],
rules: [],
filters: {},
sortBy: 'timestamp',
sortOrder: 'desc',
currentPage: 1,
itemsPerPage: 25,
loading: {
alerts: false,
rules: false,
actions: false,
},
errors: {
alerts: null,
rules: null,
actions: null,
},
});
},
}),
{
name: 'alerts-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
filters: state.filters,
sortBy: state.sortBy,
sortOrder: state.sortOrder,
itemsPerPage: state.itemsPerPage,
settings: state.settings,
}),
}
)
);
// Selectors
export const useAlerts = () => useAlertsStore((state) => state.alerts);
export const useAlertRules = () => useAlertsStore((state) => state.rules);
export const useAlertFilters = () => useAlertsStore((state) => state.filters);
export const useAlertSettings = () => useAlertsStore((state) => state.settings);
export const useAlertLoading = () => useAlertsStore((state) => state.loading);
export const useAlertErrors = () => useAlertsStore((state) => state.errors);
// Computed selectors
export const useUnreadAlertsCount = () => useAlertsStore((state) => state.getUnreadCount());
export const useCriticalAlertsCount = () => useAlertsStore((state) => state.getCriticalCount());
export const useAlertsByCategory = () => useAlertsStore((state) => state.getAlertsByCategory());
// Actions hook
export const useAlertActions = () => useAlertsStore((state) => ({
// Alerts
loadAlerts: state.loadAlerts,
createAlert: state.createAlert,
updateAlert: state.updateAlert,
deleteAlert: state.deleteAlert,
acknowledgeAlert: state.acknowledgeAlert,
resolveAlert: state.resolveAlert,
dismissAlert: state.dismissAlert,
bulkUpdateAlerts: state.bulkUpdateAlerts,
// Rules
loadRules: state.loadRules,
createRule: state.createRule,
updateRule: state.updateRule,
deleteRule: state.deleteRule,
toggleRule: state.toggleRule,
// UI
setFilters: state.setFilters,
clearFilters: state.clearFilters,
setSorting: state.setSorting,
setPage: state.setPage,
setItemsPerPage: state.setItemsPerPage,
updateSettings: state.updateSettings,
// Utils
clearErrors: state.clearErrors,
reset: state.reset,
}));
// Auto-refresh setup
if (typeof window !== 'undefined') {
let refreshInterval: NodeJS.Timeout;
useAlertsStore.subscribe((state) => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
if (state.settings.autoRefreshInterval > 0) {
refreshInterval = setInterval(() => {
state.loadAlerts();
}, state.settings.autoRefreshInterval * 1000);
}
});
// Request notification permission if desktop notifications are enabled
useAlertsStore.subscribe((state) => {
if (state.settings.desktopNotifications && 'Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
});
}

View File

@@ -40,7 +40,7 @@ export interface AuthState {
canAccess: (resource: string, action: string) => boolean;
}
import { authService } from '../services/api/auth.service';
import { authService } from '../api';
export const useAuthStore = create<AuthState>()(
persist(
@@ -60,11 +60,11 @@ export const useAuthStore = create<AuthState>()(
const response = await authService.login({ email, password });
if (response.success && response.data) {
if (response && response.access_token) {
set({
user: response.data.user || null,
token: response.data.access_token,
refreshToken: response.data.refresh_token || null,
user: response.user || null,
token: response.access_token,
refreshToken: response.refresh_token || null,
isAuthenticated: true,
isLoading: false,
error: null,
@@ -91,11 +91,11 @@ export const useAuthStore = create<AuthState>()(
const response = await authService.register(userData);
if (response.success && response.data) {
if (response && response.access_token) {
set({
user: response.data.user || null,
token: response.data.access_token,
refreshToken: response.data.refresh_token || null,
user: response.user || null,
token: response.access_token,
refreshToken: response.refresh_token || null,
isAuthenticated: true,
isLoading: false,
error: null,
@@ -138,10 +138,10 @@ export const useAuthStore = create<AuthState>()(
const response = await authService.refreshToken(refreshToken);
if (response.success && response.data) {
if (response && response.access_token) {
set({
token: response.data.access_token,
refreshToken: response.data.refresh_token || refreshToken,
token: response.access_token,
refreshToken: response.refresh_token || refreshToken,
isLoading: false,
error: null,
});
@@ -224,7 +224,7 @@ export const useAuthStore = create<AuthState>()(
onRehydrateStorage: () => (state) => {
// Initialize API client with stored token when store rehydrates
if (state?.token) {
import('../services/api/client').then(({ apiClient }) => {
import('../api').then(({ apiClient }) => {
apiClient.setAuthToken(state.token!);
if (state.user?.tenant_id) {

View File

@@ -9,5 +9,3 @@ export type { Theme, Language, ViewMode, SidebarState, Toast, Modal, UIState } f
export { useTenantStore, useCurrentTenant, useAvailableTenants, useTenantLoading, useTenantError, useTenantActions, useTenantPermissions, useTenant } from './tenant.store';
export type { TenantState } from './tenant.store';
export { useAlertsStore, useAlerts, useAlertRules, useAlertFilters, useAlertSettings, useUnreadAlertsCount, useCriticalAlertsCount } from './alerts.store';
export type { Alert, AlertRule, AlertCondition, AlertAction, AlertsState, AlertType, AlertCategory, AlertPriority, AlertStatus } from './alerts.store';

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { tenantService, TenantResponse } from '../services/api/tenant.service';
import { tenantService, type TenantResponse } from '../api';
import { useAuthUser } from './auth.store';
export interface TenantState {
@@ -45,22 +45,16 @@ export const useTenantStore = create<TenantState>()(
const { availableTenants } = get();
// Find tenant in available tenants first
// Find tenant in available tenants
const targetTenant = availableTenants?.find(t => t.id === tenantId);
if (!targetTenant) {
throw new Error('Tenant not found in available tenants');
}
// Switch tenant using service
const response = await tenantService.switchTenant(tenantId);
if (response.success && response.data?.tenant) {
get().setCurrentTenant(response.data.tenant);
set({ isLoading: false });
return true;
} else {
throw new Error(response.error || 'Failed to switch tenant');
}
// Switch tenant (frontend-only operation)
get().setCurrentTenant(targetTenant);
set({ isLoading: false });
return true;
} catch (error) {
set({
isLoading: false,
@@ -84,8 +78,8 @@ export const useTenantStore = create<TenantState>()(
const response = await tenantService.getUserTenants(user.id);
if (response.success && response.data) {
const tenants = Array.isArray(response.data) ? response.data : [response.data];
if (response) {
const tenants = Array.isArray(response) ? response : [response];
set({
availableTenants: tenants,
@@ -98,7 +92,7 @@ export const useTenantStore = create<TenantState>()(
get().setCurrentTenant(tenants[0]);
}
} else {
throw new Error(response.error || 'Failed to load user tenants');
throw new Error('Failed to load user tenants');
}
} catch (error) {
set({
@@ -181,7 +175,7 @@ export const useTenantStore = create<TenantState>()(
onRehydrateStorage: () => (state) => {
// Initialize API client with stored tenant when store rehydrates
if (state?.currentTenant) {
import('../services/api/client').then(({ apiClient }) => {
import('../api').then(({ apiClient }) => {
apiClient.setTenantId(state.currentTenant!.id);
});
}

View File

@@ -0,0 +1,27 @@
import { useEffect } from 'react';
import { useIsAuthenticated } from './auth.store';
import { useTenantActions, useAvailableTenants } from './tenant.store';
/**
* Hook to automatically initialize tenant data when user is authenticated
* This should be used at the app level to ensure tenant data is loaded
*/
export const useTenantInitializer = () => {
const isAuthenticated = useIsAuthenticated();
const availableTenants = useAvailableTenants();
const { loadUserTenants } = useTenantActions();
useEffect(() => {
if (isAuthenticated && !availableTenants) {
// Load user's available tenants when authenticated and not already loaded
loadUserTenants();
}
}, [isAuthenticated, availableTenants, loadUserTenants]);
// Also load tenants when user becomes authenticated (e.g., after login)
useEffect(() => {
if (isAuthenticated && availableTenants === null) {
loadUserTenants();
}
}, [isAuthenticated, availableTenants, loadUserTenants]);
};