feat(agenten): Runde 4 — «Meine Agenten» mit fünf digitalen Mitarbeitenden
Setzt das Umsetzungsbriefing Runde 4 im bestehenden Frontend um. Informationsarchitektur - «Teamübersicht» heisst «Meine Agenten»; ihr bisheriger Inhalt (Kreisgrafik, Auswertung, Verbindungszusammenfassung) ist entfallen. - Personalverwaltung, Bearbeitungsverlauf und Kanäle & Systeme sind jetzt Reiter derselben Seite (?section=), gestaltet wie die Auswahl im Bearbeitungsverlauf. Es wird nur der gewählte Bereich gerendert. - Die alten Unterseitenpfade leiten um, damit Lesezeichen nicht brechen. Agentenbestand - Reto und Lea vollständig entfernt — aus Navigation, Dossiers, Protokoll, Verbindungen, Vorgängen und Porträtbestand. - Retos Aufgaben liegen bei Bruno: Nachbereitung, WhatsApp-Anruf, Protokoll und Kundennotiz, Ablage im CRM. - Livia ist «Exposé Master»: Lageberichte, Inserate, Angebotsbroschüren. - Sidebar führt Ferdi, Bruno, Livia, Nora, Sina mit Porträt und Funktion. Agentenseiten - Ein gemeinsamer AgentWorkspaceHero auf allen fünf Seiten. - Ferdi: Auswertungskarten, Priorität und Typfarben entfallen; Fälligkeit nur bei fünf Tagen oder weniger rot; Objektlinks nach «Meine Objekte»; neu die Terminplanung im verbundenen Kalender mit typgerechtem PDF-Ausschnitt. - Sina: reduzierte, filter- und sortierbare Objektübersicht; Detailansicht direkt editierbar, leere Pflichtfelder rot umrandet. - Nora: Signale ohne Prozentsätze und Konfidenzstufen, nur belegbare Angaben; Mehrfachauswahl leitet Objekte an Livia weiter. - Livia: aktive und archivierte Leads, Arbeitsbereich gleitet an den oberen Rand; dreistufiger Exposé-Prozess Hochladen → Exposé → Export. - Bruno: neue Seite mit Auftragsliste und Vor-/Nachbereitungs-Drawer; Glocke warnt bei Besichtigung unter 24 Stunden ohne Bericht. Datenschicht - Neu: Kalender, Exposé-Leads, Exposé-Entwürfe, Besichtigungsaufträge — je Domain, Provider, Service und Hook. - IAIService um generateExposeText erweitert; der Entwurf nutzt ausschliesslich erfasste Objektdaten und meldet Lücken, statt sie zu füllen. - Alle Objektverweise zeigen auf reale Einträge aus «Meine Objekte»; neue Detailroute /supply/properties/:propertyId. Offen: Chat, Kalender, CRM und DMS sind Frontend-Simulation ohne Anbindung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cc5582b7fd
commit
d200d4e930
352 files changed
+8069
-8512
No files matched your search
@@ -1,41 +0,0 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { aiMonitoringService } from '../services/aiMonitoringService'
|
||||
import type { AIMonitoringFilters } from '../provider/IAIMonitoringProvider'
|
||||
import type { ReviewStatus } from '../domain/enums'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
|
||||
const QK = 'aiOutputs'
|
||||
const STALE = 30_000
|
||||
|
||||
export function useAIOutputs(filters?: AIMonitoringFilters) {
|
||||
return useQuery({
|
||||
queryKey: [QK, filters ?? {}],
|
||||
queryFn: () => aiMonitoringService.getOutputs(filters),
|
||||
staleTime: STALE,
|
||||
select: (res) => res.data ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
export function useAIOutput(id: string | null) {
|
||||
return useQuery({
|
||||
queryKey: [QK, 'detail', id],
|
||||
queryFn: () => aiMonitoringService.getOutput(id!),
|
||||
staleTime: STALE,
|
||||
enabled: !!id,
|
||||
select: (res) => res.data ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateAIOutputReviewStatus() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: ReviewStatus }) =>
|
||||
aiMonitoringService.updateReviewStatus(id, status),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: [QK] })
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Status konnte nicht aktualisiert werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { agentDirectoryService } from '../services/agentDirectoryService'
|
||||
import type { AgentDirectoryEntry } from '../domain/agentDirectory'
|
||||
import { STALE_TEAM_AGENTS } from '../lib/constants'
|
||||
|
||||
/**
|
||||
* Stammdaten der digitalen Belegschaft.
|
||||
*
|
||||
* Das Verzeichnis war die letzte Datenquelle ohne Provider — die Porträtgruppe
|
||||
* las es direkt aus den Mockdaten. Jetzt läuft es über dieselbe Schichtung wie
|
||||
* alles andere und lässt sich später gegen ein echtes Verzeichnis tauschen.
|
||||
*/
|
||||
export function useAgentDirectory() {
|
||||
return useQuery({
|
||||
queryKey: ['agent-directory'],
|
||||
queryFn: async () => (await agentDirectoryService.getAll()).data,
|
||||
staleTime: STALE_TEAM_AGENTS,
|
||||
})
|
||||
}
|
||||
|
||||
/** Nachschlag anhand von IDs, in der Reihenfolge der Anfrage. */
|
||||
export function useAgentDirectoryLookup() {
|
||||
const { data: entries = [] } = useAgentDirectory()
|
||||
const byId = useMemo(() => new Map(entries.map(e => [e.id, e])), [entries])
|
||||
|
||||
return useCallback(
|
||||
(ids: string[]): AgentDirectoryEntry[] =>
|
||||
ids.map(id => byId.get(id)).filter((e): e is AgentDirectoryEntry => Boolean(e)),
|
||||
[byId],
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,24 @@
|
||||
import { useContext } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { authService } from '../services/authService'
|
||||
import { AuthContext } from '../provider/authContext'
|
||||
import type { AuthContextValue } from '../provider/authContext'
|
||||
import type { UserRole } from '../domain/enums'
|
||||
|
||||
/**
|
||||
* Zugriff auf die angemeldete Person.
|
||||
*
|
||||
* Lag bis zum Aufräumen neben der Provider-Komponente — ein Hook in einer
|
||||
* Komponentendatei kostet dort den Fast-Refresh-Zustand und steht ausserdem
|
||||
* nicht dort, wo man Hooks sucht.
|
||||
*/
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext)
|
||||
if (!ctx) throw new Error('useAuth muss innerhalb von AuthProvider verwendet werden.')
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function useLogin() {
|
||||
const navigate = useNavigate()
|
||||
return useMutation({
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { calendarService } from '../services/calendarService'
|
||||
import type { CreateCalendarEventInput } from '../domain/calendarEvent'
|
||||
import { STALE_CALENDAR } from '../lib/constants'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
|
||||
export function useCalendarStatus() {
|
||||
return useQuery({
|
||||
queryKey: ['calendar', 'status'],
|
||||
queryFn: async () => (await calendarService.getStatus()).data,
|
||||
staleTime: STALE_CALENDAR,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCalendarEventsByReminder(reminderId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['calendar', 'reminder', reminderId],
|
||||
queryFn: async () => (await calendarService.getByReminder(reminderId)).data,
|
||||
enabled: !!reminderId,
|
||||
staleTime: STALE_CALENDAR,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateCalendarEvent() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateCalendarEventInput) => (await calendarService.create(input)).data,
|
||||
onSuccess: (event) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['calendar'] })
|
||||
if (event.reminderId) {
|
||||
queryClient.invalidateQueries({ queryKey: ['calendar', 'reminder', event.reminderId] })
|
||||
}
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
useToastStore.getState().showToast(err.message || 'Termin konnte nicht eingeplant werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { UnifiedMatchResult } from '../domain/unifiedResult'
|
||||
import { WEIGHTING_KEYS, WEIGHTING_LABELS } from '../domain/needBuilder'
|
||||
import type { WeightingKey } from '../domain/needBuilder'
|
||||
import { aiService } from '../services/aiService'
|
||||
import { needService } from '../services/needService'
|
||||
import { CRITERION_ALIASES, getProp } from '../components/compare/compareUtils'
|
||||
|
||||
export function useCompareData(compareItems: UnifiedMatchResult[]) {
|
||||
const { data: aiSummary, isLoading: aiLoading } = useQuery({
|
||||
queryKey: ['ai-compare', compareItems.map(i => i.matchId)],
|
||||
queryFn: () => aiService.summarizeComparison(compareItems),
|
||||
enabled: compareItems.length >= 2,
|
||||
select: r => r.data,
|
||||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
const { data: needsData } = useQuery({
|
||||
queryKey: ['needs'],
|
||||
queryFn: () => needService.getAll(),
|
||||
select: r => r.data,
|
||||
})
|
||||
|
||||
const activeNeed = needsData
|
||||
? [...needsData].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0]
|
||||
: undefined
|
||||
|
||||
const relevantCriteria = activeNeed
|
||||
? WEIGHTING_KEYS
|
||||
.map(key => ({ key, label: WEIGHTING_LABELS[key], weight: activeNeed.weightingProfile[key] ?? 0 }))
|
||||
.filter(c => c.weight > 0)
|
||||
.sort((a, b) => b.weight - a.weight)
|
||||
: []
|
||||
|
||||
const weightedTotals = compareItems.map(item =>
|
||||
relevantCriteria.reduce((sum, { key, weight }) => {
|
||||
const factor = [...item.match.positiveFactors, ...item.match.negativeFactors]
|
||||
.find(f => CRITERION_ALIASES[key as WeightingKey].some(a => a.toLowerCase() === f.criterion.toLowerCase()))
|
||||
return sum + (factor?.score ?? 50) * weight
|
||||
}, 0)
|
||||
)
|
||||
const maxWeightedTotal = Math.max(...weightedTotals)
|
||||
const overallWinnerIdx = compareItems.length > 1 && weightedTotals.filter(t => t === maxWeightedTotal).length === 1
|
||||
? weightedTotals.indexOf(maxWeightedTotal)
|
||||
: -1
|
||||
|
||||
const bestScoreIdx = compareItems.length > 0
|
||||
? compareItems.reduce(
|
||||
(best, item, i) => item.matchScore > compareItems[best].matchScore ? i : best, 0
|
||||
)
|
||||
: -1
|
||||
|
||||
const worstConfIdx = compareItems.length > 0
|
||||
? compareItems.reduce(
|
||||
(worst, item, i) => item.match.confidenceLevel < compareItems[worst].match.confidenceLevel ? i : worst, 0
|
||||
)
|
||||
: -1
|
||||
|
||||
const dqScores = compareItems.map(item => getProp(item)?.dataQuality.score ?? 1)
|
||||
const worstDQIdx = dqScores.length > 0 ? dqScores.indexOf(Math.min(...dqScores)) : -1
|
||||
|
||||
const missingCriticalCounts = compareItems.map(
|
||||
item => item.match.missingData?.filter(m => m.importance === 'CRITICAL').length ?? 0
|
||||
)
|
||||
const maxMissingCritical = missingCriticalCounts.length > 0 ? Math.max(...missingCriticalCounts) : 0
|
||||
|
||||
return {
|
||||
aiSummary,
|
||||
aiLoading,
|
||||
activeNeed,
|
||||
relevantCriteria,
|
||||
weightedTotals,
|
||||
overallWinnerIdx,
|
||||
bestScoreIdx,
|
||||
worstConfIdx,
|
||||
dqScores,
|
||||
worstDQIdx,
|
||||
missingCriticalCounts,
|
||||
maxMissingCritical,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { exposeService } from '../services/exposeService'
|
||||
import { aiService } from '../services/aiService'
|
||||
import type { ExposeDraft } from '../domain/expose'
|
||||
import type { Property } from '../domain/property'
|
||||
import type { ExposeTextInput } from '../services/ai/IAIService'
|
||||
import { STALE_EXPOSE_LEADS } from '../lib/constants'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
|
||||
export function useExposeDraft(leadId: string, property: Property | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ['expose-draft', leadId, property?.id],
|
||||
queryFn: async () => (await exposeService.getOrCreateDraft(leadId, property!)).data,
|
||||
enabled: !!leadId && !!property,
|
||||
staleTime: STALE_EXPOSE_LEADS,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSaveExposeDraft() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (draft: ExposeDraft) => (await exposeService.save(draft)).data,
|
||||
onSuccess: (draft) => {
|
||||
queryClient.setQueryData(['expose-draft', draft.leadId, draft.propertyId], draft)
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Exposé konnte nicht gespeichert werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useGenerateExpose() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (draft: ExposeDraft) => (await exposeService.generate(draft)).data,
|
||||
onSuccess: (draft) => {
|
||||
queryClient.setQueryData(['expose-draft', draft.leadId, draft.propertyId], draft)
|
||||
useToastStore.getState().showToast('Exposé erstellt — bereit für den Export.', 'success')
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
useToastStore.getState().showToast(err.message || 'Exposé konnte nicht erstellt werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* KI-Textentwurf für einen Abschnitt.
|
||||
*
|
||||
* Fehlen belegte Angaben, liefert der Dienst keinen Text, sondern die Liste der
|
||||
* Lücken — die Oberfläche sagt das dann so, statt einen Text zu zeigen, der
|
||||
* Angaben behauptet, die niemand geprüft hat (§8.5.2, §9.6).
|
||||
*/
|
||||
export function useGenerateExposeText() {
|
||||
return useMutation({
|
||||
mutationFn: async (input: ExposeTextInput) => (await aiService.generateExposeText(input)).data,
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Textentwurf konnte nicht erzeugt werden — bitte manuell erfassen.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { exposeLeadService } from '../services/exposeLeadService'
|
||||
import type { CreateExposeLeadInput } from '../domain/exposeLead'
|
||||
import { STALE_EXPOSE_LEADS } from '../lib/constants'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
|
||||
export function useExposeLeads() {
|
||||
return useQuery({
|
||||
queryKey: ['expose-leads'],
|
||||
queryFn: async () => (await exposeLeadService.getAll()).data,
|
||||
staleTime: STALE_EXPOSE_LEADS,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateExposeLead() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateExposeLeadInput) => (await exposeLeadService.create(input)).data,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['expose-leads'] })
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
useToastStore.getState().showToast(err.message || 'Lead konnte nicht weitergeleitet werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useArchiveExposeLead() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => (await exposeLeadService.archive(id)).data,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['expose-leads'] })
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Lead konnte nicht archiviert werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMatchDetail } from './useMatches'
|
||||
import { propertyService } from '../services/propertyService'
|
||||
import { needService } from '../services/needService'
|
||||
import { futureSignalService } from '../services/futureSignalService'
|
||||
import type { ItemResponse } from '../services/types'
|
||||
import type { Property } from '../domain/property'
|
||||
import type { Need } from '../domain/need'
|
||||
import type { FutureSignal } from '../domain/futureSignal'
|
||||
|
||||
type Match = NonNullable<ReturnType<typeof useMatchDetail>['data']>
|
||||
|
||||
export function useMatchDetailData(matchId: string) {
|
||||
const { data: match, isLoading } = useMatchDetail(matchId)
|
||||
const isFuture = match?.resultType === 'FUTURE_AVAILABILITY'
|
||||
|
||||
const { data: property = null } = useQuery<ItemResponse<Property | null>, Error, Property | null>({
|
||||
queryKey: ['property', match?.propertyId],
|
||||
queryFn: () => propertyService.getById(match!.propertyId),
|
||||
enabled: !!match && !isFuture,
|
||||
select: r => r.data ?? null,
|
||||
})
|
||||
|
||||
const { data: need = null } = useQuery<ItemResponse<Need | null>, Error, Need | null>({
|
||||
queryKey: ['need', match?.needId],
|
||||
queryFn: () => needService.getById(match!.needId),
|
||||
enabled: !!match?.needId,
|
||||
select: r => r.data ?? null,
|
||||
})
|
||||
|
||||
const { data: signal = null } = useQuery<FutureSignal | null>({
|
||||
queryKey: ['signal', match?.resultId],
|
||||
queryFn: async () => {
|
||||
// resultId may be a signal ID ('signal-002') or a property ID ('prop-006')
|
||||
const byId = await futureSignalService.getById(match!.resultId!)
|
||||
if (byId.data) return byId.data
|
||||
const byProp = await futureSignalService.getByProperty(match!.resultId!)
|
||||
return byProp.data[0] ?? null
|
||||
},
|
||||
enabled: !!match && isFuture && !!match.resultId,
|
||||
})
|
||||
|
||||
return {
|
||||
match: match as Match | null | undefined,
|
||||
property,
|
||||
need,
|
||||
signal,
|
||||
isLoading,
|
||||
isFuture,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useProperties } from './useProperties'
|
||||
import type { Property } from '../domain/property'
|
||||
|
||||
/**
|
||||
* Nachschlagen von Objekten anhand ihrer ID.
|
||||
*
|
||||
* Mehrere Komponenten haben dafür bis zum Aufräumen direkt `mock-data/properties`
|
||||
* importiert und damit Provider, Service und Hook übersprungen — beim Wechsel
|
||||
* auf ein echtes Backend hätten genau diese Stellen weiter Demodaten angezeigt.
|
||||
* Der Hook liest aus derselben Abfrage wie alle anderen Objektansichten.
|
||||
*/
|
||||
export function usePropertyLookup() {
|
||||
const { data: properties = [] } = useProperties()
|
||||
|
||||
const byId = useMemo(() => new Map(properties.map(p => [p.id, p])), [properties])
|
||||
|
||||
const findProperty = useCallback(
|
||||
(id: string): Property | undefined => byId.get(id),
|
||||
[byId],
|
||||
)
|
||||
|
||||
/** Objektname, oder die ID selbst, wenn kein Objekt dazu existiert. */
|
||||
const propertyTitle = useCallback(
|
||||
(id: string): string => byId.get(id)?.title ?? id,
|
||||
[byId],
|
||||
)
|
||||
|
||||
const findProperties = useCallback(
|
||||
(ids: string[]): Property[] => ids.map(id => byId.get(id)).filter((p): p is Property => Boolean(p)),
|
||||
[byId],
|
||||
)
|
||||
|
||||
return { findProperty, findProperties, propertyTitle }
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import { useMemo } from 'react'
|
||||
import type { Property, PropertyUnit } from '../domain/property'
|
||||
import type { FutureSignal } from '../domain/futureSignal'
|
||||
import { SignalType, RiskLevel, ResultType } from '../domain/enums'
|
||||
import { mockToday } from '../lib/constants'
|
||||
|
||||
// Matches the mock date used throughout the prototype (currentDate context: 2026-05-20)
|
||||
const MOCK_TODAY = new Date('2026-05-20')
|
||||
const MOCK_TODAY = mockToday()
|
||||
|
||||
export function useSchattenmarktSignals(properties: Property[]): FutureSignal[] {
|
||||
return useMemo(() => {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { teamDemoService } from '../services/teamDemoService'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
import { QK_TEAM_AGENTS } from './useTeamAgents'
|
||||
import { QK_AGENT_WORK_ITEMS, QK_AGENT_KPIS } from './useAgentWorkItems'
|
||||
import { QK_AGENT_PROTOCOL } from './useAgentProtocol'
|
||||
import { QK_AGENT_CONNECTIONS } from './useAgentConnections'
|
||||
|
||||
/**
|
||||
* «Demo zurücksetzen». Entwertet alle vier Property-On-Bestände, damit die
|
||||
* Oberfläche unmittelbar den Auslieferungszustand zeigt — ein Reload ist nicht
|
||||
* nötig.
|
||||
*/
|
||||
export function useResetTeamDemo() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: () => teamDemoService.reset(),
|
||||
onSuccess: () => {
|
||||
for (const key of [QK_TEAM_AGENTS, QK_AGENT_WORK_ITEMS, QK_AGENT_KPIS, QK_AGENT_PROTOCOL, QK_AGENT_CONNECTIONS]) {
|
||||
queryClient.invalidateQueries({ queryKey: [key] })
|
||||
}
|
||||
useToastStore.getState().showToast('Demo zurückgesetzt. Alle lokalen Änderungen wurden verworfen.', 'success')
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Demo konnte nicht zurückgesetzt werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,20 @@ import type {
|
||||
UnifiedMatchResult,
|
||||
VerifiedPortfolioResult,
|
||||
} from '../domain/unifiedResult'
|
||||
import type { Match } from '../domain/match'
|
||||
import type { Property } from '../domain/property'
|
||||
import type { FutureSignal } from '../domain/futureSignal'
|
||||
|
||||
/**
|
||||
* Stabile Leerwerte.
|
||||
*
|
||||
* `query.data ?? []` erzeugt bei jedem Rendern ein neues Array. Jedes `useMemo`,
|
||||
* das darauf hört, rechnet dann bei jedem Rendern neu — der Zwischenspeicher
|
||||
* war wirkungslos, ohne dass man es sah.
|
||||
*/
|
||||
const EMPTY_MATCHES: Match[] = []
|
||||
const EMPTY_PROPERTIES: Property[] = []
|
||||
const EMPTY_SIGNALS: FutureSignal[] = []
|
||||
|
||||
export function useUnifiedResults(needId?: string) {
|
||||
const needMatchesQuery = useMatchesByNeed(needId ?? '')
|
||||
@@ -20,9 +34,9 @@ export function useUnifiedResults(needId?: string) {
|
||||
matchesQuery.isLoading || propertiesQuery.isLoading || signalsQuery.isLoading
|
||||
const error = matchesQuery.error ?? propertiesQuery.error ?? signalsQuery.error
|
||||
|
||||
const matches = matchesQuery.data ?? []
|
||||
const properties = propertiesQuery.data ?? []
|
||||
const signals = signalsQuery.data ?? []
|
||||
const matches = matchesQuery.data ?? EMPTY_MATCHES
|
||||
const properties = propertiesQuery.data ?? EMPTY_PROPERTIES
|
||||
const signals = signalsQuery.data ?? EMPTY_SIGNALS
|
||||
|
||||
const schattenmarktSignals = useSchattenmarktSignals(properties)
|
||||
const allSignals = useMemo(() => [...signals, ...schattenmarktSignals], [signals, schattenmarktSignals])
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { unitService } from '../services/unitService'
|
||||
import type { PropertyUnit } from '../domain/property'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
|
||||
/**
|
||||
* Änderung an einer Mieteinheit.
|
||||
*
|
||||
* Die Pre-Market-Freigabe griff bis zum Aufräumen direkt auf
|
||||
* `MockupUnitProvider` zu und übersprang damit Service und Hook. Beim Wechsel
|
||||
* auf ein echtes Backend wäre genau diese Stelle stehen geblieben — sie hätte
|
||||
* weiter in den lokalen Mockup geschrieben, während alles andere schon am
|
||||
* Server hing.
|
||||
*/
|
||||
interface UpdateUnitInput {
|
||||
unitId: string
|
||||
data: Partial<PropertyUnit>
|
||||
/** Objekt, dessen Zwischenspeicher mit erneuert wird. */
|
||||
propertyId?: string
|
||||
}
|
||||
|
||||
export function useUpdateUnit() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async ({ unitId, data }: UpdateUnitInput) =>
|
||||
(await unitService.update(unitId, data)).data,
|
||||
onSuccess: (_unit, { propertyId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['units'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['properties'] })
|
||||
if (propertyId) queryClient.invalidateQueries({ queryKey: ['property', propertyId] })
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Einheit konnte nicht gespeichert werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { visitAssignmentService } from '../services/visitAssignmentService'
|
||||
import { STALE_VISIT_ASSIGNMENTS } from '../lib/constants'
|
||||
import { useToastStore } from '../stores/toastStore'
|
||||
|
||||
export function useVisitAssignments() {
|
||||
return useQuery({
|
||||
queryKey: ['visit-assignments'],
|
||||
queryFn: async () => (await visitAssignmentService.getAll()).data,
|
||||
staleTime: STALE_VISIT_ASSIGNMENTS,
|
||||
})
|
||||
}
|
||||
|
||||
/** Grundlage der Benachrichtigungsglocke: Besichtigung in unter 24 h ohne Bericht. */
|
||||
export function useVisitReportWarnings() {
|
||||
return useQuery({
|
||||
queryKey: ['visit-assignments', 'warnings'],
|
||||
queryFn: async () => (await visitAssignmentService.getPendingReportWarnings()).data,
|
||||
staleTime: STALE_VISIT_ASSIGNMENTS,
|
||||
})
|
||||
}
|
||||
|
||||
export function useRequestVisitReport() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => (await visitAssignmentService.requestReport(id)).data,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['visit-assignments'] })
|
||||
useToastStore.getState().showToast('Bericht bei Bruno angefordert.', 'success')
|
||||
},
|
||||
onError: () => {
|
||||
useToastStore.getState().showToast('Bericht konnte nicht angefordert werden.', 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user