Files
HoruxStrategyKimi/apps/web/src/stores/ui.store.ts
HORUX360 a9b1994c48 feat: Implement Phase 1 & 2 - Full monorepo architecture
## Backend API (apps/api)
- Express.js server with TypeScript
- JWT authentication with access/refresh tokens
- Multi-tenant middleware (schema per tenant)
- Complete CRUD routes: auth, cfdis, transactions, contacts, categories, metrics, alerts
- SAT integration: CFDI 4.0 XML parser, FIEL authentication
- Metrics engine: 50+ financial metrics (Core, Startup, Enterprise)
- Rate limiting, CORS, Helmet security

## Frontend Web (apps/web)
- Next.js 14 with App Router
- Authentication pages: login, register, forgot-password
- Dashboard layout with Sidebar and Header
- Dashboard pages: overview, cash-flow, revenue, expenses, metrics
- Zustand stores for auth and UI state
- Theme support with flash prevention

## Database Package (packages/database)
- PostgreSQL migrations with multi-tenant architecture
- Public schema: plans, tenants, users, sessions, subscriptions
- Tenant schema: sat_credentials, cfdis, transactions, contacts, accounts, alerts
- Tenant management functions
- Seed data for plans and super admin

## Shared Package (packages/shared)
- TypeScript types: auth, tenant, financial, metrics, reports
- Zod validation schemas for all entities
- Utility functions for formatting

## UI Package (packages/ui)
- Chart components: LineChart, BarChart, AreaChart, PieChart
- Data components: DataTable, MetricCard, KPICard, AlertBadge
- PeriodSelector and Skeleton components

## Infrastructure
- Docker Compose: PostgreSQL 15, Redis 7, MinIO, Mailhog
- Makefile with 25+ development commands
- Development scripts: dev-setup.sh, dev-down.sh
- Complete .env.example template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:05:24 +00:00

298 lines
6.9 KiB
TypeScript

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
/**
* Tipos de tema
*/
type Theme = 'light' | 'dark' | 'system';
/**
* Tipo de notificación
*/
interface Notification {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
title: string;
message?: string;
duration?: number;
action?: {
label: string;
onClick: () => void;
};
}
/**
* Modal state
*/
interface ModalState {
isOpen: boolean;
type: string | null;
data?: unknown;
}
/**
* Estado del store de UI
*/
interface UIState {
// Sidebar
sidebarOpen: boolean;
sidebarCollapsed: boolean;
// Theme
theme: Theme;
resolvedTheme: 'light' | 'dark';
// Notifications
notifications: Notification[];
// Modal
modal: ModalState;
// Loading states
globalLoading: boolean;
loadingMessage: string | null;
// Mobile
isMobile: boolean;
// Acciones Sidebar
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
toggleSidebarCollapsed: () => void;
setSidebarCollapsed: (collapsed: boolean) => void;
// Acciones Theme
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
// Acciones Notifications
addNotification: (notification: Omit<Notification, 'id'>) => void;
removeNotification: (id: string) => void;
clearNotifications: () => void;
// Acciones Modal
openModal: (type: string, data?: unknown) => void;
closeModal: () => void;
// Acciones Loading
setGlobalLoading: (loading: boolean, message?: string) => void;
// Acciones Mobile
setIsMobile: (isMobile: boolean) => void;
}
/**
* Genera un ID único para notificaciones
*/
function generateNotificationId(): string {
return `notification-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
/**
* Detecta el tema del sistema
*/
function getSystemTheme(): 'light' | 'dark' {
if (typeof window === 'undefined') return 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/**
* Store de UI con persistencia parcial
*/
export const useUIStore = create<UIState>()(
persist(
(set, get) => ({
// Estado inicial
sidebarOpen: true,
sidebarCollapsed: false,
theme: 'dark',
resolvedTheme: 'dark',
notifications: [],
modal: {
isOpen: false,
type: null,
data: undefined,
},
globalLoading: false,
loadingMessage: null,
isMobile: false,
// Sidebar
toggleSidebar: () => {
set((state) => ({ sidebarOpen: !state.sidebarOpen }));
},
setSidebarOpen: (open: boolean) => {
set({ sidebarOpen: open });
},
toggleSidebarCollapsed: () => {
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed }));
},
setSidebarCollapsed: (collapsed: boolean) => {
set({ sidebarCollapsed: collapsed });
},
// Theme
setTheme: (theme: Theme) => {
const resolvedTheme = theme === 'system' ? getSystemTheme() : theme;
// Aplicar clase al documento
if (typeof document !== 'undefined') {
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(resolvedTheme);
}
set({ theme, resolvedTheme });
},
toggleTheme: () => {
const { theme } = get();
const newTheme = theme === 'dark' ? 'light' : 'dark';
get().setTheme(newTheme);
},
// Notifications
addNotification: (notification) => {
const id = generateNotificationId();
const newNotification: Notification = {
...notification,
id,
duration: notification.duration ?? 5000,
};
set((state) => ({
notifications: [...state.notifications, newNotification],
}));
// Auto-remove after duration
if (newNotification.duration && newNotification.duration > 0) {
setTimeout(() => {
get().removeNotification(id);
}, newNotification.duration);
}
},
removeNotification: (id: string) => {
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
}));
},
clearNotifications: () => {
set({ notifications: [] });
},
// Modal
openModal: (type: string, data?: unknown) => {
set({
modal: {
isOpen: true,
type,
data,
},
});
},
closeModal: () => {
set({
modal: {
isOpen: false,
type: null,
data: undefined,
},
});
},
// Loading
setGlobalLoading: (loading: boolean, message?: string) => {
set({
globalLoading: loading,
loadingMessage: loading ? (message || null) : null,
});
},
// Mobile
setIsMobile: (isMobile: boolean) => {
set({ isMobile });
// Cerrar sidebar en mobile
if (isMobile) {
set({ sidebarOpen: false });
}
},
}),
{
name: 'horux-ui-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
theme: state.theme,
sidebarCollapsed: state.sidebarCollapsed,
}),
onRehydrateStorage: () => (state) => {
// Aplicar tema al rehidratar
if (state) {
const resolvedTheme = state.theme === 'system' ? getSystemTheme() : state.theme;
if (typeof document !== 'undefined') {
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(resolvedTheme);
}
state.resolvedTheme = resolvedTheme;
}
},
}
)
);
/**
* Hook helper para notificaciones
*/
export const useNotifications = () => {
const { addNotification, removeNotification, clearNotifications, notifications } = useUIStore();
return {
notifications,
notify: addNotification,
remove: removeNotification,
clear: clearNotifications,
success: (title: string, message?: string) =>
addNotification({ type: 'success', title, message }),
error: (title: string, message?: string) =>
addNotification({ type: 'error', title, message }),
warning: (title: string, message?: string) =>
addNotification({ type: 'warning', title, message }),
info: (title: string, message?: string) =>
addNotification({ type: 'info', title, message }),
};
};
/**
* Hook helper para modales
*/
export const useModal = () => {
const { modal, openModal, closeModal } = useUIStore();
return {
...modal,
open: openModal,
close: closeModal,
};
};
/**
* Hook helper para el tema
*/
export const useTheme = () => {
const { theme, resolvedTheme, setTheme, toggleTheme } = useUIStore();
return {
theme,
resolvedTheme,
setTheme,
toggleTheme,
isDark: resolvedTheme === 'dark',
isLight: resolvedTheme === 'light',
};
};