refactor(state): clean layoutStore, add selectors, centralise STALE constants
- layoutStore: remove 4 dead field groups (pinnedPanels, selectedResultId, compareTrayVisible, notificationsOpen) — none were read outside the store - CompareTray: drop dead useLayoutStore side-effect (state was write-only) - 8 components: replace bare useStore() with explicit selectors / useShallow to prevent unnecessary re-renders on unrelated state mutations - lib/constants: add STALE_MARKET_SIGNALS + STALE_REVIEW_QUEUE (30s each) - useMarketSignals, useReviewQueue: use global constants instead of hook-local magic numbers - Add STATE_MANAGEMENT.md: decision tree + rules for RQ/Zustand/local/derived Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ const WORKSPACE_HOME: Record<string, string> = {
|
||||
}
|
||||
|
||||
function RoleRedirect() {
|
||||
const { currentUser } = useSessionStore()
|
||||
const currentUser = useSessionStore(s => s.currentUser)
|
||||
const first = currentUser?.allowedWorkspaces[0] ?? WorkspaceType.SUPPLY
|
||||
return <Navigate to={WORKSPACE_HOME[first] ?? '/supply/dashboard'} replace />
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Sparkles } from 'lucide-react'
|
||||
import { useAssistantStore } from '../../stores/assistantStore'
|
||||
|
||||
export function GlobalAIAssistantButton() {
|
||||
const { isOpen, open } = useAssistantStore()
|
||||
const isOpen = useAssistantStore(s => s.isOpen)
|
||||
const open = useAssistantStore(s => s.open)
|
||||
|
||||
return (
|
||||
<Tooltip title="AI Assistent öffnen" placement="left">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Box, Divider, Drawer, IconButton, TextField, Tooltip, Typography } from '@mui/material'
|
||||
import { RotateCcw, Send, Sparkles, X } from 'lucide-react'
|
||||
import { useLocation } from 'react-router'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useAssistantStore } from '../../stores/assistantStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { aiAssistantService } from '../../services/aiAssistantService'
|
||||
@@ -21,7 +22,7 @@ function resolveWorkspace(pathname: string): WorkspaceType | null {
|
||||
|
||||
export function GlobalAIAssistantDrawer() {
|
||||
const { isOpen, close, context, setContext, messages, isLoading, error, addMessage, setLoading, setError, clearConversation } =
|
||||
useAssistantStore()
|
||||
useAssistantStore(useShallow(s => s))
|
||||
|
||||
const { currentUser } = useSessionStore()
|
||||
const location = useLocation()
|
||||
|
||||
@@ -17,8 +17,10 @@ import { WorkspaceType } from '../../domain/enums'
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function AppShell() {
|
||||
const { activeWorkspace, sidebarCollapsed, setActiveWorkspace, toggleSidebar } =
|
||||
useLayoutStore()
|
||||
const activeWorkspace = useLayoutStore(s => s.activeWorkspace)
|
||||
const sidebarCollapsed = useLayoutStore(s => s.sidebarCollapsed)
|
||||
const setActiveWorkspace = useLayoutStore(s => s.setActiveWorkspace)
|
||||
const toggleSidebar = useLayoutStore(s => s.toggleSidebar)
|
||||
const { currentUser } = useSessionStore()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router'
|
||||
import { Box, Button, IconButton, Typography } from '@mui/material'
|
||||
import { X } from 'lucide-react'
|
||||
import { useCompareStore } from '../../stores/compareStore'
|
||||
import { useLayoutStore } from '../../stores/layoutStore'
|
||||
|
||||
const TYPE_DOT: Record<string, string> = {
|
||||
VERIFIED_PORTFOLIO: '#1e3a5f',
|
||||
@@ -14,15 +12,10 @@ const TYPE_DOT: Record<string, string> = {
|
||||
|
||||
export function CompareTray() {
|
||||
const { compareItems, removeFromCompare, clearCompare } = useCompareStore()
|
||||
const { setCompareTrayVisible } = useLayoutStore()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const isDemand = location.pathname.startsWith('/demand')
|
||||
|
||||
useEffect(() => {
|
||||
setCompareTrayVisible(compareItems.length > 0 && isDemand)
|
||||
}, [compareItems.length, setCompareTrayVisible, isDemand])
|
||||
|
||||
if (!isDemand) return null
|
||||
|
||||
const getTitle = (item: (typeof compareItems)[number]) => {
|
||||
|
||||
@@ -18,7 +18,9 @@ const PANEL_PLACEHOLDERS: Record<RightPanelContentType, string> = {
|
||||
}
|
||||
|
||||
export function RightContextPanel() {
|
||||
const { isRightPanelOpen, rightPanelContentType, closeRightPanel } = useLayoutStore()
|
||||
const isRightPanelOpen = useLayoutStore(s => s.isRightPanelOpen)
|
||||
const rightPanelContentType = useLayoutStore(s => s.rightPanelContentType)
|
||||
const closeRightPanel = useLayoutStore(s => s.closeRightPanel)
|
||||
|
||||
const title = rightPanelContentType ? PANEL_TITLES[rightPanelContentType] : ''
|
||||
const placeholder = rightPanelContentType ? PANEL_PLACEHOLDERS[rightPanelContentType] : ''
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { useReminders } from '../../hooks/useReminders'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useReminderStore } from '../../stores/reminderStore'
|
||||
import { ReminderListRow } from './ReminderListRow'
|
||||
import { ReminderCard } from './ReminderCard'
|
||||
@@ -34,7 +35,13 @@ export function ReminderFeed() {
|
||||
const {
|
||||
filterType, filterStatus, filterPriority, searchQuery, viewMode,
|
||||
setFilterType, setFilterStatus, setFilterPriority, setSearchQuery,
|
||||
} = useReminderStore()
|
||||
} = useReminderStore(useShallow(s => ({
|
||||
filterType: s.filterType, filterStatus: s.filterStatus,
|
||||
filterPriority: s.filterPriority, searchQuery: s.searchQuery,
|
||||
viewMode: s.viewMode, setFilterType: s.setFilterType,
|
||||
setFilterStatus: s.setFilterStatus, setFilterPriority: s.setFilterPriority,
|
||||
setSearchQuery: s.setSearchQuery,
|
||||
})))
|
||||
|
||||
if (isLoading) return <ReminderSkeleton />
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, ToggleButtonGroup, ToggleButton, TextField, InputAdornment, Typography } from '@mui/material'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useReminderStore } from '../../stores/reminderStore'
|
||||
import { ReminderType, ReminderStatus, ReminderPriority } from '../../domain/reminder'
|
||||
import type { ReminderType as ReminderTypeType, ReminderStatus as ReminderStatusType, ReminderPriority as ReminderPriorityType } from '../../domain/reminder'
|
||||
@@ -36,7 +37,13 @@ export function ReminderFilterBar() {
|
||||
filterPriority, setFilterPriority,
|
||||
searchQuery, setSearchQuery,
|
||||
viewMode, setViewMode,
|
||||
} = useReminderStore()
|
||||
} = useReminderStore(useShallow(s => ({
|
||||
filterType: s.filterType, setFilterType: s.setFilterType,
|
||||
filterStatus: s.filterStatus, setFilterStatus: s.setFilterStatus,
|
||||
filterPriority: s.filterPriority, setFilterPriority: s.setFilterPriority,
|
||||
searchQuery: s.searchQuery, setSearchQuery: s.setSearchQuery,
|
||||
viewMode: s.viewMode, setViewMode: s.setViewMode,
|
||||
})))
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-3">
|
||||
|
||||
@@ -3,14 +3,13 @@ import { marketIntelligenceService } from '../services/marketIntelligenceService
|
||||
import { reviewService } from '../services/reviewService'
|
||||
import type { MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
|
||||
const STALE_SIGNALS = 30_000
|
||||
import { STALE_MARKET_SIGNALS } from '../lib/constants'
|
||||
|
||||
export function useMarketSignals(filters?: MarketSignalFilters) {
|
||||
return useQuery({
|
||||
queryKey: ['market-signals', filters ?? {}],
|
||||
queryFn: () => marketIntelligenceService.getSignals(filters),
|
||||
staleTime: STALE_SIGNALS,
|
||||
staleTime: STALE_MARKET_SIGNALS,
|
||||
select: (res) => res.data ?? [],
|
||||
})
|
||||
}
|
||||
@@ -20,7 +19,7 @@ export function useMarketSignalDetail(id: string | null) {
|
||||
queryKey: ['market-signal', id],
|
||||
queryFn: () => marketIntelligenceService.getSignalDetail(id!),
|
||||
enabled: id !== null,
|
||||
staleTime: STALE_SIGNALS,
|
||||
staleTime: STALE_MARKET_SIGNALS,
|
||||
select: (res) => res.data ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ import { useSessionStore } from '../stores/sessionStore'
|
||||
import type { ReviewFilters } from '../provider/IReviewProvider'
|
||||
import type { ReviewTaskStatus } from '../domain/review'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
import { STALE_REVIEW_QUEUE } from '../lib/constants'
|
||||
|
||||
const STALE_REVIEW = 30_000
|
||||
const QK = 'reviewQueue'
|
||||
|
||||
export function useReviewQueue(filters?: ReviewFilters) {
|
||||
return useQuery({
|
||||
queryKey: [QK, filters ?? {}],
|
||||
queryFn: () => reviewService.getTasks(filters),
|
||||
staleTime: STALE_REVIEW,
|
||||
staleTime: STALE_REVIEW_QUEUE,
|
||||
select: (res) => res.data ?? [],
|
||||
})
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function useReviewTask(id: string | null) {
|
||||
return useQuery({
|
||||
queryKey: [QK, 'task', id],
|
||||
queryFn: () => reviewService.getTask(id!),
|
||||
staleTime: STALE_REVIEW,
|
||||
staleTime: STALE_REVIEW_QUEUE,
|
||||
enabled: !!id,
|
||||
select: (res) => res.data ?? null,
|
||||
})
|
||||
|
||||
@@ -30,6 +30,8 @@ export const PROB_MEDIUM = 0.5
|
||||
export const STALE_PROPERTIES = 5 * 60 * 1000
|
||||
export const STALE_MATCHES = 2 * 60 * 1000
|
||||
export const STALE_SIGNALS = 5 * 60 * 1000
|
||||
export const STALE_MARKET_SIGNALS = 30 * 1000 // aggressive — market leads refresh often
|
||||
export const STALE_REVIEW_QUEUE = 30 * 1000 // aggressive — ops team works in real-time
|
||||
|
||||
// Route paths — single source of truth
|
||||
export const ROUTES = {
|
||||
|
||||
@@ -16,7 +16,10 @@ interface AuthContextValue {
|
||||
const AuthContext = createContext<AuthContextValue | null>(null)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const { currentUser, isAuthenticated, login, logout } = useSessionStore()
|
||||
const currentUser = useSessionStore(s => s.currentUser)
|
||||
const isAuthenticated = useSessionStore(s => s.isAuthenticated)
|
||||
const login = useSessionStore(s => s.login)
|
||||
const logout = useSessionStore(s => s.logout)
|
||||
|
||||
const value: AuthContextValue = {
|
||||
user: currentUser,
|
||||
|
||||
@@ -12,39 +12,24 @@ export type RightPanelContentType = typeof RightPanelContentType[keyof typeof Ri
|
||||
interface LayoutState {
|
||||
activeWorkspace: WorkspaceType
|
||||
sidebarCollapsed: boolean
|
||||
pinnedPanels: string[]
|
||||
isRightPanelOpen: boolean
|
||||
rightPanelContentType: RightPanelContentType | null
|
||||
compareTrayVisible: boolean
|
||||
selectedResultId: string | null
|
||||
notificationsOpen: boolean
|
||||
// Actions
|
||||
setActiveWorkspace: (workspace: WorkspaceType) => void
|
||||
toggleSidebar: () => void
|
||||
pinPanel: (panelId: string) => void
|
||||
unpinPanel: (panelId: string) => void
|
||||
openRightPanel: (type: RightPanelContentType) => void
|
||||
closeRightPanel: () => void
|
||||
toggleRightPanel: (type: RightPanelContentType) => void
|
||||
setCompareTrayVisible: (visible: boolean) => void
|
||||
setSelectedResultId: (id: string | null) => void
|
||||
toggleNotifications: () => void
|
||||
}
|
||||
|
||||
export const useLayoutStore = create<LayoutState>((set, get) => ({
|
||||
activeWorkspace: WorkspaceType.SUPPLY,
|
||||
sidebarCollapsed: false,
|
||||
pinnedPanels: [],
|
||||
isRightPanelOpen: false,
|
||||
rightPanelContentType: null,
|
||||
compareTrayVisible: false,
|
||||
selectedResultId: null,
|
||||
notificationsOpen: false,
|
||||
|
||||
setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }),
|
||||
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
|
||||
pinPanel: (panelId) => set((s) => ({ pinnedPanels: [...s.pinnedPanels, panelId] })),
|
||||
unpinPanel: (panelId) => set((s) => ({ pinnedPanels: s.pinnedPanels.filter(id => id !== panelId) })),
|
||||
openRightPanel: (type) => set({ isRightPanelOpen: true, rightPanelContentType: type }),
|
||||
closeRightPanel: () => set({ isRightPanelOpen: false, rightPanelContentType: null }),
|
||||
toggleRightPanel: (type) => {
|
||||
@@ -55,7 +40,4 @@ export const useLayoutStore = create<LayoutState>((set, get) => ({
|
||||
set({ isRightPanelOpen: true, rightPanelContentType: type })
|
||||
}
|
||||
},
|
||||
setCompareTrayVisible: (visible) => set({ compareTrayVisible: visible }),
|
||||
setSelectedResultId: (id) => set({ selectedResultId: id }),
|
||||
toggleNotifications: () => set((s) => ({ notificationsOpen: !s.notificationsOpen })),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user