ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View 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();
}
});
}

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

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

View 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';

View 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);
});
}