erp-construccion/docs/06-frontend-specs/stores/STORES-spec.md

18 KiB

Zustand Stores Specification

Version: 1.0.0 Fecha: 2025-12-05


Global Stores

AuthStore

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

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

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

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

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

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

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

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

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

// 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

// 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

// 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