Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM, Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA. Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
545
frontend/src/hooks/useApi.ts
Normal file
545
frontend/src/hooks/useApi.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import axios from 'axios'
|
||||
import type {
|
||||
Quote, GeoNews, GeoRiskScore, PatternMatch, TradeIdea,
|
||||
EconomicEvent, HistoricalCandle, BacktestResult
|
||||
} from '../types'
|
||||
|
||||
export const api = axios.create({ baseURL: '/api' })
|
||||
|
||||
// ── Market ────────────────────────────────────────────────────────────────────
|
||||
export const useAllQuotes = () =>
|
||||
useQuery<Record<string, Quote[]>>({
|
||||
queryKey: ['quotes'],
|
||||
queryFn: () => api.get('/market/quotes').then(r => r.data),
|
||||
refetchInterval: 60_000,
|
||||
})
|
||||
|
||||
export const useHistory = (symbol: string, period = '1y', interval = '1d') =>
|
||||
useQuery<HistoricalCandle[]>({
|
||||
queryKey: ['history', symbol, period, interval],
|
||||
queryFn: () =>
|
||||
api.get(`/market/history/${encodeURIComponent(symbol)}`, { params: { period, interval } }).then(r => r.data),
|
||||
enabled: !!symbol,
|
||||
})
|
||||
|
||||
// ── Geo ───────────────────────────────────────────────────────────────────────
|
||||
export const useGeoNews = () =>
|
||||
useQuery<GeoNews[]>({
|
||||
queryKey: ['geo-news'],
|
||||
queryFn: () => api.get('/geo/news').then(r => r.data),
|
||||
refetchInterval: 3_600_000,
|
||||
})
|
||||
|
||||
export const useGeoRiskScore = () =>
|
||||
useQuery<GeoRiskScore>({
|
||||
queryKey: ['geo-risk-score'],
|
||||
queryFn: () => api.get('/geo/risk-score').then(r => r.data),
|
||||
refetchInterval: 3_600_000,
|
||||
})
|
||||
|
||||
export const usePatternMatches = () =>
|
||||
useQuery<PatternMatch[]>({
|
||||
queryKey: ['pattern-matches'],
|
||||
queryFn: () => api.get('/geo/pattern-matches').then(r => r.data),
|
||||
})
|
||||
|
||||
export const usePatternRelevance = (days: number) =>
|
||||
useQuery({
|
||||
queryKey: ['pattern-relevance', days],
|
||||
queryFn: () => api.get('/geo/pattern-relevance', { params: { days } }).then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useTradeIdeas = () =>
|
||||
useQuery<TradeIdea[]>({
|
||||
queryKey: ['trade-ideas'],
|
||||
queryFn: () => api.get('/geo/trade-ideas').then(r => r.data),
|
||||
})
|
||||
|
||||
export const useCalendar = () =>
|
||||
useQuery<EconomicEvent[]>({
|
||||
queryKey: ['calendar'],
|
||||
queryFn: () => api.get('/geo/calendar').then(r => r.data),
|
||||
})
|
||||
|
||||
// ── Options ───────────────────────────────────────────────────────────────────
|
||||
export const useIvSurface = (symbol: string) =>
|
||||
useQuery({
|
||||
queryKey: ['iv-surface', symbol],
|
||||
queryFn: () => api.get('/options/iv-surface', { params: { symbol } }).then(r => r.data),
|
||||
enabled: !!symbol,
|
||||
})
|
||||
|
||||
export const usePnlCurve = (params: {
|
||||
symbol: string; strike: number; expiry_days: number
|
||||
option_type: string; quantity: number; premium_paid: number
|
||||
}) =>
|
||||
useQuery({
|
||||
queryKey: ['pnl-curve', params],
|
||||
queryFn: () => api.get('/options/pnl-curve', { params }).then(r => r.data),
|
||||
enabled: !!params.symbol && !!params.strike && !!params.premium_paid,
|
||||
})
|
||||
|
||||
// ── Backtest ──────────────────────────────────────────────────────────────────
|
||||
export const useBacktest = () =>
|
||||
useMutation<BacktestResult, Error, Record<string, unknown>>({
|
||||
mutationFn: (data) => api.post('/backtest/run', data).then(r => r.data),
|
||||
})
|
||||
|
||||
// ── AI ────────────────────────────────────────────────────────────────────────
|
||||
export const useAiStatus = () =>
|
||||
useQuery({
|
||||
queryKey: ['ai-status'],
|
||||
queryFn: () => api.get('/ai/status').then(r => r.data),
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
export const useAiTopIdeas = () =>
|
||||
useQuery({
|
||||
queryKey: ['ai-top-ideas'],
|
||||
queryFn: () => api.get('/ai/top-ideas').then(r => r.data),
|
||||
enabled: false,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
export const useAnalyzeSpeech = () =>
|
||||
useMutation({
|
||||
mutationFn: (data: { text: string; speaker?: string }) =>
|
||||
api.post('/ai/analyze-speech', data).then(r => r.data),
|
||||
})
|
||||
|
||||
export const useEvaluatePattern = () =>
|
||||
useMutation({
|
||||
mutationFn: (pattern: Record<string, unknown>) =>
|
||||
api.post('/ai/evaluate-pattern', { pattern }).then(r => r.data),
|
||||
})
|
||||
|
||||
export const useSuggestPattern = () =>
|
||||
useMutation({
|
||||
mutationFn: (context: string) =>
|
||||
api.post('/ai/suggest-pattern', { context }).then(r => r.data),
|
||||
})
|
||||
|
||||
export const useAiTopIdeasRefetch = () =>
|
||||
useMutation({
|
||||
mutationFn: () => api.get('/ai/top-ideas').then(r => r.data),
|
||||
})
|
||||
|
||||
// ── Portfolio ─────────────────────────────────────────────────────────────────
|
||||
export const usePortfolioPositions = (status = 'open') =>
|
||||
useQuery({
|
||||
queryKey: ['portfolio', status],
|
||||
queryFn: () => api.get('/portfolio/positions', { params: { status } }).then(r => r.data),
|
||||
refetchInterval: status === 'open' ? 60_000 : false,
|
||||
})
|
||||
|
||||
export const usePortfolioSummary = () =>
|
||||
useQuery({
|
||||
queryKey: ['portfolio-summary'],
|
||||
queryFn: () => api.get('/portfolio/summary').then(r => r.data),
|
||||
refetchInterval: 60_000,
|
||||
})
|
||||
|
||||
export const usePnlHistory = () =>
|
||||
useQuery({
|
||||
queryKey: ['pnl-history'],
|
||||
queryFn: () => api.get('/portfolio/pnl-history').then(r => r.data),
|
||||
})
|
||||
|
||||
export const useAddPosition = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: Record<string, unknown>) =>
|
||||
api.post('/portfolio/add', data).then(r => r.data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['portfolio'] })
|
||||
qc.invalidateQueries({ queryKey: ['portfolio-summary'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useClosePosition = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, close_value }: { id: string; close_value: number }) =>
|
||||
api.post(`/portfolio/close/${id}`, { close_value }).then(r => r.data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['portfolio'] })
|
||||
qc.invalidateQueries({ queryKey: ['portfolio-summary'] })
|
||||
qc.invalidateQueries({ queryKey: ['pnl-history'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
export const useConfig = () =>
|
||||
useQuery({
|
||||
queryKey: ['config'],
|
||||
queryFn: () => api.get('/config/').then(r => r.data),
|
||||
})
|
||||
|
||||
export const useSources = () =>
|
||||
useQuery({
|
||||
queryKey: ['sources'],
|
||||
queryFn: () => api.get('/config/sources').then(r => r.data),
|
||||
})
|
||||
|
||||
export const useUpdateSources = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (sources: Record<string, unknown>) =>
|
||||
api.put('/config/sources', { sources }).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['sources'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateApiKeys = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (keys: Record<string, string>) =>
|
||||
api.put('/config/api-keys', keys).then(r => r.data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['config'] })
|
||||
qc.invalidateQueries({ queryKey: ['ai-status'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── AI Pattern Scoring ────────────────────────────────────────────────────────
|
||||
export const useScorePatterns = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (params: { top_n?: number; category_filter?: string; template?: string }) =>
|
||||
api.post('/ai/score-patterns', params).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['last-scores'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useLastScores = () =>
|
||||
useQuery({
|
||||
queryKey: ['last-scores'],
|
||||
queryFn: () => api.get('/ai/last-scores').then(r => r.data),
|
||||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
export const useSuggestNewPatterns = () =>
|
||||
useMutation({
|
||||
mutationFn: () => api.post('/ai/suggest-new-patterns').then(r => r.data),
|
||||
})
|
||||
|
||||
export const useAnalysisConfig = () =>
|
||||
useQuery({
|
||||
queryKey: ['analysis-config'],
|
||||
queryFn: () => api.get('/config/analysis').then(r => r.data),
|
||||
})
|
||||
|
||||
export const useSaveAnalysisConfig = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (cfg: { top_n?: number; category_filter?: string; template?: string }) =>
|
||||
api.put('/config/analysis', cfg).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['analysis-config'] }),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Patterns ──────────────────────────────────────────────────────────────────
|
||||
export const useAllPatterns = () =>
|
||||
useQuery({
|
||||
queryKey: ['all-patterns'],
|
||||
queryFn: () => api.get('/patterns/all').then(r => r.data),
|
||||
})
|
||||
|
||||
export const usePatternSimilarity = () =>
|
||||
useQuery({
|
||||
queryKey: ['pattern-similarity'],
|
||||
queryFn: () => api.get('/ai/pattern-similarity').then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useMacroRegime = () => {
|
||||
const qc = useQueryClient()
|
||||
const query = useQuery({
|
||||
queryKey: ['macro-regime'],
|
||||
queryFn: () => api.get('/market/macro-regime').then(r => r.data),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
refetchInterval: 15 * 60 * 1000,
|
||||
})
|
||||
const forceRefetch = async () => {
|
||||
const fresh = await api.get('/market/macro-regime?force=true').then(r => r.data)
|
||||
qc.setQueryData(['macro-regime'], fresh)
|
||||
return fresh
|
||||
}
|
||||
return { ...query, forceRefetch }
|
||||
}
|
||||
|
||||
export const useSavePattern = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (pattern: Record<string, unknown>) =>
|
||||
api.post('/patterns/custom', pattern).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeletePattern = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
api.delete(`/patterns/custom/${id}`).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useTogglePattern = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
api.put(`/patterns/toggle/${id}`).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Auto-Cycle ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const useCycleStatus = () =>
|
||||
useQuery({
|
||||
queryKey: ['cycle-status'],
|
||||
queryFn: () => api.get('/cycle/status').then(r => r.data),
|
||||
staleTime: 5_000,
|
||||
// Poll every 5s while a cycle is running, every 30s otherwise
|
||||
refetchInterval: (query) => ((query.state.data as any)?.running ? 5_000 : 30_000),
|
||||
})
|
||||
|
||||
export const useCycleHistory = (limit = 20) =>
|
||||
useQuery({
|
||||
queryKey: ['cycle-history', limit],
|
||||
queryFn: () => api.get(`/cycle/history?limit=${limit}`).then(r => r.data),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
export const useUpdateCycleConfig = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (cfg: { enabled?: boolean; interval_hours?: number; similarity_threshold?: number; min_ev_threshold?: number; min_score_threshold?: number }) =>
|
||||
api.post('/cycle/config', cfg).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['cycle-status'] }),
|
||||
})
|
||||
}
|
||||
|
||||
/** Keys to refresh once a cycle finishes — covers cockpit, macro, journal */
|
||||
export const CYCLE_REFRESH_KEYS = [
|
||||
['macro-regime'],
|
||||
['last-scores'],
|
||||
['all-patterns'],
|
||||
['cycle-history'],
|
||||
['cycle-status'],
|
||||
['journal-summary'],
|
||||
['journal-mtm'],
|
||||
['journal-geo'],
|
||||
['journal-macro'],
|
||||
['geo-risk-score'],
|
||||
['pattern-matches'],
|
||||
]
|
||||
|
||||
export const useTriggerCycle = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: () => api.post('/cycle/trigger').then(r => r.data),
|
||||
onSuccess: () => {
|
||||
// Immediate status refresh so the spinner shows
|
||||
qc.invalidateQueries({ queryKey: ['cycle-status'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount this hook once at a high level (e.g. CyclesSection or App).
|
||||
* It watches cycle-status and, when a running cycle finishes, refreshes
|
||||
* all cockpit + journal queries so the UI reflects the new scores/regime.
|
||||
*/
|
||||
export const useCycleWatcher = () => {
|
||||
const qc = useQueryClient()
|
||||
const { data: statusData } = useCycleStatus()
|
||||
const wasRunning = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const running = (statusData as any)?.running ?? false
|
||||
if (running) {
|
||||
wasRunning.current = true
|
||||
} else if (wasRunning.current) {
|
||||
// Transition: was running → now done → refresh everything
|
||||
wasRunning.current = false
|
||||
CYCLE_REFRESH_KEYS.forEach(key => qc.invalidateQueries({ queryKey: key }))
|
||||
}
|
||||
}, [(statusData as any)?.running])
|
||||
}
|
||||
|
||||
// ── Journal de Bord ──────────────────────────────────────────────────────────
|
||||
|
||||
export const useJournalSummary = () =>
|
||||
useQuery({
|
||||
queryKey: ['journal-summary'],
|
||||
queryFn: () => api.get('/journal/summary').then(r => r.data),
|
||||
staleTime: 2 * 60_000,
|
||||
})
|
||||
|
||||
export const useMacroHistory = (days = 15) =>
|
||||
useQuery({
|
||||
queryKey: ['journal-macro', days],
|
||||
queryFn: () => api.get(`/journal/macro-history?days=${days}`).then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useGeoHistory = (days = 30) =>
|
||||
useQuery({
|
||||
queryKey: ['journal-geo', days],
|
||||
queryFn: () => api.get(`/journal/geo-history?days=${days}`).then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useTradeMtm = (days = 30) =>
|
||||
useQuery({
|
||||
queryKey: ['journal-mtm', days],
|
||||
queryFn: () => api.get(`/journal/trade-mtm?days=${days}`).then(r => r.data),
|
||||
staleTime: 0,
|
||||
refetchInterval: 5 * 60_000, // re-fetch live prices every 5 minutes
|
||||
refetchIntervalInBackground: false,
|
||||
})
|
||||
|
||||
// ── Risk Profiles ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const useRiskProfiles = () =>
|
||||
useQuery({
|
||||
queryKey: ['risk-profiles'],
|
||||
queryFn: () => api.get('/profiles').then(r => r.data),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
export const useUpsertProfile = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (profile: {
|
||||
id?: number; name: string; min_score: number; min_gain_pct: number;
|
||||
color?: string; enabled?: boolean; sort_order?: number
|
||||
}) => {
|
||||
if (profile.id) {
|
||||
return api.put(`/profiles/${profile.id}`, profile).then(r => r.data)
|
||||
}
|
||||
return api.post('/profiles', profile).then(r => r.data)
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['risk-profiles'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteProfile = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => api.delete(`/profiles/${id}`).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['risk-profiles'] }),
|
||||
})
|
||||
}
|
||||
|
||||
export const usePreviewTradeScore = (score: number, gainPct: number, enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: ['profile-preview', score, gainPct],
|
||||
queryFn: () => api.get(`/profiles/preview?score=${score}&gain_pct=${gainPct}`).then(r => r.data),
|
||||
enabled,
|
||||
staleTime: 0,
|
||||
})
|
||||
|
||||
// ── Reasoning Traces / Post-mortem ────────────────────────────────────────────
|
||||
|
||||
export const useTradePostmortem = (tradeId: number | null) =>
|
||||
useQuery({
|
||||
queryKey: ['postmortem', tradeId],
|
||||
queryFn: () => api.get(`/reasoning/postmortem/${tradeId}`).then(r => r.data),
|
||||
enabled: tradeId !== null,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
export const useAnalyzePostmortem = () =>
|
||||
useMutation({
|
||||
mutationFn: (tradeId: number) =>
|
||||
api.post(`/reasoning/postmortem/${tradeId}/analyze`).then(r => r.data),
|
||||
})
|
||||
|
||||
export const usePortfolioReportData = (days: number) =>
|
||||
useQuery({
|
||||
queryKey: ['portfolio-report-data', days],
|
||||
queryFn: () => api.get(`/reasoning/portfolio-report?days=${days}`).then(r => r.data),
|
||||
staleTime: 120_000,
|
||||
})
|
||||
|
||||
export const useGeneratePortfolioReport = () =>
|
||||
useMutation({
|
||||
mutationFn: (days: number) =>
|
||||
api.post(`/reasoning/portfolio-report/generate?days=${days}`).then(r => r.data),
|
||||
})
|
||||
|
||||
export const useAiReportsList = () =>
|
||||
useQuery({
|
||||
queryKey: ['ai-reports-list'],
|
||||
queryFn: () => api.get('/reasoning/reports').then(r => r.data),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
export const useAiReport = (reportId: number | null) =>
|
||||
useQuery({
|
||||
queryKey: ['ai-report', reportId],
|
||||
queryFn: () => api.get(`/reasoning/reports/${reportId}`).then(r => r.data),
|
||||
enabled: reportId !== null,
|
||||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
// ── Super Contexte / Knowledge Base ──────────────────────────────────────────
|
||||
export const useKnowledgeState = () =>
|
||||
useQuery({
|
||||
queryKey: ['knowledge-state'],
|
||||
queryFn: () => api.get('/knowledge/state').then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useKnowledgeHistory = () =>
|
||||
useQuery({
|
||||
queryKey: ['knowledge-history'],
|
||||
queryFn: () => api.get('/knowledge/history').then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useKnowledgeStateVersion = (stateId: number | null) =>
|
||||
useQuery({
|
||||
queryKey: ['knowledge-state-version', stateId],
|
||||
queryFn: () => api.get(`/knowledge/history/${stateId}`).then(r => r.data),
|
||||
enabled: stateId !== null,
|
||||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
export const useKnowledgeEntries = () =>
|
||||
useQuery({
|
||||
queryKey: ['knowledge-entries'],
|
||||
queryFn: () => api.get('/knowledge/entries').then(r => r.data),
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
|
||||
export const useSynthesizeKnowledge = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: () => api.post('/knowledge/synthesize').then(r => r.data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['knowledge-state'] })
|
||||
qc.invalidateQueries({ queryKey: ['knowledge-history'] })
|
||||
qc.invalidateQueries({ queryKey: ['knowledge-entries'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const usePatchKbEntryStatus = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, status }: { id: number; status: string }) =>
|
||||
api.patch(`/knowledge/entries/${id}/status`, { status }).then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['knowledge-entries'] }),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user