import { useCallback, useMemo, useState } from 'react' import { Alert, Box, Button, Divider, MenuItem, Step, StepButton, Stepper, TextField, Typography, } from '@mui/material' import { Download, Eye, FileText, Sparkles } from 'lucide-react' import type { ExposeDraft, ExposeValues } from '../../domain/expose' import { ExposeBranding } from '../../domain/expose' import type { Property } from '../../domain/property' import { ExposeMediaManager } from './ExposeMediaManager' import { ExposeFieldGroup } from './ExposeFieldGroup' import { PanelLoadingState } from '../ui' import { useExposeDraft, useGenerateExpose, useGenerateExposeText, useSaveExposeDraft } from '../../hooks/useExpose' import { textFactsFrom } from '../../services/exposeService' import { downloadExposePdf, downloadExposeWord, openExposePreview } from '../../services/exposeExport' import { EXPOSE_SECTIONS, missingRequiredExposeKeys } from '../../lib/exposeFields' import { EXPOSE_STEPS, EXPOSE_TONALITY_LABELS } from '../../lib/constants' import { ExposeTextSection } from '../../services/ai/IAIService' import type { ExposeTonality } from '../../services/ai/IAIService' import { useToastStore } from '../../stores/toastStore' import { DS_BRAND, DS_NEUTRAL, DS_SLATE, DS_TEXT } from '../../lib/ds' /** Welcher Textabschnitt gehört zu welchem Feld? Basis für «KI-Entwurf erstellen». */ const TEXT_FIELD_SECTIONS: { key: string; section: ExposeTextSection; label: string }[] = [ { key: 'exposeTitel', section: ExposeTextSection.TITLE, label: 'Exposé-Titel' }, { key: 'kurzbeschrieb', section: ExposeTextSection.TEASER, label: 'Kurzbeschrieb' }, { key: 'objektbeschrieb', section: ExposeTextSection.OBJECT, label: 'Objektbeschrieb' }, { key: 'lagebeschrieb', section: ExposeTextSection.LOCATION, label: 'Lagebeschrieb' }, { key: 'gemeindebeschrieb', section: ExposeTextSection.MUNICIPALITY, label: 'Gemeindebeschrieb' }, { key: 'ausstattungsbeschrieb',section: ExposeTextSection.FEATURES, label: 'Ausstattungsbeschrieb' }, { key: 'highlights', section: ExposeTextSection.HIGHLIGHTS, label: 'Highlights' }, ] interface Props { leadId: string property: Property } /** * Der dreistufige Exposé-Prozess: Hochladen → Exposé → Export (Runde 4, §8.5). * * Lädt den Entwurf und übergibt ihn als Startwert an den Arbeitsbereich. Die * Trennung in Lader und Arbeitsbereich hat einen Grund: der lokale Entwurf soll * genau einmal aus dem gespeicherten Stand entstehen. Über `key` löst der * Wechsel auf ein anderes Objekt einen frischen Arbeitsbereich aus, statt einen * Effekt zu brauchen, der den lokalen Stand nachträglich überschreibt. */ export function ExposeWizard({ leadId, property }: Props) { const { data: stored, isLoading } = useExposeDraft(leadId, property) if (isLoading || !stored) return return } /** * Der Entwurf lebt lokal, solange gearbeitet wird, und geht erst beim * Speichern in den Service. Das ist Absicht: bei jedem Tastendruck zu * speichern hiesse, jeden Zwischenstand zum gültigen Stand zu erklären. * * «Speichern» und «Erstellen» sind getrennt — Speichern hält den Zwischenstand * fest, Erstellen prüft die Vollständigkeit und schaltet den Export frei. */ function ExposeWizardBody({ initialDraft }: { initialDraft: ExposeDraft }) { const saveDraft = useSaveExposeDraft() const generate = useGenerateExpose() const generateText = useGenerateExposeText() const showToast = useToastStore(s => s.showToast) const [step, setStep] = useState(0) const [draft, setDraft] = useState(initialDraft) const missing = useMemo( () => new Set(missingRequiredExposeKeys(draft.values)), [draft], ) const setValue = useCallback((key: string, value: string) => { setDraft(prev => ({ ...prev, values: { ...prev.values, [key]: value } })) }, []) const setValues = useCallback((patch: ExposeValues) => { setDraft(prev => ({ ...prev, values: { ...prev.values, ...patch } })) }, []) const runTextDraft = useCallback( async (target: { key: string; section: ExposeTextSection; label: string }) => { const result = await generateText.mutateAsync({ section: target.section, tonality: draft.tonality, facts: textFactsFrom(draft.values), }) if (result.missingFacts.length > 0) { showToast( `${target.label}: Es fehlen belegte Angaben (${result.missingFacts.join(', ')}) — bitte zuerst erfassen.`, 'warning', ) return } setValues({ [target.key]: result.text }) }, [draft, generateText, setValues, showToast], ) const runAllTextDrafts = useCallback(async () => { for (const target of TEXT_FIELD_SECTIONS) { await runTextDraft(target) } }, [runTextDraft]) const isGenerated = Boolean(draft.generatedAt) return ( {/* Schrittleiste */} {EXPOSE_STEPS.map((label, index) => ( setStep(index)} sx={{ textTransform: 'none' }}> {label} ))} {/* ── Schritt 1: Hochladen ── */} {step === 0 && ( setDraft({ ...draft, images })} /> )} {/* ── Schritt 2: Exposé ── */} {step === 1 && ( setDraft({ ...draft, tonality: e.target.value as ExposeTonality })} sx={{ width: 210 }} > {Object.entries(EXPOSE_TONALITY_LABELS).map(([value, label]) => ( {label} ))} {missing.size > 0 && ( {missing.size} Pflichtangabe(n) fehlen )} {EXPOSE_SECTIONS.map(section => ( {TEXT_FIELD_SECTIONS.map(t => ( ))} ) : undefined } /> ))} {/* Medien-Kuration bleibt aus Schritt 1 erhalten und wird hier nur verlinkt — dieselbe Liste zweimal zu rendern wäre Duplikat. */} Kontakte & CI setDraft({ ...draft, contactPerson: e.target.value })} /> setDraft({ ...draft, branding: e.target.value as ExposeBranding })} > Aus Maklerprofil übernehmen Manuell setzen {draft.branding === ExposeBranding.MANUAL && ( setDraft({ ...draft, brandColor: e.target.value })} /> )} )} {/* ── Schritt 3: Export ── */} {step === 2 && ( {!isGenerated && ( Das Exposé ist noch nicht erstellt. Vorschau und Download zeigen den aktuellen Stand. )} {missing.size > 0 && ( Es fehlen noch {missing.size} Pflichtangabe(n) — im Schritt «Exposé» rot umrandet. )} Der PDF-Export nutzt den Druckdialog des Browsers («Als PDF speichern»); CI, Bilder, Texte und Anhänge folgen der Exposé-Konfiguration. )} {/* Speichern und Erstellen sind klar getrennt (§8.5.2). */} {isGenerated ? `Erstellt am ${new Date(draft.generatedAt!).toLocaleString('de-CH')}` : 'Noch kein Exposé erstellt.'} ) }