738 lines
18 KiB
Markdown
738 lines
18 KiB
Markdown
# Zustand Stores Specification
|
|
|
|
**Version:** 1.0.0
|
|
**Fecha:** 2025-12-05
|
|
|
|
---
|
|
|
|
## Global Stores
|
|
|
|
### AuthStore
|
|
|
|
```typescript
|
|
interface AuthState {
|
|
// State
|
|
user: User | null;
|
|
token: string | null;
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
|
|
// Actions
|
|
login: (credentials: LoginDto) => Promise<void>;
|
|
logout: () => void;
|
|
refreshToken: () => Promise<void>;
|
|
updateUser: (data: Partial<User>) => void;
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>()(
|
|
devtools(
|
|
persist(
|
|
(set, get) => ({
|
|
user: null,
|
|
token: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
|
|
login: async (credentials) => {
|
|
set({ isLoading: true });
|
|
try {
|
|
const response = await authApi.login(credentials);
|
|
set({
|
|
user: response.user,
|
|
token: response.token,
|
|
isAuthenticated: true,
|
|
isLoading: false
|
|
});
|
|
} catch (error) {
|
|
set({ isLoading: false });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
logout: () => {
|
|
set({
|
|
user: null,
|
|
token: null,
|
|
isAuthenticated: false
|
|
});
|
|
},
|
|
|
|
refreshToken: async () => {
|
|
const response = await authApi.refresh();
|
|
set({ token: response.token });
|
|
},
|
|
|
|
updateUser: (data) => {
|
|
set((state) => ({
|
|
user: state.user ? { ...state.user, ...data } : null
|
|
}));
|
|
}
|
|
}),
|
|
{
|
|
name: 'auth-store',
|
|
partialize: (state) => ({
|
|
token: state.token,
|
|
user: state.user
|
|
})
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### TenantStore
|
|
|
|
```typescript
|
|
interface TenantState {
|
|
// State
|
|
currentTenant: Tenant | null;
|
|
availableTenants: Tenant[];
|
|
|
|
// Actions
|
|
setCurrentTenant: (tenant: Tenant) => void;
|
|
setAvailableTenants: (tenants: Tenant[]) => void;
|
|
switchTenant: (tenantId: string) => Promise<void>;
|
|
}
|
|
|
|
export const useTenantStore = create<TenantState>()(
|
|
devtools(
|
|
persist(
|
|
(set, get) => ({
|
|
currentTenant: null,
|
|
availableTenants: [],
|
|
|
|
setCurrentTenant: (tenant) => set({ currentTenant: tenant }),
|
|
setAvailableTenants: (tenants) => set({ availableTenants: tenants }),
|
|
|
|
switchTenant: async (tenantId) => {
|
|
const tenant = get().availableTenants.find(t => t.id === tenantId);
|
|
if (tenant) {
|
|
set({ currentTenant: tenant });
|
|
// Reload data for new tenant
|
|
window.location.reload();
|
|
}
|
|
}
|
|
}),
|
|
{
|
|
name: 'tenant-store'
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### UIStore
|
|
|
|
```typescript
|
|
interface UIState {
|
|
// Sidebar
|
|
sidebarCollapsed: boolean;
|
|
sidebarPinned: boolean;
|
|
|
|
// Theme
|
|
theme: 'light' | 'dark' | 'system';
|
|
primaryColor: string;
|
|
|
|
// Modal state
|
|
activeModal: string | null;
|
|
modalData: any;
|
|
|
|
// Actions
|
|
toggleSidebar: () => void;
|
|
setSidebarCollapsed: (collapsed: boolean) => void;
|
|
setSidebarPinned: (pinned: boolean) => void;
|
|
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
|
openModal: (name: string, data?: any) => void;
|
|
closeModal: () => void;
|
|
}
|
|
|
|
export const useUIStore = create<UIState>()(
|
|
devtools(
|
|
persist(
|
|
(set) => ({
|
|
sidebarCollapsed: false,
|
|
sidebarPinned: true,
|
|
theme: 'system',
|
|
primaryColor: '#3b82f6',
|
|
activeModal: null,
|
|
modalData: null,
|
|
|
|
toggleSidebar: () =>
|
|
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
|
|
|
setSidebarCollapsed: (collapsed) =>
|
|
set({ sidebarCollapsed: collapsed }),
|
|
|
|
setSidebarPinned: (pinned) =>
|
|
set({ sidebarPinned: pinned }),
|
|
|
|
setTheme: (theme) =>
|
|
set({ theme }),
|
|
|
|
openModal: (name, data) =>
|
|
set({ activeModal: name, modalData: data }),
|
|
|
|
closeModal: () =>
|
|
set({ activeModal: null, modalData: null })
|
|
}),
|
|
{
|
|
name: 'ui-store',
|
|
partialize: (state) => ({
|
|
sidebarCollapsed: state.sidebarCollapsed,
|
|
sidebarPinned: state.sidebarPinned,
|
|
theme: state.theme,
|
|
primaryColor: state.primaryColor
|
|
})
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### NotificationStore
|
|
|
|
```typescript
|
|
interface Notification {
|
|
id: string;
|
|
type: 'info' | 'success' | 'warning' | 'error';
|
|
title: string;
|
|
message?: string;
|
|
duration?: number;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
}
|
|
|
|
interface NotificationState {
|
|
notifications: Notification[];
|
|
unreadCount: number;
|
|
|
|
addNotification: (notification: Omit<Notification, 'id'>) => void;
|
|
removeNotification: (id: string) => void;
|
|
clearAll: () => void;
|
|
markAllAsRead: () => void;
|
|
}
|
|
|
|
export const useNotificationStore = create<NotificationState>()(
|
|
devtools((set) => ({
|
|
notifications: [],
|
|
unreadCount: 0,
|
|
|
|
addNotification: (notification) =>
|
|
set((state) => ({
|
|
notifications: [
|
|
...state.notifications,
|
|
{ ...notification, id: crypto.randomUUID() }
|
|
],
|
|
unreadCount: state.unreadCount + 1
|
|
})),
|
|
|
|
removeNotification: (id) =>
|
|
set((state) => ({
|
|
notifications: state.notifications.filter((n) => n.id !== id)
|
|
})),
|
|
|
|
clearAll: () =>
|
|
set({ notifications: [], unreadCount: 0 }),
|
|
|
|
markAllAsRead: () =>
|
|
set({ unreadCount: 0 })
|
|
}))
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Feature Stores
|
|
|
|
### ConstructionStore
|
|
|
|
```typescript
|
|
interface ConstructionState {
|
|
// Filters
|
|
projectFilters: ProjectFilters;
|
|
developmentFilters: DevelopmentFilters;
|
|
|
|
// View preferences
|
|
projectViewMode: 'list' | 'grid' | 'kanban';
|
|
budgetViewMode: 'table' | 'chart';
|
|
ganttViewMode: 'day' | 'week' | 'month';
|
|
|
|
// Selected items
|
|
selectedProjectId: string | null;
|
|
selectedDevelopmentId: string | null;
|
|
|
|
// Actions
|
|
setProjectFilters: (filters: Partial<ProjectFilters>) => void;
|
|
resetProjectFilters: () => void;
|
|
setDevelopmentFilters: (filters: Partial<DevelopmentFilters>) => void;
|
|
setProjectViewMode: (mode: 'list' | 'grid' | 'kanban') => void;
|
|
setBudgetViewMode: (mode: 'table' | 'chart') => void;
|
|
setGanttViewMode: (mode: 'day' | 'week' | 'month') => void;
|
|
setSelectedProject: (id: string | null) => void;
|
|
setSelectedDevelopment: (id: string | null) => void;
|
|
}
|
|
|
|
const initialProjectFilters: ProjectFilters = {
|
|
status: undefined,
|
|
search: '',
|
|
clientId: undefined,
|
|
managerId: undefined,
|
|
dateRange: undefined,
|
|
page: 1,
|
|
limit: 20
|
|
};
|
|
|
|
export const useConstructionStore = create<ConstructionState>()(
|
|
devtools(
|
|
persist(
|
|
(set) => ({
|
|
projectFilters: initialProjectFilters,
|
|
developmentFilters: {},
|
|
projectViewMode: 'list',
|
|
budgetViewMode: 'table',
|
|
ganttViewMode: 'week',
|
|
selectedProjectId: null,
|
|
selectedDevelopmentId: null,
|
|
|
|
setProjectFilters: (filters) =>
|
|
set((state) => ({
|
|
projectFilters: { ...state.projectFilters, ...filters }
|
|
})),
|
|
|
|
resetProjectFilters: () =>
|
|
set({ projectFilters: initialProjectFilters }),
|
|
|
|
setDevelopmentFilters: (filters) =>
|
|
set((state) => ({
|
|
developmentFilters: { ...state.developmentFilters, ...filters }
|
|
})),
|
|
|
|
setProjectViewMode: (mode) =>
|
|
set({ projectViewMode: mode }),
|
|
|
|
setBudgetViewMode: (mode) =>
|
|
set({ budgetViewMode: mode }),
|
|
|
|
setGanttViewMode: (mode) =>
|
|
set({ ganttViewMode: mode }),
|
|
|
|
setSelectedProject: (id) =>
|
|
set({ selectedProjectId: id }),
|
|
|
|
setSelectedDevelopment: (id) =>
|
|
set({ selectedDevelopmentId: id })
|
|
}),
|
|
{
|
|
name: 'construction-store',
|
|
partialize: (state) => ({
|
|
projectViewMode: state.projectViewMode,
|
|
budgetViewMode: state.budgetViewMode,
|
|
ganttViewMode: state.ganttViewMode
|
|
})
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### ComplianceStore
|
|
|
|
```typescript
|
|
interface ComplianceState {
|
|
// Filters
|
|
complianceFilters: ComplianceFilters;
|
|
auditFilters: AuditFilters;
|
|
|
|
// View
|
|
dashboardView: 'summary' | 'detailed';
|
|
|
|
// Selected
|
|
selectedProgramId: string | null;
|
|
selectedAuditId: string | null;
|
|
|
|
// Actions
|
|
setComplianceFilters: (filters: Partial<ComplianceFilters>) => void;
|
|
setAuditFilters: (filters: Partial<AuditFilters>) => void;
|
|
setDashboardView: (view: 'summary' | 'detailed') => void;
|
|
setSelectedProgram: (id: string | null) => void;
|
|
setSelectedAudit: (id: string | null) => void;
|
|
}
|
|
|
|
export const useComplianceStore = create<ComplianceState>()(
|
|
devtools(
|
|
persist(
|
|
(set) => ({
|
|
complianceFilters: {},
|
|
auditFilters: {},
|
|
dashboardView: 'summary',
|
|
selectedProgramId: null,
|
|
selectedAuditId: null,
|
|
|
|
setComplianceFilters: (filters) =>
|
|
set((state) => ({
|
|
complianceFilters: { ...state.complianceFilters, ...filters }
|
|
})),
|
|
|
|
setAuditFilters: (filters) =>
|
|
set((state) => ({
|
|
auditFilters: { ...state.auditFilters, ...filters }
|
|
})),
|
|
|
|
setDashboardView: (view) =>
|
|
set({ dashboardView: view }),
|
|
|
|
setSelectedProgram: (id) =>
|
|
set({ selectedProgramId: id }),
|
|
|
|
setSelectedAudit: (id) =>
|
|
set({ selectedAuditId: id })
|
|
}),
|
|
{
|
|
name: 'compliance-store'
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### FinanceStore
|
|
|
|
```typescript
|
|
interface FinanceState {
|
|
// Filters
|
|
transactionFilters: TransactionFilters;
|
|
reportFilters: ReportFilters;
|
|
|
|
// View
|
|
dashboardPeriod: 'month' | 'quarter' | 'year';
|
|
chartType: 'bar' | 'line' | 'area';
|
|
|
|
// Selected
|
|
selectedAccountId: string | null;
|
|
|
|
// Actions
|
|
setTransactionFilters: (filters: Partial<TransactionFilters>) => void;
|
|
setReportFilters: (filters: Partial<ReportFilters>) => void;
|
|
setDashboardPeriod: (period: 'month' | 'quarter' | 'year') => void;
|
|
setChartType: (type: 'bar' | 'line' | 'area') => void;
|
|
setSelectedAccount: (id: string | null) => void;
|
|
}
|
|
|
|
export const useFinanceStore = create<FinanceState>()(
|
|
devtools(
|
|
persist(
|
|
(set) => ({
|
|
transactionFilters: {},
|
|
reportFilters: {},
|
|
dashboardPeriod: 'month',
|
|
chartType: 'bar',
|
|
selectedAccountId: null,
|
|
|
|
setTransactionFilters: (filters) =>
|
|
set((state) => ({
|
|
transactionFilters: { ...state.transactionFilters, ...filters }
|
|
})),
|
|
|
|
setReportFilters: (filters) =>
|
|
set((state) => ({
|
|
reportFilters: { ...state.reportFilters, ...filters }
|
|
})),
|
|
|
|
setDashboardPeriod: (period) =>
|
|
set({ dashboardPeriod: period }),
|
|
|
|
setChartType: (type) =>
|
|
set({ chartType: type }),
|
|
|
|
setSelectedAccount: (id) =>
|
|
set({ selectedAccountId: id })
|
|
}),
|
|
{
|
|
name: 'finance-store'
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### AssetsStore
|
|
|
|
```typescript
|
|
interface AssetsState {
|
|
// Filters
|
|
assetFilters: AssetFilters;
|
|
maintenanceFilters: MaintenanceFilters;
|
|
workOrderFilters: WorkOrderFilters;
|
|
|
|
// View
|
|
assetViewMode: 'list' | 'grid';
|
|
mapCenter: LatLng | null;
|
|
mapZoom: number;
|
|
|
|
// Selected
|
|
selectedAssetId: string | null;
|
|
|
|
// Actions
|
|
setAssetFilters: (filters: Partial<AssetFilters>) => void;
|
|
setMaintenanceFilters: (filters: Partial<MaintenanceFilters>) => void;
|
|
setWorkOrderFilters: (filters: Partial<WorkOrderFilters>) => void;
|
|
setAssetViewMode: (mode: 'list' | 'grid') => void;
|
|
setMapCenter: (center: LatLng) => void;
|
|
setMapZoom: (zoom: number) => void;
|
|
setSelectedAsset: (id: string | null) => void;
|
|
}
|
|
|
|
export const useAssetsStore = create<AssetsState>()(
|
|
devtools(
|
|
persist(
|
|
(set) => ({
|
|
assetFilters: {},
|
|
maintenanceFilters: {},
|
|
workOrderFilters: {},
|
|
assetViewMode: 'list',
|
|
mapCenter: null,
|
|
mapZoom: 12,
|
|
selectedAssetId: null,
|
|
|
|
setAssetFilters: (filters) =>
|
|
set((state) => ({
|
|
assetFilters: { ...state.assetFilters, ...filters }
|
|
})),
|
|
|
|
setMaintenanceFilters: (filters) =>
|
|
set((state) => ({
|
|
maintenanceFilters: { ...state.maintenanceFilters, ...filters }
|
|
})),
|
|
|
|
setWorkOrderFilters: (filters) =>
|
|
set((state) => ({
|
|
workOrderFilters: { ...state.workOrderFilters, ...filters }
|
|
})),
|
|
|
|
setAssetViewMode: (mode) =>
|
|
set({ assetViewMode: mode }),
|
|
|
|
setMapCenter: (center) =>
|
|
set({ mapCenter: center }),
|
|
|
|
setMapZoom: (zoom) =>
|
|
set({ mapZoom: zoom }),
|
|
|
|
setSelectedAsset: (id) =>
|
|
set({ selectedAssetId: id })
|
|
}),
|
|
{
|
|
name: 'assets-store'
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
### DocumentsStore
|
|
|
|
```typescript
|
|
interface DocumentsState {
|
|
// Navigation
|
|
currentFolderId: string | null;
|
|
folderPath: Folder[];
|
|
|
|
// Filters
|
|
documentFilters: DocumentFilters;
|
|
|
|
// View
|
|
viewMode: 'list' | 'grid';
|
|
sortBy: 'name' | 'date' | 'size';
|
|
sortOrder: 'asc' | 'desc';
|
|
|
|
// Selection
|
|
selectedDocuments: string[];
|
|
|
|
// Upload
|
|
uploadQueue: UploadItem[];
|
|
|
|
// Actions
|
|
setCurrentFolder: (id: string | null) => void;
|
|
setFolderPath: (path: Folder[]) => void;
|
|
navigateToFolder: (folder: Folder) => void;
|
|
navigateUp: () => void;
|
|
setDocumentFilters: (filters: Partial<DocumentFilters>) => void;
|
|
setViewMode: (mode: 'list' | 'grid') => void;
|
|
setSortBy: (sortBy: 'name' | 'date' | 'size') => void;
|
|
setSortOrder: (order: 'asc' | 'desc') => void;
|
|
selectDocument: (id: string) => void;
|
|
deselectDocument: (id: string) => void;
|
|
selectAllDocuments: (ids: string[]) => void;
|
|
clearSelection: () => void;
|
|
addToUploadQueue: (item: UploadItem) => void;
|
|
removeFromUploadQueue: (id: string) => void;
|
|
clearUploadQueue: () => void;
|
|
}
|
|
|
|
export const useDocumentsStore = create<DocumentsState>()(
|
|
devtools(
|
|
persist(
|
|
(set, get) => ({
|
|
currentFolderId: null,
|
|
folderPath: [],
|
|
documentFilters: {},
|
|
viewMode: 'list',
|
|
sortBy: 'name',
|
|
sortOrder: 'asc',
|
|
selectedDocuments: [],
|
|
uploadQueue: [],
|
|
|
|
setCurrentFolder: (id) =>
|
|
set({ currentFolderId: id }),
|
|
|
|
setFolderPath: (path) =>
|
|
set({ folderPath: path }),
|
|
|
|
navigateToFolder: (folder) => {
|
|
const currentPath = get().folderPath;
|
|
const index = currentPath.findIndex(f => f.id === folder.id);
|
|
|
|
if (index >= 0) {
|
|
// Navigate to existing folder in path
|
|
set({
|
|
currentFolderId: folder.id,
|
|
folderPath: currentPath.slice(0, index + 1)
|
|
});
|
|
} else {
|
|
// Navigate to new folder
|
|
set({
|
|
currentFolderId: folder.id,
|
|
folderPath: [...currentPath, folder]
|
|
});
|
|
}
|
|
},
|
|
|
|
navigateUp: () => {
|
|
const path = get().folderPath;
|
|
if (path.length > 1) {
|
|
set({
|
|
currentFolderId: path[path.length - 2].id,
|
|
folderPath: path.slice(0, -1)
|
|
});
|
|
} else {
|
|
set({
|
|
currentFolderId: null,
|
|
folderPath: []
|
|
});
|
|
}
|
|
},
|
|
|
|
setDocumentFilters: (filters) =>
|
|
set((state) => ({
|
|
documentFilters: { ...state.documentFilters, ...filters }
|
|
})),
|
|
|
|
setViewMode: (mode) =>
|
|
set({ viewMode: mode }),
|
|
|
|
setSortBy: (sortBy) =>
|
|
set({ sortBy }),
|
|
|
|
setSortOrder: (order) =>
|
|
set({ sortOrder: order }),
|
|
|
|
selectDocument: (id) =>
|
|
set((state) => ({
|
|
selectedDocuments: [...state.selectedDocuments, id]
|
|
})),
|
|
|
|
deselectDocument: (id) =>
|
|
set((state) => ({
|
|
selectedDocuments: state.selectedDocuments.filter(d => d !== id)
|
|
})),
|
|
|
|
selectAllDocuments: (ids) =>
|
|
set({ selectedDocuments: ids }),
|
|
|
|
clearSelection: () =>
|
|
set({ selectedDocuments: [] }),
|
|
|
|
addToUploadQueue: (item) =>
|
|
set((state) => ({
|
|
uploadQueue: [...state.uploadQueue, item]
|
|
})),
|
|
|
|
removeFromUploadQueue: (id) =>
|
|
set((state) => ({
|
|
uploadQueue: state.uploadQueue.filter(i => i.id !== id)
|
|
})),
|
|
|
|
clearUploadQueue: () =>
|
|
set({ uploadQueue: [] })
|
|
}),
|
|
{
|
|
name: 'documents-store',
|
|
partialize: (state) => ({
|
|
viewMode: state.viewMode,
|
|
sortBy: state.sortBy,
|
|
sortOrder: state.sortOrder
|
|
})
|
|
}
|
|
)
|
|
)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Store Patterns
|
|
|
|
### Selectors
|
|
|
|
```typescript
|
|
// Memoized selectors for performance
|
|
export const selectActiveProjects = (state: ConstructionState) =>
|
|
state.projectFilters.status === 'active';
|
|
|
|
export const selectFilteredProjects = createSelector(
|
|
[(state) => state.projects, (state) => state.projectFilters],
|
|
(projects, filters) => {
|
|
return projects.filter(p => {
|
|
if (filters.status && p.status !== filters.status) return false;
|
|
if (filters.search && !p.name.includes(filters.search)) return false;
|
|
return true;
|
|
});
|
|
}
|
|
);
|
|
```
|
|
|
|
### Actions with Side Effects
|
|
|
|
```typescript
|
|
// Using middleware for side effects
|
|
const logMiddleware = (config) => (set, get, api) =>
|
|
config(
|
|
(...args) => {
|
|
console.log(' applying', args);
|
|
set(...args);
|
|
console.log(' new state', get());
|
|
},
|
|
get,
|
|
api
|
|
);
|
|
```
|
|
|
|
### Hydration
|
|
|
|
```typescript
|
|
// Hydrate store from server
|
|
export const hydrateStore = async () => {
|
|
const userData = await fetchUserData();
|
|
useAuthStore.setState({ user: userData });
|
|
|
|
const tenantData = await fetchTenantData();
|
|
useTenantStore.setState({ currentTenant: tenantData });
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
*Ultima actualizacion: 2025-12-05*
|