Start integrating the onboarding flow with backend 6
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
27
frontend/src/stores/useTenantInitializer.ts
Normal file
27
frontend/src/stores/useTenantInitializer.ts
Normal 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]);
|
||||
};
|
||||
Reference in New Issue
Block a user