## 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>
298 lines
6.9 KiB
TypeScript
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',
|
|
};
|
|
};
|