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:
@@ -0,0 +1,27 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { aiService, parseListingText } from '../services/aiService'
|
||||
import type { OfferEmailPayload } from '../services/aiService'
|
||||
|
||||
export function useParseNeed() {
|
||||
return useMutation({
|
||||
mutationFn: (text: string) => aiService.parseNeed(text),
|
||||
})
|
||||
}
|
||||
|
||||
export function useGenerateOfferEmail() {
|
||||
return useMutation({
|
||||
mutationFn: (payload: OfferEmailPayload) => aiService.generateOfferEmail(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function useGenerateDecisionBrief() {
|
||||
return useMutation({
|
||||
mutationFn: (shortlistId: string) => aiService.generateDecisionBrief(shortlistId),
|
||||
})
|
||||
}
|
||||
|
||||
export function useParseListingText() {
|
||||
return useMutation({
|
||||
mutationFn: (text: string) => parseListingText(text),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { aiAssistantService } from '../services/aiAssistantService'
|
||||
import type { AssistantContext } from '../domain/assistant'
|
||||
|
||||
export function useAssistantSuggestions(context: AssistantContext | null) {
|
||||
return useQuery({
|
||||
queryKey: ['assistant-suggestions', context],
|
||||
queryFn: () => aiAssistantService.getSuggestions(context!),
|
||||
enabled: !!context,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
|
||||
export function useAssistantAnswer() {
|
||||
return useMutation({
|
||||
mutationFn: ({ context, question }: { context: AssistantContext; question: string }) =>
|
||||
aiAssistantService.answerQuestion(context, question),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { authService } from '../services/authService'
|
||||
import type { UserRole } from '../domain/enums'
|
||||
|
||||
export function useLogin() {
|
||||
const navigate = useNavigate()
|
||||
return useMutation({
|
||||
mutationFn: ({ email, password }: { email: string; password: string }) =>
|
||||
authService.login(email, password),
|
||||
onSuccess: () => {
|
||||
navigate('/')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSwitchDemoRole() {
|
||||
const navigate = useNavigate()
|
||||
return useMutation({
|
||||
mutationFn: (role: UserRole) => authService.switchDemoRole(role),
|
||||
onSuccess: () => {
|
||||
navigate('/')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useSwitchOrganization() {
|
||||
return useMutation({
|
||||
mutationFn: (organizationId: string) => authService.switchOrganization(organizationId),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { inquiryReportService } from '../services/inquiryReportService'
|
||||
import type { InquiryPreparationReportDraft } from '../domain/inquiryReport'
|
||||
|
||||
export function useInquiryReportByInquiry(inquiryId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['inquiry-report', inquiryId],
|
||||
queryFn: () => inquiryReportService.getByInquiry(inquiryId!),
|
||||
enabled: !!inquiryId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateInquiryReport() {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
inquiryId,
|
||||
selectedPropertyIds,
|
||||
}: {
|
||||
inquiryId: string
|
||||
selectedPropertyIds: string[]
|
||||
}) => inquiryReportService.create(inquiryId, selectedPropertyIds),
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateInquiryReport() {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
draftId,
|
||||
data,
|
||||
}: {
|
||||
draftId: string
|
||||
data: Partial<InquiryPreparationReportDraft>
|
||||
}) => inquiryReportService.update(draftId, data),
|
||||
})
|
||||
}
|
||||
|
||||
export function useFinalizeInquiryReport() {
|
||||
return useMutation({
|
||||
mutationFn: (draftId: string) => inquiryReportService.finalize(draftId),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { marketReportService } from '../services/marketReportService'
|
||||
|
||||
export function useMarketReport(propertyId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['market-report', propertyId],
|
||||
queryFn: () => marketReportService.getByProperty(propertyId!),
|
||||
enabled: !!propertyId,
|
||||
})
|
||||
}
|
||||
@@ -54,3 +54,24 @@ export function useMatchDetail(id: string) {
|
||||
select: (res) => res.data ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
export function useNeedMatchesForProperty(propertyId: string, opts?: { minScore?: number }) {
|
||||
return useQuery({
|
||||
queryKey: ['need-matches-for-property', propertyId, opts ?? {}],
|
||||
queryFn: () => matchService.getNeedMatchesForProperty(propertyId, opts),
|
||||
staleTime: STALE_MATCHES,
|
||||
enabled: Boolean(propertyId),
|
||||
})
|
||||
}
|
||||
|
||||
export function useAdditionalMatchesForInquiry(
|
||||
inquiryId: string | null,
|
||||
opts?: { minScore?: number; excludePropertyId?: string },
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['additional-matches-for-inquiry', inquiryId, opts ?? {}],
|
||||
queryFn: () => matchService.getAdditionalMatchesForInquiry(inquiryId!, opts),
|
||||
staleTime: STALE_MATCHES,
|
||||
enabled: !!inquiryId,
|
||||
})
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { needService } from '../services/needService'
|
||||
import type { CreateNeedInput } from '../domain/need'
|
||||
|
||||
export function useNeeds() {
|
||||
return useQuery({
|
||||
@@ -19,3 +20,13 @@ export function useNeed(id: string) {
|
||||
}
|
||||
|
||||
export const useNeedProfiles = useNeeds
|
||||
|
||||
export function useCreateNeed() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (input: CreateNeedInput) => needService.create(input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['needs'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { offerReportService } from '../services/offerReportService'
|
||||
import type { OfferReportDraft } from '../domain/offerReport'
|
||||
|
||||
export function useOfferReportByInquiry(inquiryId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['offer-report', inquiryId],
|
||||
queryFn: () => offerReportService.getByInquiry(inquiryId!),
|
||||
enabled: !!inquiryId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateOfferReport() {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
inquiryId,
|
||||
propertyId,
|
||||
tenantName,
|
||||
propertyTitle,
|
||||
}: {
|
||||
inquiryId: string
|
||||
propertyId: string
|
||||
tenantName?: string
|
||||
propertyTitle?: string
|
||||
}) => offerReportService.create(inquiryId, propertyId, tenantName, propertyTitle),
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateOfferReport() {
|
||||
return useMutation({
|
||||
mutationFn: ({ draftId, data }: { draftId: string; data: Partial<OfferReportDraft> }) =>
|
||||
offerReportService.update(draftId, data),
|
||||
})
|
||||
}
|
||||
|
||||
export function useGenerateOfferReportPdf() {
|
||||
return useMutation({
|
||||
mutationFn: (draftId: string) => offerReportService.generatePdf(draftId),
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { propertyService } from '../services/propertyService'
|
||||
import { matchService } from '../services/matchService'
|
||||
import { futureSignalService } from '../services/futureSignalService'
|
||||
import type { AssetType, ResultType } from '../domain/enums'
|
||||
import type { CreatePropertyInput, UpdatePropertyInput } from '../domain/property'
|
||||
import { STALE_PROPERTIES, STALE_MATCHES, STALE_SIGNALS } from '../lib/constants'
|
||||
|
||||
interface PropertyFilter {
|
||||
@@ -64,3 +65,35 @@ export function usePropertySignals(propertyId: string | null) {
|
||||
select: (res) => res.data ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateProperty() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, input }: { id: string; input: UpdatePropertyInput }) =>
|
||||
propertyService.update(id, input),
|
||||
onSuccess: (_, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['properties'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['property', id] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateProperty() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (input: CreatePropertyInput) => propertyService.create(input),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['properties'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRemoveProperty() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => propertyService.remove(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['properties'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,6 +75,19 @@ export function useAddReviewNote() {
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateReviewTask() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (signalId: string) => reviewService.createReviewTask(signalId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QK] })
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Prüfungsaufgabe konnte nicht erstellt werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Legacy exports
|
||||
export function useApproveReviewItem() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useMemo } from 'react'
|
||||
import { unitMatchService } from '../services/unitMatchService'
|
||||
import type { Property, PropertyUnit } from '../domain/property'
|
||||
|
||||
/**
|
||||
* Synchronous wrappers — unitMatchService methods are pure synchronous computations.
|
||||
* useMemo ensures we don't recompute on every render unnecessarily.
|
||||
*/
|
||||
|
||||
export function useUnitMatchesMap(freeUnits: PropertyUnit[], property: Property) {
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
freeUnits.map(u => [u.id, unitMatchService.getMatchesForUnit(u, property)]),
|
||||
),
|
||||
[freeUnits, property],
|
||||
)
|
||||
}
|
||||
|
||||
export function useUnitBundle(selectedUnits: PropertyUnit[]) {
|
||||
return useMemo(
|
||||
() => (selectedUnits.length >= 2 ? unitMatchService.buildBundle(selectedUnits) : null),
|
||||
[selectedUnits],
|
||||
)
|
||||
}
|
||||
|
||||
export function useBundleMatches(selectedUnits: PropertyUnit[], property: Property) {
|
||||
return useMemo(
|
||||
() =>
|
||||
selectedUnits.length >= 2
|
||||
? unitMatchService.getMatchesForBundle(selectedUnits, property)
|
||||
: [],
|
||||
[selectedUnits, property],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { weightingService } from '../services/weightingService'
|
||||
import type { WeightingKey } from '../domain/needBuilder'
|
||||
|
||||
/**
|
||||
* Synchronous wrapper — weightingService.getDefaultWeights is a pure synchronous
|
||||
* computation with no async/side-effects. No useQuery needed.
|
||||
*/
|
||||
export function useDefaultWeights(assetType?: string): Record<WeightingKey, number> {
|
||||
return weightingService.getDefaultWeights(assetType)
|
||||
}
|
||||
Reference in New Issue
Block a user