feat(team): Datenschicht für Property On
Property On wird als Funktionsbereich "Teamübersicht" in Property Match eingebettet — sieben digitale Mitarbeiter, die ausführen, während der Mensch freigibt, anpasst oder zurückweist. Durchgehend Provider → Service → Hook, kein übersprungener Layer: - domain/: teamAgent, agentWorkItem, agentProtocol, agentConnection, agentFilters - mock-data/: 7 Personalblätter aus dem Agenten-Katalog (Aufgabenzahlen exakt: Ferdi 9, Sina 7, Lea 11, Bruno 9, Reto 10, Livia 11, Nora 8), 14 Vorgänge, 38 Protokolleinträge, 9 Verbindungen - provider/: vier Mockup-Provider mit versionierter localStorage-Persistenz - services/: Fachlogik inkl. Freigabe, Zurückweisung, Kennzahlen, Demo-Reset - hooks/: React Query, Mutationen entwerten Vorgänge, Kennzahlen und Protokoll gemeinsam — sonst zeigt die Oberfläche widersprüchliche Zahlen Namensgebung: Der Entitätstyp heisst TeamAgent, nicht Agent. Im Repo existiert bereits PowerOn als KI-Backend-Proxy, und Claude Code legt Arbeitskopien unter agent-* ab; ein nackter Typ Agent wäre in Suchen nicht auffindbar. Alle Satellitentypen sind Agent*-präfixiert, weil domain/index.ts per export * bündelt und generische Namen dort kollidieren. Eigener Protokolltyp statt ActivityEvent: den Namen gibt es im Repo dreifach und gegenseitig inkompatibel (domain/activityEvent.ts, governanceService.ts, PropertyActivityLogPanel.tsx). Ein Anschluss hätte den Konflikt zementiert. Demo-Uhr in lib/teamClock.ts: die Mockdaten sind auf den 20.05.2026 verankert. Gegen die Systemzeit gerechnet wären die Filter "heute / diese Woche / dieser Monat" an jedem anderen Kalendertag leer und die Auswertung sähe kaputt aus, obwohl sie korrekt rechnet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,10 @@ export const STALE_MATCHES = 2 * 60 * 1000
|
||||
export const STALE_SIGNALS = 5 * 60 * 1000
|
||||
export const STALE_MARKET_SIGNALS = 30 * 1000 // aggressive — market leads refresh often
|
||||
export const STALE_REVIEW_QUEUE = 30 * 1000 // aggressive — ops team works in real-time
|
||||
export const STALE_TEAM_AGENTS = 5 * 60 * 1000
|
||||
export const STALE_AGENT_WORK_ITEMS = 15 * 1000 // aggressive — Freigaben ändern die Liste sofort
|
||||
export const STALE_AGENT_PROTOCOL = 15 * 1000 // aggressive — jede Aktion schreibt einen Eintrag
|
||||
export const STALE_AGENT_CONNECTIONS = 60 * 1000
|
||||
|
||||
// Route paths — single source of truth
|
||||
export const ROUTES = {
|
||||
@@ -42,6 +46,15 @@ export const ROUTES = {
|
||||
MATCH_CENTER: '/supply/match-center',
|
||||
FUTURE_AVAILABILITY: '/supply/future-availability',
|
||||
DATA_QUALITY: '/supply/data-quality',
|
||||
// Property On — «Teamübersicht». Basispfad bewusst `/supply/team` statt
|
||||
// `/supply/teamuebersicht`: kürzer, kollidiert nicht mit `PowerOn` in Suchen
|
||||
// und passt zum Ordner `components/team/`. Hierarchie und Deep-Links bleiben.
|
||||
TEAM: '/supply/team',
|
||||
TEAM_PERSONNEL: '/supply/team/personalverwaltung',
|
||||
TEAM_HISTORY: '/supply/team/bearbeitungsverlauf',
|
||||
TEAM_HISTORY_DONE: '/supply/team/bearbeitungsverlauf/erledigte-auftraege',
|
||||
TEAM_HISTORY_PENDING: '/supply/team/bearbeitungsverlauf/pendente-anfragen',
|
||||
TEAM_CONNECTIONS: '/supply/team/kanaele-systeme',
|
||||
},
|
||||
DEMAND: {
|
||||
AI_SEARCH: '/demand/ai-search',
|
||||
@@ -132,6 +145,198 @@ export const DATA_QUALITY_LABELS: Record<string, string> = {
|
||||
INCOMPLETE: 'Unvollständig',
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Property On — «Teamübersicht»
|
||||
//
|
||||
// Schweizer Schreibweise, kein Eszett. Sprache der Zielgruppe: Immobilien-
|
||||
// bewirtschafter, nicht KI-Entwickler — deshalb «Aufgabe», «Freigabe»,
|
||||
// «Systemzugang» statt Prompt, Token, Tool Call.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const AGENT_STATUS_LABELS: Record<string, string> = {
|
||||
ACTIVE: 'Aktiv',
|
||||
BUILDING: 'Im Aufbau',
|
||||
PAUSED: 'Pausiert',
|
||||
}
|
||||
|
||||
export const AGENT_AUTONOMY_LABELS: Record<string, string> = {
|
||||
AUTONOMOUS: 'Autonom',
|
||||
APPROVAL_REQUIRED: 'Freigabe erforderlich',
|
||||
PROPOSAL_ONLY: 'Nur Vorschlag',
|
||||
}
|
||||
|
||||
export const AGENT_CHANNEL_LABELS: Record<string, string> = {
|
||||
EMAIL: 'E-Mail',
|
||||
TEAMS: 'Microsoft Teams',
|
||||
WHATSAPP: 'WhatsApp Business',
|
||||
WORKSPACE_CHAT: 'Workspace-Chat',
|
||||
CALENDAR: 'Kalender',
|
||||
WEB_FORM: 'Website-Formular',
|
||||
PORTAL_LEAD_MAIL: 'Portal-Lead-Mail',
|
||||
PHONE_VOICE: 'Telefon / Voice',
|
||||
PHONE_NOTE: 'Telefonnotiz',
|
||||
AUDIO_OUTPUT: 'Audioausgabe',
|
||||
STRUCTURED_MAIL_FALLBACK: 'Strukturierter Mail-Fallback',
|
||||
}
|
||||
|
||||
export const AGENT_CHANNEL_DIRECTION_LABELS: Record<string, string> = {
|
||||
INBOUND: 'Eingang',
|
||||
OUTBOUND: 'Ausgang',
|
||||
BOTH: 'Eingang und Ausgang',
|
||||
}
|
||||
|
||||
export const AGENT_CONNECTION_STATUS_LABELS: Record<string, string> = {
|
||||
CONNECTED: 'Verbunden',
|
||||
DISCONNECTED: 'Nicht verbunden',
|
||||
ROADMAP: 'Geplant',
|
||||
}
|
||||
|
||||
export const AGENT_SYSTEM_LABELS: Record<string, string> = {
|
||||
M365_EXCHANGE: 'Microsoft 365 / Exchange',
|
||||
OUTLOOK: 'Outlook',
|
||||
SHAREPOINT: 'SharePoint',
|
||||
ONEDRIVE: 'OneDrive',
|
||||
FILESHARE: 'Windows-Fileshare',
|
||||
DMS: 'Dokumentenablage (DMS)',
|
||||
CALENDAR: 'Kalender',
|
||||
IMMOTOP2: 'ImmoTop2',
|
||||
ABACUS_ABAIMMO: 'Abacus AbaImmo',
|
||||
RIMO_R5: 'Rimo R5',
|
||||
GARAIO_REM: 'Garaio REM',
|
||||
CRM: 'CRM',
|
||||
ZEFIX: 'Zefix',
|
||||
SHAB: 'SHAB',
|
||||
PUBLIC_WEB: 'Öffentliche Webquellen',
|
||||
PORTAL_LEAD_INBOX: 'Portal-Lead-Postfach',
|
||||
WORKSPACE: 'Property-On-Workspace',
|
||||
}
|
||||
|
||||
export const AGENT_ACCESS_LABELS: Record<string, string> = {
|
||||
READ: 'Lesen',
|
||||
WRITE: 'Schreiben',
|
||||
READ_WRITE: 'Lesen und schreiben',
|
||||
}
|
||||
|
||||
export const AGENT_WORK_ITEM_STATUS_LABELS: Record<string, string> = {
|
||||
PENDING: 'Offen',
|
||||
APPROVED: 'Freigegeben',
|
||||
REJECTED: 'Zurückgewiesen',
|
||||
EDITING: 'In Bearbeitung',
|
||||
COMPLETED: 'Abgeschlossen',
|
||||
}
|
||||
|
||||
export const AGENT_PRIORITY_LABELS: Record<string, string> = {
|
||||
LOW: 'Niedrig',
|
||||
MEDIUM: 'Mittel',
|
||||
HIGH: 'Hoch',
|
||||
CRITICAL: 'Kritisch',
|
||||
}
|
||||
|
||||
export const AGENT_WORK_ITEM_KIND_LABELS: Record<string, string> = {
|
||||
MESSAGE_TRIAGE: 'Nachricht kategorisiert',
|
||||
REMINDER_CREATED: 'Reminder erstellt',
|
||||
CONTRACT_ANSWER: 'Vertragsauskunft',
|
||||
LOCATION_MODULE: 'Lagebaustein',
|
||||
INQUIRY_QUALIFIED: 'Anfrage qualifiziert',
|
||||
AUTO_REJECTION: 'Automatische Absage',
|
||||
APPOINTMENT: 'Termin',
|
||||
BRIEFING: 'Besichtigungs-Briefing',
|
||||
RECAP: 'Gesprächsprotokoll',
|
||||
MARKET_SIGNAL: 'Marktsignal',
|
||||
ESCALATION: 'Eskalation',
|
||||
DOC_GAP: 'Dokumentationslücke',
|
||||
SYSTEM_ISSUE: 'Systemproblem',
|
||||
}
|
||||
|
||||
export const AGENT_DOMAIN_AREA_LABELS: Record<string, string> = {
|
||||
CONTRACT_DEADLINES: 'Fristen und Verträge',
|
||||
DOCUMENTATION: 'Dokumentation',
|
||||
INQUIRIES: 'Anfragen',
|
||||
VIEWINGS: 'Besichtigungen',
|
||||
LOCATION: 'Lage',
|
||||
MARKET: 'Markt',
|
||||
}
|
||||
|
||||
export const AGENT_WORK_ITEM_ACTION_LABELS: Record<string, string> = {
|
||||
APPROVE: 'Freigeben',
|
||||
ONE_CLICK_CONFIRM: 'Mit einem Klick bestätigen',
|
||||
DECIDE: 'Entscheidung treffen',
|
||||
EDIT: 'Anpassen',
|
||||
REJECT: 'Zurückweisen',
|
||||
ANSWER_QUERY: 'Rückfrage beantworten',
|
||||
OPEN_SOURCE: 'Quelle öffnen',
|
||||
MARK_DONE: 'Als erledigt markieren',
|
||||
}
|
||||
|
||||
export const AGENT_PROTOCOL_STATUS_LABELS: Record<string, string> = {
|
||||
SUCCESS: 'Erfolgreich',
|
||||
WARNING: 'Warnung',
|
||||
ERROR: 'Fehler',
|
||||
INFO: 'Hinweis',
|
||||
}
|
||||
|
||||
export const AGENT_PROTOCOL_EVENT_LABELS: Record<string, string> = {
|
||||
TASK_RUN: 'Aufgabe ausgeführt',
|
||||
MESSAGE_PROCESSED: 'Nachricht verarbeitet',
|
||||
REMINDER_SENT: 'Erinnerung versendet',
|
||||
ESCALATION: 'Eskalation',
|
||||
APPROVAL: 'Freigabe',
|
||||
REJECTION: 'Zurückweisung',
|
||||
EDIT: 'Anpassung',
|
||||
SETTINGS_SAVED: 'Einstellungen gespeichert',
|
||||
TASKS_SAVED: 'Aufgaben gespeichert',
|
||||
AGENT_PAUSED: 'Mitarbeiter pausiert',
|
||||
AGENT_RESUMED: 'Mitarbeiter reaktiviert',
|
||||
CHANNEL_TESTED: 'Verbindung getestet',
|
||||
CHANNEL_TOGGLED: 'Kanal umgeschaltet',
|
||||
CONNECTION_CREATED: 'Verbindung angelegt',
|
||||
CONNECTION_REMOVED: 'Verbindung getrennt',
|
||||
DEMO_RESET: 'Demo zurückgesetzt',
|
||||
}
|
||||
|
||||
export const AGENT_CONNECTION_CATEGORY_LABELS: Record<string, string> = {
|
||||
EMAIL_M365: 'E-Mail / Microsoft 365',
|
||||
WHATSAPP: 'WhatsApp Business',
|
||||
TEAMS: 'Microsoft Teams',
|
||||
CALENDAR: 'Kalender',
|
||||
DOCUMENT_STORE: 'Dokumentenablage',
|
||||
PHONE_VOICE: 'Telefon / Voice',
|
||||
ERP: 'ERP',
|
||||
CRM: 'CRM',
|
||||
PUBLIC_SOURCES: 'Öffentliche Datenquellen',
|
||||
}
|
||||
|
||||
export const AGENT_ERP_VENDOR_LABELS: Record<string, string> = {
|
||||
IMMOTOP2: 'ImmoTop2',
|
||||
ABACUS_ABAIMMO: 'Abacus AbaImmo',
|
||||
RIMO_R5: 'Rimo R5',
|
||||
GARAIO_REM: 'Garaio REM',
|
||||
OTHER: 'Anderes ERP',
|
||||
}
|
||||
|
||||
export const AGENT_SETTING_GROUP_LABELS: Record<string, string> = {
|
||||
GENERAL: 'Allgemein',
|
||||
APPROVAL: 'Freigabe und Eskalation',
|
||||
DELIVERY: 'Zustellung',
|
||||
SPECIFIC: 'Fachliche Einstellungen',
|
||||
}
|
||||
|
||||
/** Zeitraumfilter für Auswertung und Bearbeitungsverlauf (§4.2, §5.3). */
|
||||
export const AGENT_PERIOD_LABELS: Record<string, string> = {
|
||||
TODAY: 'Heute',
|
||||
WEEK: 'Diese Woche',
|
||||
MONTH: 'Dieser Monat',
|
||||
YEAR: 'Dieses Jahr',
|
||||
ALL: 'Gesamter Zeitraum',
|
||||
}
|
||||
|
||||
/** Simulierte Ladezeit für Demo-Aktionen (§19.1: 300–800 ms). */
|
||||
export const AGENT_SIM_DELAY_MS = 420
|
||||
export const AGENT_SIM_DELAY_SLOW_MS = 760
|
||||
|
||||
/** Hinweis, der auf jeder Property-On-Seite dezent mitläuft (§19.3). */
|
||||
export const AGENT_DEMO_NOTICE = 'Demo-Modus – keine echten Daten oder Systemverbindungen.'
|
||||
|
||||
// Swiss CRB/BKP fit-out investment benchmarks (CHF/m², tenant's cost)
|
||||
export const FIT_OUT_COST_CHF_PER_SQM: Record<string, { min: number; max: number }> = {
|
||||
SHELL: { min: 800, max: 1500 },
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Property On — Demo-Uhr.
|
||||
*
|
||||
* Die Mockdaten sind fachlich auf den 20.05.2026 verankert. Würden die
|
||||
* Zeitraumfilter «heute / diese Woche / dieser Monat» gegen die echte Systemzeit
|
||||
* rechnen, wären sie an jedem anderen Kalendertag leer und die Auswertung sähe
|
||||
* kaputt aus, obwohl sie korrekt arbeitet.
|
||||
*
|
||||
* Deshalb läuft der gesamte Property-On-Bereich gegen eine Demo-Uhr: sie startet
|
||||
* am Anker und schreitet ab Anwendungsstart in Echtzeit fort. Damit landet jede
|
||||
* Freigabe, die der Nutzer während der Demo auslöst, verlässlich im Bucket
|
||||
* «heute» — und die Seed-Daten liegen dort, wo der Katalog sie erwartet.
|
||||
*
|
||||
* Ausschliesslich für Property On. Der Rest der Anwendung rechnet unverändert
|
||||
* mit `new Date()`.
|
||||
*/
|
||||
|
||||
import { MOCK_TODAY } from '../mock-data/teamAgents'
|
||||
|
||||
const ANCHOR_MS = new Date(MOCK_TODAY).getTime()
|
||||
const BOOT_MS = Date.now()
|
||||
|
||||
/** Aktueller Zeitpunkt der Demo-Uhr. */
|
||||
export function teamNow(): Date {
|
||||
return new Date(ANCHOR_MS + (Date.now() - BOOT_MS))
|
||||
}
|
||||
|
||||
/** Aktueller Zeitpunkt als ISO-String — für Zeitstempel in Vorgängen und Protokoll. */
|
||||
export function teamNowIso(): string {
|
||||
return teamNow().toISOString()
|
||||
}
|
||||
|
||||
/** Datum und Uhrzeit, de-CH: «20.05.2026, 08:15». */
|
||||
export function formatTeamDateTime(iso: string): string {
|
||||
return new Intl.DateTimeFormat('de-CH', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(iso))
|
||||
}
|
||||
|
||||
/** Nur die Uhrzeit, de-CH: «08:15». */
|
||||
export function formatTeamTime(iso: string): string {
|
||||
return new Intl.DateTimeFormat('de-CH', { hour: '2-digit', minute: '2-digit' }).format(new Date(iso))
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative Angabe gegen die Demo-Uhr.
|
||||
*
|
||||
* `formatRelativeDate` aus `lib/utils.ts` rechnet gegen `Date.now()` und würde
|
||||
* für die auf den 20.05.2026 verankerten Bestände durchgehend Abstände von
|
||||
* Monaten ausgeben — jeder Vorgang sähe veraltet aus.
|
||||
*/
|
||||
export function formatTeamRelative(iso: string): string {
|
||||
const diff = teamNow().getTime() - new Date(iso).getTime()
|
||||
if (diff < 0) return formatTeamDateTime(iso)
|
||||
|
||||
const minutes = Math.floor(diff / 60_000)
|
||||
if (minutes < 1) return 'gerade eben'
|
||||
if (minutes < 60) return `vor ${minutes} Minute${minutes === 1 ? '' : 'n'}`
|
||||
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `vor ${hours} Stunde${hours === 1 ? '' : 'n'}`
|
||||
|
||||
const days = Math.floor(hours / 24)
|
||||
if (days < 7) return `vor ${days} Tag${days === 1 ? '' : 'en'}`
|
||||
|
||||
return formatTeamDateTime(iso)
|
||||
}
|
||||
Reference in New Issue
Block a user