Design: Vertrags- und Versions-Formular als Dialoge, Fehler im Dialog

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 14:41:40 +00:00
1 parent a9fa9f58e6
commit 84186d1429
3 files changed
+78 -77

No files matched your search

+40 -39
View File
@@ -1,6 +1,6 @@
import { useState } from 'react' import { useRef, useState } from 'react'
import { FaTimes } from 'react-icons/fa' import { useScrollIntoView } from '../../hooks/useScrollIntoView'
import { Button } from '../ui' import { Button, Dialog, Field } from '../ui'
const FIELDS = ['provider', 'purpose', 'cost', 'costInterval', 'startDate', 'termMonths', 'renewalMonths', 'noticeValue', 'noticeUnit', 'note'] const FIELDS = ['provider', 'purpose', 'cost', 'costInterval', 'startDate', 'termMonths', 'renewalMonths', 'noticeValue', 'noticeUnit', 'note']
const NUMBERS = ['termMonths', 'renewalMonths', 'noticeValue'] const NUMBERS = ['termMonths', 'renewalMonths', 'noticeValue']
@@ -31,6 +31,8 @@ export default function ContractForm({ contract, onSave, onClose }) {
const [files, setFiles] = useState([]) const [files, setFiles] = useState([])
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const errorRef = useRef(null)
useScrollIntoView(errorRef, error)
const set = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value })) const set = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }))
const submit = async (e) => { const submit = async (e) => {
@@ -47,60 +49,59 @@ export default function ContractForm({ contract, onSave, onClose }) {
} }
const input = (key, label, props = {}) => ( const input = (key, label, props = {}) => (
<div className="form-group"> <Field label={label} htmlFor={`contract-${key}`}>
<label className="form-label">{label}</label> <input className="input" id={`contract-${key}`} name={key} value={form[key] || ''} onChange={set(key)} {...props} />
<input className="form-control" name={key} value={form[key] || ''} onChange={set(key)} {...props} /> </Field>
</div>
) )
return ( return (
<div className="overlay"> <Dialog
<span className="overlay-close" onClick={onClose}><FaTimes /></span> as="form"
<div className="overlay-content"> wide
<h2 className="mb-2">{contract ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}</h2> title={contract ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}
{error && <div className="legal-error">{error}</div>} busy={saving}
<form onSubmit={submit}> onClose={onClose}
onSubmit={submit}
footer={
<>
<span className="grow" />
<Button type="button" onClick={onClose} disabled={saving}>Abbrechen</Button>
<Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>
</>
}
>
{error && <div className="msg err" role="alert" ref={errorRef}>{error}</div>}
{input('provider', 'Anbieter', { required: true })} {input('provider', 'Anbieter', { required: true })}
{input('purpose', 'Wofür')} {input('purpose', 'Wofür')}
<div className="legal-form-row"> <div className="field-row">
{input('cost', 'Kosten (€)', { inputMode: 'decimal' })} {input('cost', 'Kosten (€)', { inputMode: 'decimal' })}
<div className="form-group"> <Field label="pro" htmlFor="contract-costInterval">
<label className="form-label">pro</label> <select className="select" id="contract-costInterval" name="costInterval" value={form.costInterval} onChange={set('costInterval')}>
<select className="form-control" name="costInterval" value={form.costInterval} onChange={set('costInterval')}>
<option value="month">Monat</option> <option value="month">Monat</option>
<option value="year">Jahr</option> <option value="year">Jahr</option>
</select> </select>
</div> </Field>
</div> </div>
{input('startDate', 'Vertragsbeginn', { type: 'date' })} {input('startDate', 'Vertragsbeginn', { type: 'date' })}
<div className="legal-form-row"> <div className="field-row">
{input('termMonths', 'Laufzeit (Monate, 0 = jederzeit kündbar)', { type: 'number', min: 0 })} {input('termMonths', 'Laufzeit (Monate, 0 = jederzeit kündbar)', { type: 'number', min: 0 })}
{input('renewalMonths', 'Verlängert sich um (Monate, 0 = endet)', { type: 'number', min: 0 })} {input('renewalMonths', 'Verlängert sich um (Monate, 0 = endet)', { type: 'number', min: 0 })}
</div> </div>
<div className="legal-form-row"> <div className="field-row">
{input('noticeValue', 'Kündigungsfrist', { type: 'number', min: 0 })} {input('noticeValue', 'Kündigungsfrist', { type: 'number', min: 0 })}
<div className="form-group"> <Field label="Einheit" htmlFor="contract-noticeUnit">
<label className="form-label">Einheit</label> <select className="select" id="contract-noticeUnit" name="noticeUnit" value={form.noticeUnit} onChange={set('noticeUnit')}>
<select className="form-control" name="noticeUnit" value={form.noticeUnit} onChange={set('noticeUnit')}>
<option value="months">Monate</option> <option value="months">Monate</option>
<option value="days">Tage</option> <option value="days">Tage</option>
</select> </select>
</Field>
</div> </div>
</div> <Field label="Notiz" htmlFor="contract-note">
<div className="form-group"> <textarea className="textarea" id="contract-note" name="note" rows={3} value={form.note || ''} onChange={set('note')} />
<label className="form-label">Notiz</label> </Field>
<textarea className="form-control" name="note" rows={3} value={form.note || ''} onChange={set('note')} /> <Field label="Dateien hinzufügen" htmlFor="contract-files">
</div> <input type="file" className="legal-file" id="contract-files" multiple accept=".pdf,.html,.htm,.png,.jpg,.jpeg,.webp,.doc,.docx" onChange={(e) => setFiles([...e.target.files])} />
<div className="form-group"> </Field>
<label className="form-label">Dateien hinzufügen</label> </Dialog>
<input type="file" multiple accept=".pdf,.html,.htm,.png,.jpg,.jpeg,.webp,.doc,.docx" onChange={(e) => setFiles([...e.target.files])} />
</div>
<div className="flex gap-2">
<Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>
<Button type="button" onClick={onClose}>Abbrechen</Button>
</div>
</form>
</div>
</div>
) )
} }
+36 -36
View File
@@ -1,6 +1,6 @@
import { useState } from 'react' import { useRef, useState } from 'react'
import { FaTimes } from 'react-icons/fa' import { useScrollIntoView } from '../../hooks/useScrollIntoView'
import { Button } from '../ui' import { Button, Dialog, Field } from '../ui'
import { todayIso } from './legalUi' import { todayIso } from './legalUi'
export default function VersionForm({ title, onSave, onClose }) { export default function VersionForm({ title, onSave, onClose }) {
@@ -8,6 +8,8 @@ export default function VersionForm({ title, onSave, onClose }) {
const [files, setFiles] = useState([]) const [files, setFiles] = useState([])
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const errorRef = useRef(null)
useScrollIntoView(errorRef, error)
const set = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value })) const set = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }))
const submit = async (e) => { const submit = async (e) => {
@@ -24,41 +26,39 @@ export default function VersionForm({ title, onSave, onClose }) {
} }
return ( return (
<div className="overlay"> <Dialog
<span className="overlay-close" onClick={onClose}><FaTimes /></span> as="form"
<div className="overlay-content"> title={`Neue Version: ${title}`}
<h2 className="mb-2">Neue Version: {title}</h2> busy={saving}
{error && <div className="legal-error">{error}</div>} onClose={onClose}
<form onSubmit={submit}> onSubmit={submit}
<div className="form-group"> footer={
<label className="form-label">Version</label> <>
<input className="form-control" name="version" value={form.version} onChange={set('version')} placeholder="z. B. v6" required /> <span className="grow" />
</div> <Button type="button" onClick={onClose} disabled={saving}>Abbrechen</Button>
<div className="form-group"> <Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>
<label className="form-label">Status</label> </>
<select className="form-control" name="status" value={form.status} onChange={set('status')}> }
>
{error && <div className="msg err" role="alert" ref={errorRef}>{error}</div>}
<Field label="Version" htmlFor="version-version">
<input className="input" id="version-version" name="version" value={form.version} onChange={set('version')} placeholder="z. B. v6" required />
</Field>
<Field label="Status" htmlFor="version-status">
<select className="select" id="version-status" name="status" value={form.status} onChange={set('status')}>
<option value="current">Gültig</option> <option value="current">Gültig</option>
<option value="draft">Entwurf (noch nicht live)</option> <option value="draft">Entwurf (noch nicht live)</option>
</select> </select>
</div> </Field>
<div className="form-group"> <Field label="Gültig ab" htmlFor="version-validFrom">
<label className="form-label">Gültig ab</label> <input className="input" id="version-validFrom" type="date" name="validFrom" value={form.validFrom} onChange={set('validFrom')} required />
<input className="form-control" type="date" name="validFrom" value={form.validFrom} onChange={set('validFrom')} required /> </Field>
</div> <Field label="Was hat sich geändert?" htmlFor="version-changeNote">
<div className="form-group"> <textarea className="textarea" id="version-changeNote" name="changeNote" rows={4} value={form.changeNote} onChange={set('changeNote')} />
<label className="form-label">Was hat sich geändert?</label> </Field>
<textarea className="form-control" name="changeNote" rows={4} value={form.changeNote} onChange={set('changeNote')} /> <Field label="Dateien (PDF, HTML, Bild, Word)" htmlFor="version-files">
</div> <input type="file" className="legal-file" id="version-files" multiple accept=".pdf,.html,.htm,.png,.jpg,.jpeg,.webp,.doc,.docx" onChange={(e) => setFiles([...e.target.files])} />
<div className="form-group"> </Field>
<label className="form-label">Dateien (PDF, HTML, Bild, Word)</label> </Dialog>
<input type="file" multiple accept=".pdf,.html,.htm,.png,.jpg,.jpeg,.webp,.doc,.docx" onChange={(e) => setFiles([...e.target.files])} />
</div>
<div className="flex gap-2">
<Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>
<Button type="button" onClick={onClose}>Abbrechen</Button>
</div>
</form>
</div>
</div>
) )
} }
+2 -2
View File
@@ -615,8 +615,8 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
.legal-files .legal-dl { color: var(--text-2); } .legal-files .legal-dl { color: var(--text-2); }
.legal-msg { margin: 0 0 12px; } .legal-msg { margin: 0 0 12px; }
.legal-doc h2.section .link { white-space: nowrap; } /* "Neue Version" bleibt am Handy eine Zeile */ .legal-doc h2.section .link { white-space: nowrap; } /* "Neue Version" bleibt am Handy eine Zeile */
.legal-error { background: var(--danger); color: #fff; padding: 8px 12px; border-radius: var(--r-sm); margin: 8px 0; } .legal-file { padding: 2px 1px; font-size: 13px; color: var(--text-2); } /* Platz für den Ring des Knopfs, das Dateifeld schneidet ihn sonst ab */
.legal-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); } .legal-file::file-selector-button { height: 28px; padding: 0 10px; margin-right: 10px; border: 0; border-radius: var(--radius); background: var(--bg); box-shadow: var(--ring); font: inherit; font-weight: 500; color: var(--text); cursor: pointer; }
.legal-contracts { margin-top: var(--sp-2); } .legal-contracts { margin-top: var(--sp-2); }
.legal-contracts .cell-title .link { text-align: left; } /* Knopf zentriert sonst lange Anbieternamen */ .legal-contracts .cell-title .link { text-align: left; } /* Knopf zentriert sonst lange Anbieternamen */
.legal-contracts tr.row:has(+ .legal-detail) td { border-bottom: 0; } /* offene Zeile und Detail bilden einen Block, die Haarlinie steht darunter */ .legal-contracts tr.row:has(+ .legal-detail) td { border-bottom: 0; } /* offene Zeile und Detail bilden einen Block, die Haarlinie steht darunter */