feat(nora): Runde 10 Prompt 3 — Property Matching gegen den eigenen Bestand
Nora arbeitet jetzt mit dem Objektbestand statt mit Quellen. - Lead-Detail auf ganzer Seite statt in der Schublade; darunter der neue Bereich «Property Match». Derselbe Baustein wie bisher, nur mit einem Layoutschalter — keine zweite Detailansicht - «Das hat Livia recherchiert» heisst «Erstelltes Suchprofil» und zeigt lead.semanticSearchProfile. Ein Kasten, nicht zwei: Bestandsleads fallen auf ihren bisherigen Text zurück - Matching-Logik in features/matching/leadPropertyMatcher.ts: Das Suchprofil wird nach Merkmalen gelesen, die darin tatsächlich vorkommen, und gegen das Objekt geprüft. Ein fehlendes Objektmerkmal geht nicht in die Rechnung ein — es gilt als nicht bekannt, nicht als erfüllt. Deterministisch, kein Zufall - Trefferliste ist dieselbe Tabelle wie «Meine Objekte», ergänzt um eine erste Spalte «Match». Die übrigen Spalten werden anteilig gestaucht, damit keine waagrechte Bildlaufleiste entsteht - «Belegte Objekte einbeziehen», Vorgabe aus; Umschalten rechnet neu - MatchRun als eigene Entität mit Persistenz. Genau ein Eintrag je Lauf, auch bei der Übergabe aus Livia — eine Sperre verhindert den zweiten Lauf durch Neuaufbau der Komponente - Aus Livia übergebene Leads starten das Matching einmal; ein normal geöffneter Lead startet nichts - Kennzahlen «Anzahl durchgeführte Matchings» und «Anzahl betroffene Objekte» kommen aus dem laufenden Stand, nicht aus dem Katalog - Aufgaben, Systeme (ERP statt Zefix/SHAB/Webquellen) und Einstellungen auf das Matching zugeschnitten - Info-Knopf erklärt den Match-Wert entlang der tatsächlichen Rechnung Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
314a6a36e3
commit
c6011ac093
15 files changed
+1338
-275
No files matched your search
@@ -1,16 +1,28 @@
|
||||
import { memo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import type { AgentMetric, TeamAgent } from '../../domain/teamAgent'
|
||||
import { EmptyState } from '../ui'
|
||||
import { useMatchRuns } from '../../hooks/useMatchRuns'
|
||||
import { useFutureSignals } from '../../hooks/useFutureSignals'
|
||||
import { isRisiko } from '../../lib/leadSignal'
|
||||
import { DS_BG, DS_BORDER, DS_TEXT } from '../../lib/ds'
|
||||
|
||||
/**
|
||||
* Reiter «Kennzahlen».
|
||||
*
|
||||
* Zeigt ausschliesslich die Kennzahlen, die im Personalblatt des Mitarbeitenden
|
||||
* bereits hinterlegt sind. Es werden keine Werte berechnet, hochgerechnet oder
|
||||
* ergänzt — was der Katalog nicht führt, steht hier auch nicht.
|
||||
* Zeigt die Kennzahlen, die das Personalblatt führt. Es wird nichts
|
||||
* hochgerechnet und nichts ergänzt — was der Katalog nicht führt, steht hier
|
||||
* auch nicht.
|
||||
*
|
||||
* Ausnahme seit Runde 10 (§3.11): Noras beide Kennzahlen kommen aus dem
|
||||
* laufenden Stand der Anwendung und nicht aus dem Katalog. Ein fester Wert
|
||||
* wäre dort eine Behauptung — die eine Zahl zählt tatsächlich ausgeführte
|
||||
* Matching-Läufe, die andere tatsächlich zugeordnete Objekte.
|
||||
*/
|
||||
|
||||
/** Kennungen, deren Wert zur Laufzeit entsteht. */
|
||||
const MATCH_RUNS_METRIC = 'nora-metric-match-runs'
|
||||
const AFFECTED_OBJECTS_METRIC = 'nora-metric-affected-objects'
|
||||
const MetricTile = memo(function MetricTile({ metric }: { metric: AgentMetric }) {
|
||||
return (
|
||||
<Box
|
||||
@@ -46,7 +58,46 @@ const MetricTile = memo(function MetricTile({ metric }: { metric: AgentMetric })
|
||||
})
|
||||
|
||||
export function AgentMetricsTab({ agent }: { agent: TeamAgent }) {
|
||||
if (agent.metrics.length === 0) {
|
||||
const { data: matchRuns = [] } = useMatchRuns()
|
||||
const { data: signals = [] } = useFutureSignals()
|
||||
|
||||
/**
|
||||
* Die beiden Laufzeit-Kennzahlen einsetzen.
|
||||
*
|
||||
* «Betroffene Objekte» zählt Risikosignale mit eindeutig zugeordnetem
|
||||
* Objekt — und zwar die Objekte, nicht die Signale: Zwei Meldungen über
|
||||
* dasselbe Objekt sind ein betroffenes Objekt, nicht zwei.
|
||||
*/
|
||||
const metrics = useMemo((): AgentMetric[] => {
|
||||
const laeufe = matchRuns.length
|
||||
const betroffene = new Set(
|
||||
signals.filter(sig => isRisiko(sig) && sig.propertyId).map(sig => sig.propertyId!),
|
||||
).size
|
||||
|
||||
return agent.metrics.map((m) => {
|
||||
if (m.id === MATCH_RUNS_METRIC) {
|
||||
return {
|
||||
...m,
|
||||
value: String(laeufe),
|
||||
hint: laeufe === 0
|
||||
? 'Noch kein Matching ausgeführt. Die Zahl steigt nur durch einen tatsächlichen Lauf.'
|
||||
: `Zuletzt am ${new Date(matchRuns[0].timestamp).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' })} für «${matchRuns[0].leadTitle}».`,
|
||||
}
|
||||
}
|
||||
if (m.id === AFFECTED_OBJECTS_METRIC) {
|
||||
return {
|
||||
...m,
|
||||
value: String(betroffene),
|
||||
hint: betroffene === 0
|
||||
? 'Derzeit ist keinem Risikosignal ein Objekt aus dem Bestand eindeutig zugeordnet.'
|
||||
: m.hint,
|
||||
}
|
||||
}
|
||||
return m
|
||||
})
|
||||
}, [agent.metrics, matchRuns, signals])
|
||||
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="Keine Kennzahlen hinterlegt"
|
||||
@@ -63,7 +114,7 @@ export function AgentMetricsTab({ agent }: { agent: TeamAgent }) {
|
||||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, minmax(0, 1fr))', lg: 'repeat(3, minmax(0, 1fr))' },
|
||||
}}
|
||||
>
|
||||
{agent.metrics.map((metric) => (
|
||||
{metrics.map((metric) => (
|
||||
<MetricTile key={metric.id} metric={metric} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
Reference in new issue
Block a user