refactor(arch): eliminate direct service calls in components — route all through hooks

New hook files:
- useAuth.ts: useLogin, useSwitchDemoRole, useSwitchOrganization
- useAI.ts: useParseNeed, useGenerateOfferEmail, useGenerateDecisionBrief, useParseListingText
- useAssistant.ts: useAssistantSuggestions (useQuery), useAssistantAnswer (useMutation)
- useOfferReport.ts: useOfferReportByInquiry, useCreateOfferReport, useUpdateOfferReport, useGenerateOfferReportPdf
- useInquiryReport.ts: useInquiryReportByInquiry, useCreateInquiryReport, useUpdateInquiryReport, useFinalizeInquiryReport
- useMarketReport.ts: useMarketReport
- useWeighting.ts: useDefaultWeights (synchronous wrapper)
- useUnitMatches.ts: useUnitMatchesMap, useUnitBundle, useBundleMatches (useMemo wrappers)

Extended hooks: useProperties (add update/create/remove mutations),
useNeeds (add useCreateNeed), useMatches (add useNeedMatchesForProperty,
useAdditionalMatchesForInquiry), useReviewQueue (add useCreateReviewTask)

Updated 21 components/pages: all direct service imports replaced with hooks.
Deliberate exception: getRecommendedActions in DataQuality.tsx (pure sync utility, no provider access).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 14:56:32 +02:00
parent f487435a94
commit e36c5bc979
33 changed files with 666 additions and 407 deletions
+86 -67
View File
@@ -18,9 +18,9 @@ import {
NeedCardPreview,
NeedBuilderErrorState,
} from '../../components/demand'
import { aiService } from '../../services/aiService'
import { needService } from '../../services/needService'
import { weightingService } from '../../services/weightingService'
import { useParseNeed } from '../../hooks/useAI'
import { useCreateNeed } from '../../hooks/useNeeds'
import { useDefaultWeights } from '../../hooks/useWeighting'
import { NeedBuilderStep } from '../../domain/needBuilder'
import type { ParseNeedResult, ParsedNeedCriteria, WeightingKey } from '../../domain/needBuilder'
import { generateSummary, buildNeedInput } from '../../services/aiSearch/needSearchMapper'
@@ -35,6 +35,10 @@ export default function AISearch() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const parseNeedMutation = useParseNeed()
const createNeedMutation = useCreateNeed()
const defaultWeights = useDefaultWeights()
const [step, setStep] = useState<typeof NeedBuilderStep[keyof typeof NeedBuilderStep]>(NeedBuilderStep.IDLE)
const [intent, setIntent] = useState<ActionIntent>('search')
const [inputText, setInputText] = useState('')
@@ -42,7 +46,7 @@ export default function AISearch() {
const [criteria, setCriteria] = useState<ParsedNeedCriteria>({})
const [parseResult, setParseResult] = useState<ParseNeedResult | null>(null)
const [editedCriteria, setEditedCriteria] = useState<ParsedNeedCriteria | null>(null)
const [weights, setWeights] = useState<Record<WeightingKey, number>>(weightingService.getDefaultWeights())
const [weights, setWeights] = useState<Record<WeightingKey, number>>(defaultWeights)
const [weightingKey, setWeightingKey] = useState(0)
const [needTitle, setNeedTitle] = useState('')
const [error, setError] = useState<string | null>(null)
@@ -64,71 +68,85 @@ export default function AISearch() {
setInputText(text)
}
async function handleAiAutofill() {
function handleAiAutofill() {
setStep(NeedBuilderStep.PARSING)
setError(null)
try {
const resp = await aiService.parseNeed(inputText)
const result = resp.data
setParseResult(result)
setCriteria({ ...result.extractedCriteria })
setWeights(result.suggestedWeights as Record<WeightingKey, number>)
setWeightingKey(k => k + 1)
isManualTextRef.current = false
setInputText('')
setIsAutoGen(false)
setStep(NeedBuilderStep.IDLE)
} catch {
setError('Die KI-Analyse ist fehlgeschlagen.')
setStep(NeedBuilderStep.ERROR)
}
}
// Resolve criteria (parse text if needed), then either search or show save preview
async function handleAction(chosenIntent: ActionIntent) {
setIntent(chosenIntent)
setError(null)
let resolved: ParsedNeedCriteria = criteria
let resolvedResult: ParseNeedResult | null = parseResult
if (!hasStructuredData && inputText.trim()) {
setStep(NeedBuilderStep.PARSING)
try {
const resp = await aiService.parseNeed(inputText)
resolved = resp.data.extractedCriteria
resolvedResult = resp.data
setCriteria(resolved)
setWeights(resp.data.suggestedWeights as Record<WeightingKey, number>)
parseNeedMutation.mutate(inputText, {
onSuccess: (resp) => {
const result = resp.data
setParseResult(result)
setCriteria({ ...result.extractedCriteria })
setWeights(result.suggestedWeights as Record<WeightingKey, number>)
setWeightingKey(k => k + 1)
isManualTextRef.current = false
setInputText('')
setIsAutoGen(false)
setParseResult(resp.data)
} catch {
setStep(NeedBuilderStep.IDLE)
},
onError: () => {
setError('Die KI-Analyse ist fehlgeschlagen.')
setStep(NeedBuilderStep.ERROR)
return
}
},
})
}
// Resolve criteria (parse text if needed), then either search or show save preview
function handleAction(chosenIntent: ActionIntent) {
setIntent(chosenIntent)
setError(null)
const resolved: ParsedNeedCriteria = criteria
const resolvedResult: ParseNeedResult | null = parseResult
if (!hasStructuredData && inputText.trim()) {
setStep(NeedBuilderStep.PARSING)
parseNeedMutation.mutate(inputText, {
onSuccess: (resp) => {
const parsedResolved = resp.data.extractedCriteria
const parsedResult = resp.data
setCriteria(parsedResolved)
setWeights(resp.data.suggestedWeights as Record<WeightingKey, number>)
setWeightingKey(k => k + 1)
isManualTextRef.current = false
setInputText('')
setIsAutoGen(false)
setParseResult(resp.data)
continueAction(chosenIntent, parsedResolved, parsedResult)
},
onError: () => {
setError('Die KI-Analyse ist fehlgeschlagen.')
setStep(NeedBuilderStep.ERROR)
},
})
return
}
continueAction(chosenIntent, resolved, resolvedResult)
}
function continueAction(
chosenIntent: ActionIntent,
resolved: ParsedNeedCriteria,
resolvedResult: ParseNeedResult | null,
) {
if (chosenIntent === 'search') {
// Save as DRAFT and navigate immediately
setStep(NeedBuilderStep.SAVING)
try {
const conf = resolvedResult
? Object.values(resolvedResult.confidenceByField).reduce((s, v) => s + v, 0) /
Math.max(Object.values(resolvedResult.confidenceByField).length, 1)
: 0.5
const input = buildNeedInput(resolved, weights, needTitle, conf, 'DRAFT')
const created = await needService.create(input)
await queryClient.invalidateQueries({ queryKey: ['needs'] })
await queryClient.invalidateQueries({ queryKey: ['matches'] })
navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } })
} catch {
setError('Suche fehlgeschlagen.')
setStep(NeedBuilderStep.ERROR)
}
const conf = resolvedResult
? Object.values(resolvedResult.confidenceByField).reduce((s, v) => s + v, 0) /
Math.max(Object.values(resolvedResult.confidenceByField).length, 1)
: 0.5
const input = buildNeedInput(resolved, weights, needTitle, conf, 'DRAFT')
createNeedMutation.mutate(input, {
onSuccess: (created) => {
queryClient.invalidateQueries({ queryKey: ['matches'] })
navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } })
},
onError: () => {
setError('Suche fehlgeschlagen.')
setStep(NeedBuilderStep.ERROR)
},
})
return
}
@@ -156,21 +174,22 @@ export default function AISearch() {
setStep(NeedBuilderStep.READY_TO_SAVE)
}
async function handleSaveProfile() {
function handleSaveProfile() {
if (!editedCriteria || !parseResult) return
setStep(NeedBuilderStep.SAVING)
const entries = Object.entries(parseResult.confidenceByField)
const conf = entries.length > 0 ? entries.reduce((s, [, v]) => s + v, 0) / entries.length : 0
try {
const input = buildNeedInput(editedCriteria, weights, needTitle, conf, 'ACTIVE')
const created = await needService.create(input)
await queryClient.invalidateQueries({ queryKey: ['needs'] })
await queryClient.invalidateQueries({ queryKey: ['matches'] })
navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } })
} catch {
setError('Speichern fehlgeschlagen.')
setStep(NeedBuilderStep.ERROR)
}
const input = buildNeedInput(editedCriteria, weights, needTitle, conf, 'ACTIVE')
createNeedMutation.mutate(input, {
onSuccess: (created) => {
queryClient.invalidateQueries({ queryKey: ['matches'] })
navigate('/demand/results', { state: { fromNeedBuilder: true, activeNeedId: created.data.id } })
},
onError: () => {
setError('Speichern fehlgeschlagen.')
setStep(NeedBuilderStep.ERROR)
},
})
}
function handleRetry() {