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,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, Navigate } from 'react-router'
|
||||
import { Navigate } from 'react-router'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Building2 } from 'lucide-react'
|
||||
import { authService } from '../../services/authService'
|
||||
import { useLogin, useSwitchDemoRole } from '../../hooks/useAuth'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { UserRole } from '../../domain/enums'
|
||||
|
||||
@@ -23,42 +23,37 @@ const DEMO_ROLES: { role: UserRole; label: string; description: string }[] = [
|
||||
|
||||
export default function LoginScreen() {
|
||||
const { isAuthenticated } = useSessionStore()
|
||||
const navigate = useNavigate()
|
||||
const loginMutation = useLogin()
|
||||
const switchDemoRoleMutation = useSwitchDemoRole()
|
||||
const [email, setEmail] = useState('admin@ideal-sharing.ch')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
async function handleLogin(e: React.FormEvent) {
|
||||
const loading = loginMutation.isPending || switchDemoRoleMutation.isPending
|
||||
|
||||
function handleLogin(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!email) {
|
||||
setError('Bitte E-Mail-Adresse eingeben.')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
await authService.login(email, password)
|
||||
navigate('/')
|
||||
} catch {
|
||||
setError('Anmeldung fehlgeschlagen. Bitte erneut versuchen.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
loginMutation.mutate(
|
||||
{ email, password },
|
||||
{
|
||||
onError: () => {
|
||||
setError('Anmeldung fehlgeschlagen. Bitte erneut versuchen.')
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function handleDemoLogin(role: UserRole) {
|
||||
setLoading(true)
|
||||
try {
|
||||
await authService.switchDemoRole(role)
|
||||
navigate('/')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
function handleDemoLogin(role: UserRole) {
|
||||
switchDemoRoleMutation.mutate(role)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -14,10 +14,9 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ErrorState, LoadingPage, SectionContainer } from '../../components/ui'
|
||||
import { DataQualityBadge, FreshnessIndicator } from '../../components/data-quality'
|
||||
import { propertyService } from '../../services/propertyService'
|
||||
import { useProperties } from '../../hooks/useProperties'
|
||||
import { getRecommendedActions } from '../../services/dataQualityService'
|
||||
import { DataFreshness } from '../../domain/enums'
|
||||
|
||||
@@ -32,16 +31,11 @@ function getQualityColor(score: number): 'success' | 'warning' | 'error' {
|
||||
export default function DataQuality() {
|
||||
const [qualityFilter, setQualityFilter] = useState<QualityFilter>('')
|
||||
|
||||
const { data: resp, isLoading, error } = useQuery({
|
||||
queryKey: ['properties'],
|
||||
queryFn: () => propertyService.getAll(),
|
||||
})
|
||||
const { data: properties = [], isLoading, error } = useProperties()
|
||||
|
||||
if (isLoading) return <LoadingPage />
|
||||
if (error) return <ErrorState />
|
||||
|
||||
const properties = resp?.data ?? []
|
||||
|
||||
const avgScore = properties.length
|
||||
? properties.reduce((sum, p) => sum + p.dataQuality.score, 0) / properties.length
|
||||
: 0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
@@ -17,8 +16,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { useProperties } from '../../hooks/useProperties'
|
||||
import { propertyService } from '../../services/propertyService'
|
||||
import { useProperties, useUpdateProperty, useRemoveProperty } from '../../hooks/useProperties'
|
||||
import type { Property } from '../../domain/property'
|
||||
|
||||
const ASSET_LABELS: Record<string, string> = {
|
||||
@@ -37,41 +35,36 @@ function formatDate(iso?: string) {
|
||||
|
||||
export default function MyListings() {
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
|
||||
const { data: listings = [], isLoading } = useProperties({ sourceType: 'DIRECT' })
|
||||
const updateProperty = useUpdateProperty()
|
||||
const removeProperty = useRemoveProperty()
|
||||
|
||||
const [togglingId, setTogglingId] = useState<string | null>(null)
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null)
|
||||
const [confirmDelete, setConfirmDelete] = useState<Property | null>(null)
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
|
||||
async function handleToggleStatus(p: Property) {
|
||||
setTogglingId(p.id)
|
||||
setActionError(null)
|
||||
try {
|
||||
const next = p.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
|
||||
await propertyService.update(p.id, { status: next })
|
||||
qc.invalidateQueries({ queryKey: ['properties'] })
|
||||
} catch {
|
||||
setActionError('Status konnte nicht geändert werden.')
|
||||
} finally {
|
||||
setTogglingId(null)
|
||||
}
|
||||
function handleToggleStatus(p: Property) {
|
||||
const next = p.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
|
||||
updateProperty.mutate(
|
||||
{ id: p.id, input: { status: next } },
|
||||
{
|
||||
onError: () => {
|
||||
setActionError('Status konnte nicht geändert werden.')
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function handleDelete(p: Property) {
|
||||
setDeletingId(p.id)
|
||||
setActionError(null)
|
||||
try {
|
||||
await propertyService.remove(p.id)
|
||||
qc.invalidateQueries({ queryKey: ['properties'] })
|
||||
} catch {
|
||||
setActionError('Inserat konnte nicht gelöscht werden.')
|
||||
} finally {
|
||||
setDeletingId(null)
|
||||
setConfirmDelete(null)
|
||||
}
|
||||
function handleDelete(p: Property) {
|
||||
removeProperty.mutate(p.id, {
|
||||
onSuccess: () => {
|
||||
setConfirmDelete(null)
|
||||
},
|
||||
onError: () => {
|
||||
setActionError('Inserat konnte nicht gelöscht werden.')
|
||||
setConfirmDelete(null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -184,35 +177,29 @@ export default function MyListings() {
|
||||
|
||||
{/* Status toggle */}
|
||||
<Box>
|
||||
{togglingId === p.id ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<Tooltip title={p.status === 'ACTIVE' ? 'Deaktivieren' : 'Aktivieren'}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={p.status === 'ACTIVE'}
|
||||
onChange={() => handleToggleStatus(p)}
|
||||
sx={{ '& .MuiSwitch-thumb': { width: 14, height: 14 } }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={p.status === 'ACTIVE' ? 'Deaktivieren' : 'Aktivieren'}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={p.status === 'ACTIVE'}
|
||||
onChange={() => handleToggleStatus(p)}
|
||||
disabled={updateProperty.isPending}
|
||||
sx={{ '& .MuiSwitch-thumb': { width: 14, height: 14 } }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Delete */}
|
||||
<Box>
|
||||
{deletingId === p.id ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<Tooltip title="Inserat löschen">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setConfirmDelete(p)}
|
||||
sx={{ color: '#94a3b8', '&:hover': { color: '#ef4444' } }}
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Inserat löschen">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setConfirmDelete(p)}
|
||||
disabled={removeProperty.isPending}
|
||||
sx={{ color: '#94a3b8', '&:hover': { color: '#ef4444' } }}
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
@@ -16,8 +15,8 @@ import {
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { ArrowLeft, CheckCircle, ImagePlus, Sparkles, X } from 'lucide-react'
|
||||
import { propertyService } from '../../services/propertyService'
|
||||
import { parseListingText } from '../../services/aiService'
|
||||
import { useCreateProperty } from '../../hooks/useProperties'
|
||||
import { useParseListingText } from '../../hooks/useAI'
|
||||
import {
|
||||
ASSET_TYPE_LABELS,
|
||||
SOFT_FACTORS,
|
||||
@@ -29,10 +28,12 @@ import { buildCreatePropertyInput } from './newListingMapper'
|
||||
|
||||
export default function NewListing() {
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
const { state } = useLocation() as { state: LocationState | null }
|
||||
const pre = state?.prefill ?? {}
|
||||
|
||||
const createProperty = useCreateProperty()
|
||||
const parseListingMutation = useParseListingText()
|
||||
|
||||
// Core fields
|
||||
const [assetType, setAssetType] = useState(pre.assetType ?? 'OFFICE')
|
||||
const [street, setStreet] = useState(pre.street ?? '')
|
||||
@@ -64,34 +65,33 @@ export default function NewListing() {
|
||||
|
||||
// AI
|
||||
const [aiText, setAiText] = useState('')
|
||||
const [aiParsing, setAiParsing] = useState(false)
|
||||
const [aiApplied, setAiApplied] = useState(false)
|
||||
|
||||
// Submit
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [created, setCreated] = useState(false)
|
||||
|
||||
const aiParsing = parseListingMutation.isPending
|
||||
const submitting = createProperty.isPending
|
||||
|
||||
const isPrefilled = !!pre.propertyId
|
||||
|
||||
async function handleAiParse() {
|
||||
function handleAiParse() {
|
||||
if (!aiText.trim()) return
|
||||
setAiParsing(true)
|
||||
try {
|
||||
const parsed = await parseListingText(aiText)
|
||||
if (parsed.assetType) setAssetType(parsed.assetType)
|
||||
if (parsed.areaSqm) setAreaSqm(String(parsed.areaSqm))
|
||||
if (parsed.rentPerSqm) setRentPerSqm(String(parsed.rentPerSqm))
|
||||
if (parsed.city && !city) setCity(parsed.city)
|
||||
if (parsed.fitOut) setFitOut(parsed.fitOut)
|
||||
if (parsed.parking) setParking(String(parsed.parking))
|
||||
if (parsed.softLevels) {
|
||||
setSoftLevels(prev => ({ ...prev, ...parsed.softLevels }))
|
||||
}
|
||||
setAiApplied(true)
|
||||
} finally {
|
||||
setAiParsing(false)
|
||||
}
|
||||
parseListingMutation.mutate(aiText, {
|
||||
onSuccess: (parsed) => {
|
||||
if (parsed.assetType) setAssetType(parsed.assetType)
|
||||
if (parsed.areaSqm) setAreaSqm(String(parsed.areaSqm))
|
||||
if (parsed.rentPerSqm) setRentPerSqm(String(parsed.rentPerSqm))
|
||||
if (parsed.city && !city) setCity(parsed.city)
|
||||
if (parsed.fitOut) setFitOut(parsed.fitOut)
|
||||
if (parsed.parking) setParking(String(parsed.parking))
|
||||
if (parsed.softLevels) {
|
||||
setSoftLevels(prev => ({ ...prev, ...parsed.softLevels }))
|
||||
}
|
||||
setAiApplied(true)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function addImage() {
|
||||
@@ -111,11 +111,10 @@ export default function NewListing() {
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
function handleSubmit() {
|
||||
const err = validate()
|
||||
if (err) { setError(err); return }
|
||||
setError(null)
|
||||
setSubmitting(true)
|
||||
|
||||
const input = buildCreatePropertyInput({
|
||||
assetType,
|
||||
@@ -135,15 +134,14 @@ export default function NewListing() {
|
||||
images,
|
||||
})
|
||||
|
||||
try {
|
||||
await propertyService.create(input)
|
||||
qc.invalidateQueries({ queryKey: ['properties'] })
|
||||
setCreated(true)
|
||||
} catch {
|
||||
setError('Fehler beim Erstellen des Inserats. Bitte erneut versuchen.')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
createProperty.mutate(input, {
|
||||
onSuccess: () => {
|
||||
setCreated(true)
|
||||
},
|
||||
onError: () => {
|
||||
setError('Fehler beim Erstellen des Inserats. Bitte erneut versuchen.')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
|
||||
Reference in New Issue
Block a user