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>
This commit is contained in:
297
apps/web/src/stores/ui.store.ts
Normal file
297
apps/web/src/stores/ui.store.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
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',
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user