ADD new frontend
This commit is contained in:
620
frontend/src/stores/alerts.store.ts
Normal file
620
frontend/src/stores/alerts.store.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
263
frontend/src/stores/auth.store.ts
Normal file
263
frontend/src/stores/auth.store.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'admin' | 'manager' | 'baker' | 'staff';
|
||||
permissions: string[];
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
avatar?: string;
|
||||
lastLogin?: string;
|
||||
preferences?: {
|
||||
language: string;
|
||||
timezone: string;
|
||||
theme: 'light' | 'dark';
|
||||
notifications: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
// State
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
refreshAuth: () => Promise<void>;
|
||||
updateUser: (updates: Partial<User>) => void;
|
||||
clearError: () => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
|
||||
// Permission helpers
|
||||
hasPermission: (permission: string) => boolean;
|
||||
hasRole: (role: string) => boolean;
|
||||
canAccess: (resource: string, action: string) => boolean;
|
||||
}
|
||||
|
||||
// Mock API functions (replace with actual API calls)
|
||||
const mockLogin = async (email: string, password: string): Promise<{ user: User; token: string; refreshToken: string }> => {
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
if (email === 'admin@bakery.com' && password === 'admin') {
|
||||
return {
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'admin@bakery.com',
|
||||
name: 'Admin User',
|
||||
role: 'admin',
|
||||
permissions: ['*'],
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: 'Panadería San Miguel',
|
||||
avatar: undefined,
|
||||
lastLogin: new Date().toISOString(),
|
||||
preferences: {
|
||||
language: 'es',
|
||||
timezone: 'Europe/Madrid',
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
token: 'mock-jwt-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Credenciales inválidas');
|
||||
};
|
||||
|
||||
const mockRefreshToken = async (refreshToken: string): Promise<{ token: string; refreshToken: string }> => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
if (refreshToken === 'mock-refresh-token') {
|
||||
return {
|
||||
token: 'new-mock-jwt-token',
|
||||
refreshToken: 'new-mock-refresh-token',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Invalid refresh token');
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Actions
|
||||
login: async (email: string, password: string) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await mockLogin(email, password);
|
||||
|
||||
set({
|
||||
user: response.user,
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Error de autenticación',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
refreshAuth: async () => {
|
||||
try {
|
||||
const { refreshToken } = get();
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
set({ isLoading: true });
|
||||
|
||||
const response = await mockRefreshToken(refreshToken);
|
||||
|
||||
set({
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Error al renovar sesión',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateUser: (updates: Partial<User>) => {
|
||||
const { user } = get();
|
||||
if (user) {
|
||||
set({
|
||||
user: { ...user, ...updates },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
|
||||
setLoading: (loading: boolean) => {
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
|
||||
// Permission helpers
|
||||
hasPermission: (permission: string): boolean => {
|
||||
const { user } = get();
|
||||
if (!user) return false;
|
||||
|
||||
// Admin has all permissions
|
||||
if (user.permissions.includes('*')) return true;
|
||||
|
||||
return user.permissions.includes(permission);
|
||||
},
|
||||
|
||||
hasRole: (role: string): boolean => {
|
||||
const { user } = get();
|
||||
return user?.role === role;
|
||||
},
|
||||
|
||||
canAccess: (resource: string, action: string): boolean => {
|
||||
const { user, hasPermission } = get();
|
||||
if (!user) return false;
|
||||
|
||||
// Check specific permission
|
||||
if (hasPermission(`${resource}:${action}`)) return true;
|
||||
|
||||
// Check wildcard permissions
|
||||
if (hasPermission(`${resource}:*`)) return true;
|
||||
if (hasPermission('*')) return true;
|
||||
|
||||
// Role-based access fallback
|
||||
switch (user.role) {
|
||||
case 'admin':
|
||||
return true;
|
||||
case 'manager':
|
||||
return ['inventory', 'production', 'sales', 'reports'].includes(resource);
|
||||
case 'baker':
|
||||
return ['production', 'inventory'].includes(resource) &&
|
||||
['read', 'update'].includes(action);
|
||||
case 'staff':
|
||||
return ['inventory', 'sales'].includes(resource) &&
|
||||
action === 'read';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
user: state.user,
|
||||
token: state.token,
|
||||
refreshToken: state.refreshToken,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Selectors for common use cases
|
||||
export const useAuthUser = () => useAuthStore((state) => state.user);
|
||||
export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated);
|
||||
export const useAuthLoading = () => useAuthStore((state) => state.isLoading);
|
||||
export const useAuthError = () => useAuthStore((state) => state.error);
|
||||
export const usePermissions = () => useAuthStore((state) => ({
|
||||
hasPermission: state.hasPermission,
|
||||
hasRole: state.hasRole,
|
||||
canAccess: state.canAccess,
|
||||
}));
|
||||
|
||||
// Hook for auth actions
|
||||
export const useAuthActions = () => useAuthStore((state) => ({
|
||||
login: state.login,
|
||||
logout: state.logout,
|
||||
refreshAuth: state.refreshAuth,
|
||||
updateUser: state.updateUser,
|
||||
clearError: state.clearError,
|
||||
setLoading: state.setLoading,
|
||||
}));
|
||||
99
frontend/src/stores/bakery.store.ts
Normal file
99
frontend/src/stores/bakery.store.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
export type BakeryType = 'individual' | 'central' | 'hybrid';
|
||||
export type BusinessModel = 'production' | 'retail' | 'hybrid';
|
||||
|
||||
interface BakeryState {
|
||||
// State
|
||||
currentTenant: Tenant | null;
|
||||
bakeryType: BakeryType;
|
||||
businessModel: BusinessModel;
|
||||
operatingHours: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
features: {
|
||||
inventory: boolean;
|
||||
production: boolean;
|
||||
forecasting: boolean;
|
||||
analytics: boolean;
|
||||
pos: boolean;
|
||||
};
|
||||
|
||||
// Actions
|
||||
setTenant: (tenant: Tenant) => void;
|
||||
setBakeryType: (type: BakeryType) => void;
|
||||
setBusinessModel: (model: BusinessModel) => void;
|
||||
updateFeatures: (features: Partial<BakeryState['features']>) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
subscription_plan: string;
|
||||
created_at: string;
|
||||
members_count: number;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
currentTenant: null,
|
||||
bakeryType: 'individual' as BakeryType,
|
||||
businessModel: 'production' as BusinessModel,
|
||||
operatingHours: {
|
||||
start: '04:00',
|
||||
end: '20:00',
|
||||
},
|
||||
features: {
|
||||
inventory: true,
|
||||
production: true,
|
||||
forecasting: true,
|
||||
analytics: true,
|
||||
pos: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const useBakeryStore = create<BakeryState>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set) => ({
|
||||
...initialState,
|
||||
|
||||
setTenant: (tenant) =>
|
||||
set((state) => {
|
||||
state.currentTenant = tenant;
|
||||
}),
|
||||
|
||||
setBakeryType: (type) =>
|
||||
set((state) => {
|
||||
state.bakeryType = type;
|
||||
// Adjust features based on bakery type
|
||||
if (type === 'individual') {
|
||||
state.features.pos = false;
|
||||
state.businessModel = 'production';
|
||||
} else if (type === 'central') {
|
||||
state.features.pos = true;
|
||||
state.businessModel = 'retail';
|
||||
}
|
||||
}),
|
||||
|
||||
setBusinessModel: (model) =>
|
||||
set((state) => {
|
||||
state.businessModel = model;
|
||||
}),
|
||||
|
||||
updateFeatures: (features) =>
|
||||
set((state) => {
|
||||
state.features = { ...state.features, ...features };
|
||||
}),
|
||||
|
||||
reset: () => set(() => initialState),
|
||||
})),
|
||||
{
|
||||
name: 'bakery-storage',
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
12
frontend/src/stores/index.ts
Normal file
12
frontend/src/stores/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Store exports
|
||||
export { useAuthStore, useAuthUser, useIsAuthenticated, useAuthLoading, useAuthError, usePermissions, useAuthActions } from './auth.store';
|
||||
export type { User, AuthState } from './auth.store';
|
||||
|
||||
export { useUIStore, useLanguage, useSidebar, useCompactMode, useViewMode, useLoading, useToasts, useModals, useBreadcrumbs, usePreferences, useUIActions } from './ui.store';
|
||||
export type { Theme, Language, ViewMode, SidebarState, Toast, Modal, UIState } from './ui.store';
|
||||
|
||||
export { useBakeryStore } from './bakery.store';
|
||||
export type { BakeryType, BusinessModel } from './bakery.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';
|
||||
394
frontend/src/stores/ui.store.ts
Normal file
394
frontend/src/stores/ui.store.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'auto';
|
||||
export type Language = 'es' | 'en' | 'fr' | 'pt' | 'it';
|
||||
export type ViewMode = 'list' | 'grid' | 'card';
|
||||
export type SidebarState = 'expanded' | 'collapsed' | 'hidden';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: 'success' | 'error' | 'warning' | 'info';
|
||||
title: string;
|
||||
message?: string;
|
||||
duration?: number;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
persistent?: boolean;
|
||||
}
|
||||
|
||||
export interface Modal {
|
||||
id: string;
|
||||
type: 'dialog' | 'drawer' | 'fullscreen';
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
closeable?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface UIState {
|
||||
// Theme & Appearance
|
||||
theme: Theme;
|
||||
language: Language;
|
||||
sidebarState: SidebarState;
|
||||
compactMode: boolean;
|
||||
reducedMotion: boolean;
|
||||
|
||||
// Layout & Navigation
|
||||
currentPage: string;
|
||||
breadcrumbs: Array<{ label: string; path: string }>;
|
||||
viewMode: ViewMode;
|
||||
|
||||
// Loading States
|
||||
globalLoading: boolean;
|
||||
loadingStates: Record<string, boolean>;
|
||||
|
||||
// Toasts & Notifications
|
||||
toasts: Toast[];
|
||||
|
||||
// Modals & Dialogs
|
||||
modals: Modal[];
|
||||
|
||||
// User Preferences
|
||||
preferences: {
|
||||
showTips: boolean;
|
||||
autoSave: boolean;
|
||||
confirmActions: boolean;
|
||||
defaultPageSize: number;
|
||||
dateFormat: string;
|
||||
numberFormat: string;
|
||||
timezone: string;
|
||||
};
|
||||
|
||||
// Actions
|
||||
setTheme: (theme: Theme) => void;
|
||||
setLanguage: (language: Language) => void;
|
||||
setSidebarState: (state: SidebarState) => void;
|
||||
setCompactMode: (compact: boolean) => void;
|
||||
setReducedMotion: (reduced: boolean) => void;
|
||||
|
||||
setCurrentPage: (page: string) => void;
|
||||
setBreadcrumbs: (breadcrumbs: Array<{ label: string; path: string }>) => void;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
|
||||
setGlobalLoading: (loading: boolean) => void;
|
||||
setLoading: (key: string, loading: boolean) => void;
|
||||
isLoading: (key: string) => boolean;
|
||||
|
||||
showToast: (toast: Omit<Toast, 'id'>) => string;
|
||||
hideToast: (id: string) => void;
|
||||
clearToasts: () => void;
|
||||
|
||||
showModal: (modal: Omit<Modal, 'id'>) => string;
|
||||
hideModal: (id: string) => void;
|
||||
clearModals: () => void;
|
||||
|
||||
updatePreference: <K extends keyof UIState['preferences']>(
|
||||
key: K,
|
||||
value: UIState['preferences'][K]
|
||||
) => void;
|
||||
resetPreferences: () => void;
|
||||
}
|
||||
|
||||
const defaultPreferences = {
|
||||
showTips: true,
|
||||
autoSave: true,
|
||||
confirmActions: true,
|
||||
defaultPageSize: 25,
|
||||
dateFormat: 'DD/MM/YYYY',
|
||||
numberFormat: 'european', // european, american
|
||||
timezone: 'Europe/Madrid',
|
||||
};
|
||||
|
||||
export const useUIStore = create<UIState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
theme: 'light',
|
||||
language: 'es',
|
||||
sidebarState: 'expanded',
|
||||
compactMode: false,
|
||||
reducedMotion: false,
|
||||
|
||||
currentPage: '',
|
||||
breadcrumbs: [],
|
||||
viewMode: 'list',
|
||||
|
||||
globalLoading: false,
|
||||
loadingStates: {},
|
||||
|
||||
toasts: [],
|
||||
modals: [],
|
||||
|
||||
preferences: defaultPreferences,
|
||||
|
||||
// Theme & Appearance actions
|
||||
setTheme: (theme: Theme) => {
|
||||
set({ theme });
|
||||
|
||||
// Apply theme to document
|
||||
const root = document.documentElement;
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
root.classList.remove('light');
|
||||
} else if (theme === 'light') {
|
||||
root.classList.add('light');
|
||||
root.classList.remove('dark');
|
||||
} else {
|
||||
// Auto theme - check system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (prefersDark) {
|
||||
root.classList.add('dark');
|
||||
root.classList.remove('light');
|
||||
} else {
|
||||
root.classList.add('light');
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setLanguage: (language: Language) => {
|
||||
set({ language });
|
||||
// You might want to trigger i18n language change here
|
||||
},
|
||||
|
||||
setSidebarState: (sidebarState: SidebarState) => {
|
||||
set({ sidebarState });
|
||||
},
|
||||
|
||||
setCompactMode: (compactMode: boolean) => {
|
||||
set({ compactMode });
|
||||
},
|
||||
|
||||
setReducedMotion: (reducedMotion: boolean) => {
|
||||
set({ reducedMotion });
|
||||
|
||||
// Apply reduced motion preference
|
||||
const root = document.documentElement;
|
||||
if (reducedMotion) {
|
||||
root.classList.add('reduce-motion');
|
||||
} else {
|
||||
root.classList.remove('reduce-motion');
|
||||
}
|
||||
},
|
||||
|
||||
// Navigation actions
|
||||
setCurrentPage: (currentPage: string) => {
|
||||
set({ currentPage });
|
||||
},
|
||||
|
||||
setBreadcrumbs: (breadcrumbs: Array<{ label: string; path: string }>) => {
|
||||
set({ breadcrumbs });
|
||||
},
|
||||
|
||||
setViewMode: (viewMode: ViewMode) => {
|
||||
set({ viewMode });
|
||||
},
|
||||
|
||||
// Loading actions
|
||||
setGlobalLoading: (globalLoading: boolean) => {
|
||||
set({ globalLoading });
|
||||
},
|
||||
|
||||
setLoading: (key: string, loading: boolean) => {
|
||||
set((state) => ({
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
[key]: loading,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
isLoading: (key: string): boolean => {
|
||||
return get().loadingStates[key] ?? false;
|
||||
},
|
||||
|
||||
// Toast actions
|
||||
showToast: (toast: Omit<Toast, 'id'>): string => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newToast: Toast = {
|
||||
...toast,
|
||||
id,
|
||||
duration: toast.duration ?? (toast.type === 'error' ? 0 : 5000), // Error toasts don't auto-dismiss
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
toasts: [...state.toasts, newToast],
|
||||
}));
|
||||
|
||||
// Auto-dismiss toast if duration is set
|
||||
if (newToast.duration && newToast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
get().hideToast(id);
|
||||
}, newToast.duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hideToast: (id: string) => {
|
||||
set((state) => ({
|
||||
toasts: state.toasts.filter(toast => toast.id !== id),
|
||||
}));
|
||||
},
|
||||
|
||||
clearToasts: () => {
|
||||
set({ toasts: [] });
|
||||
},
|
||||
|
||||
// Modal actions
|
||||
showModal: (modal: Omit<Modal, 'id'>): string => {
|
||||
const id = `modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newModal: Modal = {
|
||||
...modal,
|
||||
id,
|
||||
size: modal.size ?? 'md',
|
||||
closeable: modal.closeable ?? true,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
modals: [...state.modals, newModal],
|
||||
}));
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
hideModal: (id: string) => {
|
||||
const { modals } = get();
|
||||
const modal = modals.find(m => m.id === id);
|
||||
|
||||
// Call onClose callback if provided
|
||||
if (modal?.onClose) {
|
||||
modal.onClose();
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
modals: state.modals.filter(modal => modal.id !== id),
|
||||
}));
|
||||
},
|
||||
|
||||
clearModals: () => {
|
||||
// Call onClose for all modals
|
||||
const { modals } = get();
|
||||
modals.forEach(modal => {
|
||||
if (modal.onClose) {
|
||||
modal.onClose();
|
||||
}
|
||||
});
|
||||
|
||||
set({ modals: [] });
|
||||
},
|
||||
|
||||
// Preferences actions
|
||||
updatePreference: <K extends keyof UIState['preferences']>(
|
||||
key: K,
|
||||
value: UIState['preferences'][K]
|
||||
) => {
|
||||
set((state) => ({
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
resetPreferences: () => {
|
||||
set({ preferences: defaultPreferences });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'ui-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
language: state.language,
|
||||
sidebarState: state.sidebarState,
|
||||
compactMode: state.compactMode,
|
||||
reducedMotion: state.reducedMotion,
|
||||
viewMode: state.viewMode,
|
||||
preferences: state.preferences,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Selectors for common use cases
|
||||
export const useLanguage = () => useUIStore((state) => state.language);
|
||||
export const useSidebar = () => useUIStore((state) => ({
|
||||
state: state.sidebarState,
|
||||
setState: state.setSidebarState,
|
||||
}));
|
||||
export const useCompactMode = () => useUIStore((state) => state.compactMode);
|
||||
export const useViewMode = () => useUIStore((state) => state.viewMode);
|
||||
|
||||
export const useLoading = (key?: string) => {
|
||||
if (key) {
|
||||
return useUIStore((state) => state.isLoading(key));
|
||||
}
|
||||
return useUIStore((state) => state.globalLoading);
|
||||
};
|
||||
|
||||
export const useToasts = () => useUIStore((state) => state.toasts);
|
||||
export const useModals = () => useUIStore((state) => state.modals);
|
||||
|
||||
export const useBreadcrumbs = () => useUIStore((state) => ({
|
||||
breadcrumbs: state.breadcrumbs,
|
||||
setBreadcrumbs: state.setBreadcrumbs,
|
||||
}));
|
||||
|
||||
export const usePreferences = () => useUIStore((state) => state.preferences);
|
||||
|
||||
// Hook for UI actions
|
||||
export const useUIActions = () => useUIStore((state) => ({
|
||||
setTheme: state.setTheme,
|
||||
setLanguage: state.setLanguage,
|
||||
setSidebarState: state.setSidebarState,
|
||||
setCompactMode: state.setCompactMode,
|
||||
setReducedMotion: state.setReducedMotion,
|
||||
setCurrentPage: state.setCurrentPage,
|
||||
setBreadcrumbs: state.setBreadcrumbs,
|
||||
setViewMode: state.setViewMode,
|
||||
setGlobalLoading: state.setGlobalLoading,
|
||||
setLoading: state.setLoading,
|
||||
showToast: state.showToast,
|
||||
hideToast: state.hideToast,
|
||||
clearToasts: state.clearToasts,
|
||||
showModal: state.showModal,
|
||||
hideModal: state.hideModal,
|
||||
clearModals: state.clearModals,
|
||||
updatePreference: state.updatePreference,
|
||||
resetPreferences: state.resetPreferences,
|
||||
}));
|
||||
|
||||
// Initialize theme on store creation
|
||||
if (typeof window !== 'undefined') {
|
||||
// Set initial theme based on stored preference or system preference
|
||||
const storedState = localStorage.getItem('ui-storage');
|
||||
if (storedState) {
|
||||
try {
|
||||
const { state } = JSON.parse(storedState);
|
||||
useUIStore.getState().setTheme(state.theme || 'auto');
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse stored UI state:', error);
|
||||
useUIStore.getState().setTheme('auto');
|
||||
}
|
||||
} else {
|
||||
useUIStore.getState().setTheme('auto');
|
||||
}
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
const { theme } = useUIStore.getState();
|
||||
if (theme === 'auto') {
|
||||
useUIStore.getState().setTheme('auto');
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for reduced motion preference
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
|
||||
useUIStore.getState().setReducedMotion(e.matches);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user