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:
@@ -1,7 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, Button, CircularProgress, Typography } from '@mui/material'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { aiService } from '../../services/aiService'
|
||||
import { useGenerateOfferEmail } from '../../hooks/useAI'
|
||||
|
||||
interface AiOfferEmailButtonProps {
|
||||
needTitle: string
|
||||
@@ -16,33 +15,26 @@ export function AiOfferEmailButton({
|
||||
matchScores,
|
||||
onGenerated,
|
||||
}: AiOfferEmailButtonProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const generateOfferEmail = useGenerateOfferEmail()
|
||||
|
||||
const handleClick = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await aiService.generateOfferEmail({
|
||||
needTitle,
|
||||
properties: selectedProperties,
|
||||
matchScores,
|
||||
})
|
||||
onGenerated(res.data.subject, res.data.body)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'KI-Generierung fehlgeschlagen')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
const handleClick = () => {
|
||||
generateOfferEmail.mutate(
|
||||
{ needTitle, properties: selectedProperties, matchScores },
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
onGenerated(res.data.subject, res.data.body)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={loading ? <CircularProgress size={14} /> : <Sparkles size={14} />}
|
||||
startIcon={generateOfferEmail.isPending ? <CircularProgress size={14} /> : <Sparkles size={14} />}
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
disabled={generateOfferEmail.isPending}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
color: '#7c3aed',
|
||||
@@ -53,9 +45,11 @@ export function AiOfferEmailButton({
|
||||
>
|
||||
KI-Mail generieren
|
||||
</Button>
|
||||
{error && (
|
||||
{generateOfferEmail.isError && (
|
||||
<Typography variant="caption" sx={{ color: 'error.main', display: 'block', mt: 0.5 }}>
|
||||
{error}
|
||||
{generateOfferEmail.error instanceof Error
|
||||
? generateOfferEmail.error.message
|
||||
: 'KI-Generierung fehlgeschlagen'}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@mui/material'
|
||||
import { Plus, Trash2, X } from 'lucide-react'
|
||||
import type { OfferReportDraft, ViewingAppointmentOption } from '../../domain/offerReport'
|
||||
import { offerReportService } from '../../services/offerReportService'
|
||||
import { useCreateOfferReport, useUpdateOfferReport, useGenerateOfferReportPdf } from '../../hooks/useOfferReport'
|
||||
import { usePropertyById } from '../../hooks/useProperties'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { OfferWizardPdfStep } from './OfferWizardPdfStep'
|
||||
@@ -32,10 +32,16 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
|
||||
const showToast = useToastStore(s => s.showToast)
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const createOfferReport = useCreateOfferReport()
|
||||
const updateOfferReport = useUpdateOfferReport()
|
||||
const generatePdf = useGenerateOfferReportPdf()
|
||||
|
||||
useEffect(() => {
|
||||
offerReportService
|
||||
.create(inquiryId, propertyId, tenantName, property?.title ?? '')
|
||||
.then(d => { setDraft(d); setLoading(false) })
|
||||
createOfferReport.mutate(
|
||||
{ inquiryId, propertyId, tenantName, propertyTitle: property?.title ?? '' },
|
||||
{ onSuccess: (d) => { setDraft(d); setLoading(false) } },
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inquiryId, propertyId, tenantName, property?.title])
|
||||
|
||||
useEffect(() => () => { if (timerRef.current) clearInterval(timerRef.current) }, [])
|
||||
@@ -70,28 +76,31 @@ export function OfferCreationWizard({ inquiryId, propertyId, tenantName, onClose
|
||||
})
|
||||
}
|
||||
|
||||
const handleGeneratePdf = async () => {
|
||||
const handleGeneratePdf = () => {
|
||||
if (!draft) return
|
||||
await offerReportService.update(draft.id, {
|
||||
editableFields: draft.editableFields,
|
||||
viewingAppointments: draft.viewingAppointments,
|
||||
})
|
||||
setStep(3)
|
||||
setGenerating(true)
|
||||
setProgress(0)
|
||||
let p = 0
|
||||
timerRef.current = setInterval(() => {
|
||||
p += Math.random() * 18 + 8
|
||||
if (p >= 100) {
|
||||
clearInterval(timerRef.current!)
|
||||
setProgress(100)
|
||||
setGenerating(false)
|
||||
setReady(true)
|
||||
offerReportService.generatePdf(draft.id).then(setDraft)
|
||||
} else {
|
||||
setProgress(Math.min(100, p))
|
||||
}
|
||||
}, 200)
|
||||
updateOfferReport.mutate(
|
||||
{ draftId: draft.id, data: { editableFields: draft.editableFields, viewingAppointments: draft.viewingAppointments } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setStep(3)
|
||||
setGenerating(true)
|
||||
setProgress(0)
|
||||
let p = 0
|
||||
timerRef.current = setInterval(() => {
|
||||
p += Math.random() * 18 + 8
|
||||
if (p >= 100) {
|
||||
clearInterval(timerRef.current!)
|
||||
setProgress(100)
|
||||
setGenerating(false)
|
||||
setReady(true)
|
||||
generatePdf.mutate(draft.id, { onSuccess: setDraft })
|
||||
} else {
|
||||
setProgress(Math.min(100, p))
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Download, Send, X } from 'lucide-react'
|
||||
import type { Inquiry } from '../../domain/inquiry'
|
||||
import type { Property } from '../../domain/property'
|
||||
import type { InquiryPreparationReportDraft, ReportObjectFieldSelection } from '../../domain/inquiryReport'
|
||||
import { inquiryReportService } from '../../services/inquiryReportService'
|
||||
import { useCreateInquiryReport, useUpdateInquiryReport, useFinalizeInquiryReport } from '../../hooks/useInquiryReport'
|
||||
import { useToastStore } from '../../stores/toastStore'
|
||||
import { ReportObjectFieldSelector } from './ReportObjectFieldSelector'
|
||||
import { LatentInquiryReportPreview } from './LatentInquiryReportPreview'
|
||||
@@ -29,11 +29,15 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
const [draft, setDraft] = useState<InquiryPreparationReportDraft | null>(null)
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [finalizing, setFinalizing] = useState(false)
|
||||
const { data: allProperties = [] } = useProperties()
|
||||
const showToast = useToastStore(s => s.showToast)
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const createInquiryReport = useCreateInquiryReport()
|
||||
const updateInquiryReport = useUpdateInquiryReport()
|
||||
const finalizeInquiryReport = useFinalizeInquiryReport()
|
||||
const finalizing = finalizeInquiryReport.isPending
|
||||
|
||||
const portfolioProps = allProperties.filter(p => p.resultType === ResultType.VERIFIED_PORTFOLIO)
|
||||
|
||||
const selectedProperties = draft?.selectedPropertyIds
|
||||
@@ -46,7 +50,7 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const handleGenerate = () => {
|
||||
setStep(1)
|
||||
setGenerating(true)
|
||||
setProgress(0)
|
||||
@@ -57,10 +61,15 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
clearInterval(timerRef.current!)
|
||||
setProgress(100)
|
||||
setGenerating(false)
|
||||
inquiryReportService.create(inquiryId, selectedIds).then(d => {
|
||||
setDraft(d)
|
||||
setStep(2)
|
||||
})
|
||||
createInquiryReport.mutate(
|
||||
{ inquiryId, selectedPropertyIds: selectedIds },
|
||||
{
|
||||
onSuccess: (d) => {
|
||||
setDraft(d)
|
||||
setStep(2)
|
||||
},
|
||||
},
|
||||
)
|
||||
} else {
|
||||
setProgress(Math.min(100, p))
|
||||
}
|
||||
@@ -85,14 +94,21 @@ export function PreparationWizard({ inquiryId, inquiry, onClose }: Props) {
|
||||
})
|
||||
}
|
||||
|
||||
const handleFinalize = async () => {
|
||||
const handleFinalize = () => {
|
||||
if (!draft) return
|
||||
setFinalizing(true)
|
||||
await inquiryReportService.update(draft.id, { editableFields: draft.editableFields, fieldSelections: draft.fieldSelections })
|
||||
const finalized = await inquiryReportService.finalize(draft.id)
|
||||
setDraft(finalized)
|
||||
setFinalizing(false)
|
||||
setStep(3)
|
||||
updateInquiryReport.mutate(
|
||||
{ draftId: draft.id, data: { editableFields: draft.editableFields, fieldSelections: draft.fieldSelections } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
finalizeInquiryReport.mutate(draft.id, {
|
||||
onSuccess: (finalized) => {
|
||||
setDraft(finalized)
|
||||
setStep(3)
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box, Button, CircularProgress, Divider, Typography } from '@mui/material'
|
||||
import { ArrowRight, Building2, Calendar, MapPin, Ruler, Trophy } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { usePropertyById } from '../../hooks/useProperties'
|
||||
import { matchService } from '../../services/matchService'
|
||||
import { useAdditionalMatchesForInquiry } from '../../hooks/useMatches'
|
||||
import type { AdditionalPropertyMatch } from '../../domain/additionalMatch'
|
||||
|
||||
interface RelatedPropertyCardPanelProps {
|
||||
@@ -14,16 +13,10 @@ interface RelatedPropertyCardPanelProps {
|
||||
export function RelatedPropertyCardPanel({ propertyId, inquiryId }: RelatedPropertyCardPanelProps) {
|
||||
const { data: property, isLoading } = usePropertyById(propertyId)
|
||||
const navigate = useNavigate()
|
||||
const [additionalMatches, setAdditionalMatches] = useState<AdditionalPropertyMatch[]>([])
|
||||
const [loadingMatches, setLoadingMatches] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingMatches(true)
|
||||
matchService
|
||||
.getAdditionalMatchesForInquiry(inquiryId, { minScore: 80, excludePropertyId: propertyId })
|
||||
.then(setAdditionalMatches)
|
||||
.finally(() => setLoadingMatches(false))
|
||||
}, [inquiryId, propertyId])
|
||||
const {
|
||||
data: additionalMatches = [],
|
||||
isLoading: loadingMatches,
|
||||
} = useAdditionalMatchesForInquiry(inquiryId, { minScore: 80, excludePropertyId: propertyId })
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user