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
+2 -14
View File
@@ -1,25 +1,13 @@
import { useEffect, useState } from 'react'
import { Alert, Box, CircularProgress, Typography } from '@mui/material'
import { useNavigate } from 'react-router'
import type { PropertyNeedMatch } from '../../domain/match'
import { matchService } from '../../services/matchService'
import { useNeedMatchesForProperty } from '../../hooks/useMatches'
import { useOfferWizardStore } from '../../stores/offerWizardStore'
import { NeedMatchCard } from './NeedMatchCard'
export function MatchabilityTabPanel({ propertyId }: { propertyId: string }) {
const [matches, setMatches] = useState<PropertyNeedMatch[]>([])
const [loading, setLoading] = useState(true)
const navigate = useNavigate()
const setSelectedProperties = useOfferWizardStore(s => s.setSelectedProperties)
useEffect(() => {
let cancelled = false
setLoading(true)
matchService.getNeedMatchesForProperty(propertyId, { minScore: 80 }).then(result => {
if (!cancelled) { setMatches(result); setLoading(false) }
})
return () => { cancelled = true }
}, [propertyId])
const { data: matches = [], isLoading: loading } = useNeedMatchesForProperty(propertyId, { minScore: 80 })
function handleNeedCardClick() {
setSelectedProperties([propertyId])
+15 -16
View File
@@ -1,10 +1,9 @@
import { useState } from 'react'
import { Box, Chip, CircularProgress, Divider, Switch, TextField, Typography } from '@mui/material'
import { Clock, Layers, ShieldCheck, Target, TrendingUp, Users, Zap } from 'lucide-react'
import { useQueryClient } from '@tanstack/react-query'
import type { Property } from '../../domain/property'
import { MockupUnitProvider } from '../../provider/MockupUnitProvider'
import { propertyService } from '../../services/propertyService'
import { useUpdateProperty } from '../../hooks/useProperties'
import { useToastStore } from '../../stores/toastStore'
import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds'
import { floorLabel, SectionTitle } from './PropertyDetailHelpers'
@@ -14,7 +13,6 @@ export const MOCK_TODAY = new Date('2026-05-20')
export function PreMarketPanel({ p }: { p: Property }) {
const [enabled, setEnabled] = useState(p.schattenmarktRelease?.enabled ?? false)
const [leadTimeMonths, setLeadTimeMonths] = useState(p.schattenmarktRelease?.leadTimeMonths ?? 6)
const [saving, setSaving] = useState(false)
const [unitStates, setUnitStates] = useState<Record<string, { enabled: boolean; availableFrom: string }>>(() => {
const init: Record<string, { enabled: boolean; availableFrom: string }> = {}
for (const u of p.units ?? []) {
@@ -26,8 +24,9 @@ export function PreMarketPanel({ p }: { p: Property }) {
return init
})
const [unitSaving, setUnitSaving] = useState<Record<string, boolean>>({})
const queryClient = useQueryClient()
const updateProperty = useUpdateProperty()
const showToast = useToastStore(s => s.showToast)
const saving = updateProperty.isPending
if (p.resultType !== 'VERIFIED_PORTFOLIO') return null
if (!p.leaseEndDate && !p.breakoutOptionDate) return null
@@ -54,18 +53,18 @@ export function PreMarketPanel({ p }: { p: Property }) {
(['Zürich', 'Basel', 'Bern', 'Zug'].some(c => (p.location?.city ?? '').includes(c)) ? 4 : 1))
const highQualityLeads = Math.max(1, Math.floor(demandProfiles * 0.38))
async function save(nextEnabled: boolean, nextLeadTime: number) {
setSaving(true)
try {
await propertyService.update(p.id, { schattenmarktRelease: { enabled: nextEnabled, leadTimeMonths: nextLeadTime } })
await queryClient.invalidateQueries({ queryKey: ['property', p.id] })
await queryClient.invalidateQueries({ queryKey: ['properties'] })
showToast(nextEnabled ? 'Pre-Market Matching aktiviert.' : 'Pre-Market Matching deaktiviert.', 'success')
} catch {
showToast('Fehler beim Speichern.', 'error')
} finally {
setSaving(false)
}
function save(nextEnabled: boolean, nextLeadTime: number) {
updateProperty.mutate(
{ id: p.id, input: { schattenmarktRelease: { enabled: nextEnabled, leadTimeMonths: nextLeadTime } } },
{
onSuccess: () => {
showToast(nextEnabled ? 'Pre-Market Matching aktiviert.' : 'Pre-Market Matching deaktiviert.', 'success')
},
onError: () => {
showToast('Fehler beim Speichern.', 'error')
},
},
)
}
function handleToggle(_: React.ChangeEvent<HTMLInputElement>, checked: boolean) {
+18 -18
View File
@@ -10,12 +10,10 @@ import {
Tabs,
Typography,
} from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import { Edit2, Save, X } from 'lucide-react'
import type { UpdatePropertyInput } from '../../domain/property'
import { usePropertyById } from '../../hooks/useProperties'
import { usePropertyById, useUpdateProperty } from '../../hooks/useProperties'
import { PropertyDetailSkeleton } from './PropertyDetailSkeleton'
import { propertyService } from '../../services/propertyService'
import { useToastStore } from '../../stores/toastStore'
import {
getAssetTypeColor,
@@ -38,30 +36,32 @@ export function PropertyDetailView({ propertyId, onClose }: PropertyDetailViewPr
const [tab, setTab] = useState(0)
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState<UpdatePropertyInput>({})
const [saving, setSaving] = useState(false)
const queryClient = useQueryClient()
const { data: property, isLoading } = usePropertyById(propertyId)
const updateProperty = useUpdateProperty()
const showToast = useToastStore(s => s.showToast)
const saving = updateProperty.isPending
function startEdit() {
setDraft({})
setEditing(true)
}
async function saveEdit() {
function saveEdit() {
if (!property) return
setSaving(true)
try {
await propertyService.update(property.id, draft)
await queryClient.invalidateQueries({ queryKey: ['property', propertyId] })
setEditing(false)
setDraft({})
showToast('Objekt gespeichert.', 'success')
} catch {
showToast('Fehler beim Speichern.', 'error')
} finally {
setSaving(false)
}
updateProperty.mutate(
{ id: property.id, input: draft },
{
onSuccess: () => {
setEditing(false)
setDraft({})
showToast('Objekt gespeichert.', 'success')
},
onError: () => {
showToast('Fehler beim Speichern.', 'error')
},
},
)
}
if (isLoading) return <PropertyDetailSkeleton />
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { Alert, Box, Button, Chip, CircularProgress, Typography } from '@mui/material'
import { BarChart2, Building2, FileText, Lightbulb, TrendingUp } from 'lucide-react'
import { marketReportService } from '../../services/marketReportService'
import type { MarketReport, SignalCategory } from '../../domain/marketReport'
import { useMarketReport } from '../../hooks/useMarketReport'
import type { SignalCategory } from '../../domain/marketReport'
import { SignalCard } from './SignalCard'
import { BerichtDialog } from './BerichtDialog'
@@ -18,19 +18,9 @@ const SECTION_CONFIG: { category: SignalCategory; label: string; icon: React.Rea
]
export function PropertyMarketSignalsTab({ propertyId }: Props) {
const [report, setReport] = useState<MarketReport | null>(null)
const [loading, setLoading] = useState(true)
const { data: report = null, isLoading: loading } = useMarketReport(propertyId)
const [dialogOpen, setDialogOpen] = useState(false)
useEffect(() => {
let cancelled = false
setLoading(true)
marketReportService.getByProperty(propertyId).then(r => {
if (!cancelled) { setReport(r); setLoading(false) }
})
return () => { cancelled = true }
}, [propertyId])
if (loading) {
return (
<Box sx={{ p: 3, display: 'flex', justifyContent: 'center' }}>
+4 -10
View File
@@ -3,7 +3,7 @@ import { Box, Button, Checkbox, Chip, Collapse, Divider, IconButton, Tooltip, Ty
import { ChevronDown, ChevronUp, Layers, Users } from 'lucide-react'
import { useNavigate } from 'react-router'
import type { Property, PropertyUnit, UnitNeedMatch } from '../../domain/property'
import { unitMatchService } from '../../services/unitMatchService'
import { useUnitMatchesMap, useUnitBundle, useBundleMatches } from '../../hooks/useUnitMatches'
import { DS_BORDER, DS_PRE_MARKET, DS_SURFACE, DS_TEXT } from '../../lib/ds'
import { floorLabel } from './PropertyDetailHelpers'
@@ -30,17 +30,11 @@ export function UnitStructurePanel({ p }: { p: Property }) {
const freeUnits = useMemo(() => (p.units ?? []).filter(u => u.available), [p.units])
const unitMatches = useMemo(() =>
Object.fromEntries(freeUnits.map(u => [u.id, unitMatchService.getMatchesForUnit(u, p)])),
[freeUnits, p],
)
const unitMatches = useUnitMatchesMap(freeUnits, p)
const selectedFreeUnits = freeUnits.filter(u => selectedIds.has(u.id))
const bundle = selectedFreeUnits.length >= 2 ? unitMatchService.buildBundle(selectedFreeUnits) : null
const bundleMatches = useMemo(() =>
selectedFreeUnits.length >= 2 ? unitMatchService.getMatchesForBundle(selectedFreeUnits, p) : [],
[selectedFreeUnits, p],
)
const bundle = useUnitBundle(selectedFreeUnits)
const bundleMatches = useBundleMatches(selectedFreeUnits, p)
const toggleUnit = (id: string) => {
setSelectedIds(prev => {