Design P6: Angebots-Formular als Dialog (Esc, Sperre beim Speichern, Vorschlagsliste geprueft)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 14:36:45 +00:00
1 parent 2f6437ee42
commit 568911967d
1 file changed
+54 -45
+54 -45
View File
@@ -1,11 +1,10 @@
import { useEffect, useMemo, useState } from 'react'
import { FaSpinner } from 'react-icons/fa6'
import { Card, Button, AutocompleteInput } from '../ui'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Dialog, Field, Button, AutocompleteInput } from '../ui'
import { useCustomers } from '../../hooks/useCustomers'
import { useScrollIntoView } from '../../hooks/useScrollIntoView'
import { createOffer, getOfferContext } from '../../lib/teamApi'
import { customerPickLabel, findCustomerByLabel } from '../../lib/chatFormat'
import { createdMessage, evaluateOfferForm, offerConfirm } from '../../lib/offerFormat'
import '../../styles/admin-areas.css'
const MONTHS = Array.from({ length: 12 }, (_, i) => i + 1)
@@ -29,6 +28,8 @@ export default function OfferForm({ fixedCustomerId, fixedCustomerName, onCreate
const [ctx, setCtx] = useState(null) // { customerId, data } | { customerId, error }
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const errorRef = useRef(null)
useScrollIntoView(errorRef, error)
useEffect(() => {
setCtx(null)
@@ -83,62 +84,70 @@ export default function OfferForm({ fixedCustomerId, fixedCustomerName, onCreate
}
let previewBox
if (!customerId) previewBox = <div>Bitte einen Kunden aus der Liste wählen.</div>
else if (ctx?.error && ctx.customerId === customerId) previewBox = <div className="area-danger">{ctx.error}</div>
else if (!ready) previewBox = <div>Lade Kunde…</div>
if (!customerId) previewBox = <p>Bitte einen Kunden aus der Liste wählen.</p>
else if (ctx?.error && ctx.customerId === customerId) previewBox = <p className="msg err">{ctx.error}</p>
else if (!ready) previewBox = <p>Lade Kunde…</p>
else {
previewBox = (
<>
{evaluation.errors.map((l) => <div key={l} className="area-danger">{l}</div>)}
{evaluation.previewLines.map((l) => <div key={l}>{l}</div>)}
{evaluation.errors.map((l) => <p key={l} className="msg err">{l}</p>)}
{evaluation.previewLines.map((l) => <p key={l}>{l}</p>)}
</>
)
}
return (
<Card className="offer-form">
<h3 style={{ marginTop: 0 }}>Neues Angebot</h3>
<div className="credit-calc">
<label style={{ gridColumn: '1 / -1' }}>Kunde
{fixedCustomerId
? <strong style={{ color: 'var(--text)' }}>{fixedCustomerName || 'Kunde'}</strong>
: <AutocompleteInput value={label} onChange={onPick} getSuggestions={suggestions} placeholder="Name, E-Mail oder Kundennummer tippen …" />}
</label>
<label>Produkt
<select className="form-control" value={productKey} onChange={(e) => setProductKey(e.target.value)}>
<Dialog
title="Neues Angebot"
onClose={onCancel}
busy={busy}
footer={
<>
<span className="grow" />
<Button type="button" onClick={onCancel} disabled={busy}>Abbrechen</Button>
<Button variant="primary" type="button" onClick={save} disabled={!evaluation || evaluation.errors.length > 0 || busy}>
{busy ? 'Speichert …' : evaluation?.saveLabel || 'Angebot anlegen'}
</Button>
</>
}
>
<Field label="Kunde" htmlFor={fixedCustomerId ? undefined : 'offer-customer'}>
{fixedCustomerId
? <p className="strong">{fixedCustomerName || 'Kunde'}</p>
: <AutocompleteInput id="offer-customer" value={label} onChange={onPick} getSuggestions={suggestions} placeholder="Name, E-Mail oder Kundennummer tippen …" />}
</Field>
<div className="field-row">
<Field label="Produkt" htmlFor="offer-product">
<select id="offer-product" className="select" value={productKey} onChange={(e) => setProductKey(e.target.value)}>
<option value="wartung">Wartungs-Abo</option>
<option value="hosting">Hosting-Abo</option>
</select>
</label>
<label>Preis pro Monat im Angebot (€)
<input className="form-control" type="number" min="0" step="0.01" value={price} onChange={(e) => setPrice(e.target.value)} />
</label>
<label>für
<select className="form-control" value={months} onChange={(e) => setMonths(Number(e.target.value))}>
</Field>
<Field label="Preis pro Monat im Angebot (€)" htmlFor="offer-price">
<input id="offer-price" className="input" type="number" min="0" step="0.01" value={price} onChange={(e) => setPrice(e.target.value)} />
</Field>
</div>
<div className="field-row">
<Field label="für" htmlFor="offer-months">
<select id="offer-months" className="select" value={months} onChange={(e) => setMonths(Number(e.target.value))}>
{MONTHS.map((m) => <option key={m} value={m}>{m} Monat{m === 1 ? '' : 'e'}</option>)}
</select>
</label>
<label>Angebot gültig bis
<input className="form-control" type="date" value={validUntil} onChange={(e) => setValidUntil(e.target.value)} />
</label>
</Field>
<Field label="Angebot gültig bis" htmlFor="offer-valid-until">
<input id="offer-valid-until" className="input" type="date" value={validUntil} onChange={(e) => setValidUntil(e.target.value)} />
</Field>
</div>
<label className="credit-field">Hinweis für den Kunden (optional, z. B. „Übernahme Ihres IONOS-Hostings“)
<input className="form-control" maxLength={500} value={note} onChange={(e) => setNote(e.target.value)} />
</label>
<label className="credit-field">Interne Notiz (optional, sieht nur das Team)
<input className="form-control" maxLength={2000} value={internalNote} onChange={(e) => setInternalNote(e.target.value)} />
</label>
<label style={{ display: 'flex', gap: 8, alignItems: 'center', margin: '8px 0' }}>
<Field label="Hinweis für den Kunden (optional, z. B. „Übernahme Ihres IONOS-Hostings“)" htmlFor="offer-note">
<input id="offer-note" className="input" maxLength={500} value={note} onChange={(e) => setNote(e.target.value)} />
</Field>
<Field label="Interne Notiz (optional, sieht nur das Team)" htmlFor="offer-internal-note">
<input id="offer-internal-note" className="input" maxLength={2000} value={internalNote} onChange={(e) => setInternalNote(e.target.value)} />
</Field>
<label className="check">
<input type="checkbox" checked={notify} onChange={(e) => setNotify(e.target.checked)} /> Kunden per E-Mail über das Angebot informieren
</label>
<div className="area-summary">{previewBox}</div>
<div className="area-inline">
<Button variant="primary" onClick={save} disabled={!evaluation || evaluation.errors.length > 0 || busy}>
{busy ? <FaSpinner className="spinner" /> : evaluation?.saveLabel || 'Angebot anlegen'}
</Button>
<Button variant="ghost" onClick={onCancel}>Abbrechen</Button>
</div>
{error && <div className="area-error">{error}</div>}
</Card>
<div>{previewBox}</div>
{error && <p className="msg err" role="alert" ref={errorRef}>{error}</p>}
</Dialog>
)
}