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:
1 parent
2f6437ee42
commit
568911967d
1 file changed
+54
-45
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user