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
+96 -95

No files matched your search

+55 -54
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}
{input('provider', 'Anbieter', { required: true })} onSubmit={submit}
{input('purpose', 'Wofür')} footer={
<div className="legal-form-row"> <>
{input('cost', 'Kosten (€)', { inputMode: 'decimal' })} <span className="grow" />
<div className="form-group"> <Button type="button" onClick={onClose} disabled={saving}>Abbrechen</Button>
<label className="form-label">pro</label> <Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>
<select className="form-control" name="costInterval" value={form.costInterval} onChange={set('costInterval')}> </>
<option value="month">Monat</option> }
<option value="year">Jahr</option> >
</select> {error && <div className="msg err" role="alert" ref={errorRef}>{error}</div>}
</div> {input('provider', 'Anbieter', { required: true })}
</div> {input('purpose', 'Wofür')}
{input('startDate', 'Vertragsbeginn', { type: 'date' })} <div className="field-row">
<div className="legal-form-row"> {input('cost', 'Kosten (€)', { inputMode: 'decimal' })}
{input('termMonths', 'Laufzeit (Monate, 0 = jederzeit kündbar)', { type: 'number', min: 0 })} <Field label="pro" htmlFor="contract-costInterval">
{input('renewalMonths', 'Verlängert sich um (Monate, 0 = endet)', { type: 'number', min: 0 })} <select className="select" id="contract-costInterval" name="costInterval" value={form.costInterval} onChange={set('costInterval')}>
</div> <option value="month">Monat</option>
<div className="legal-form-row"> <option value="year">Jahr</option>
{input('noticeValue', 'Kündigungsfrist', { type: 'number', min: 0 })} </select>
<div className="form-group"> </Field>
<label className="form-label">Einheit</label>
<select className="form-control" name="noticeUnit" value={form.noticeUnit} onChange={set('noticeUnit')}>
<option value="months">Monate</option>
<option value="days">Tage</option>
</select>
</div>
</div>
<div className="form-group">
<label className="form-label">Notiz</label>
<textarea className="form-control" name="note" rows={3} value={form.note || ''} onChange={set('note')} />
</div>
<div className="form-group">
<label className="form-label">Dateien hinzufügen</label>
<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>
</div> {input('startDate', 'Vertragsbeginn', { type: 'date' })}
<div className="field-row">
{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 })}
</div>
<div className="field-row">
{input('noticeValue', 'Kündigungsfrist', { type: 'number', min: 0 })}
<Field label="Einheit" htmlFor="contract-noticeUnit">
<select className="select" id="contract-noticeUnit" name="noticeUnit" value={form.noticeUnit} onChange={set('noticeUnit')}>
<option value="months">Monate</option>
<option value="days">Tage</option>
</select>
</Field>
</div>
<Field label="Notiz" htmlFor="contract-note">
<textarea className="textarea" id="contract-note" name="note" rows={3} value={form.note || ''} onChange={set('note')} />
</Field>
<Field label="Dateien hinzufügen" htmlFor="contract-files">
<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])} />
</Field>
</Dialog>
) )
} }
+39 -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'
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')}> }
<option value="current">Gültig</option> >
<option value="draft">Entwurf (noch nicht live)</option> {error && <div className="msg err" role="alert" ref={errorRef}>{error}</div>}
</select> <Field label="Version" htmlFor="version-version">
</div> <input className="input" id="version-version" name="version" value={form.version} onChange={set('version')} placeholder="z. B. v6" required />
<div className="form-group"> </Field>
<label className="form-label">Gültig ab</label> <Field label="Status" htmlFor="version-status">
<input className="form-control" type="date" name="validFrom" value={form.validFrom} onChange={set('validFrom')} required /> <select className="select" id="version-status" name="status" value={form.status} onChange={set('status')}>
</div> <option value="current">Gültig</option>
<div className="form-group"> <option value="draft">Entwurf (noch nicht live)</option>
<label className="form-label">Was hat sich geändert?</label> </select>
<textarea className="form-control" name="changeNote" rows={4} value={form.changeNote} onChange={set('changeNote')} /> </Field>
</div> <Field label="Gültig ab" htmlFor="version-validFrom">
<div className="form-group"> <input className="input" id="version-validFrom" type="date" name="validFrom" value={form.validFrom} onChange={set('validFrom')} required />
<label className="form-label">Dateien (PDF, HTML, Bild, Word)</label> </Field>
<input type="file" multiple accept=".pdf,.html,.htm,.png,.jpg,.jpeg,.webp,.doc,.docx" onChange={(e) => setFiles([...e.target.files])} /> <Field label="Was hat sich geändert?" htmlFor="version-changeNote">
</div> <textarea className="textarea" id="version-changeNote" name="changeNote" rows={4} value={form.changeNote} onChange={set('changeNote')} />
<div className="flex gap-2"> </Field>
<Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button> <Field label="Dateien (PDF, HTML, Bild, Word)" htmlFor="version-files">
<Button type="button" onClick={onClose}>Abbrechen</Button> <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> </Field>
</form> </Dialog>
</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 */