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 { FaTimes } from 'react-icons/fa'
import { Button } from '../ui'
import { useRef, useState } from 'react'
import { useScrollIntoView } from '../../hooks/useScrollIntoView'
import { Button, Dialog, Field } from '../ui'
const FIELDS = ['provider', 'purpose', 'cost', 'costInterval', 'startDate', 'termMonths', 'renewalMonths', 'noticeValue', 'noticeUnit', 'note']
const NUMBERS = ['termMonths', 'renewalMonths', 'noticeValue']
@@ -31,6 +31,8 @@ export default function ContractForm({ contract, onSave, onClose }) {
const [files, setFiles] = useState([])
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const errorRef = useRef(null)
useScrollIntoView(errorRef, error)
const set = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }))
const submit = async (e) => {
@@ -47,60 +49,59 @@ export default function ContractForm({ contract, onSave, onClose }) {
}
const input = (key, label, props = {}) => (
<div className="form-group">
<label className="form-label">{label}</label>
<input className="form-control" name={key} value={form[key] || ''} onChange={set(key)} {...props} />
</div>
<Field label={label} htmlFor={`contract-${key}`}>
<input className="input" id={`contract-${key}`} name={key} value={form[key] || ''} onChange={set(key)} {...props} />
</Field>
)
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}><FaTimes /></span>
<div className="overlay-content">
<h2 className="mb-2">{contract ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}</h2>
{error && <div className="legal-error">{error}</div>}
<form onSubmit={submit}>
<Dialog
as="form"
wide
title={contract ? 'Vertrag bearbeiten' : 'Neuer Vertrag'}
busy={saving}
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('purpose', 'Wofür')}
<div className="legal-form-row">
<div className="field-row">
{input('cost', 'Kosten (€)', { inputMode: 'decimal' })}
<div className="form-group">
<label className="form-label">pro</label>
<select className="form-control" name="costInterval" value={form.costInterval} onChange={set('costInterval')}>
<Field label="pro" htmlFor="contract-costInterval">
<select className="select" id="contract-costInterval" name="costInterval" value={form.costInterval} onChange={set('costInterval')}>
<option value="month">Monat</option>
<option value="year">Jahr</option>
</select>
</div>
</Field>
</div>
{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('renewalMonths', 'Verlängert sich um (Monate, 0 = endet)', { type: 'number', min: 0 })}
</div>
<div className="legal-form-row">
<div className="field-row">
{input('noticeValue', 'Kündigungsfrist', { type: 'number', min: 0 })}
<div className="form-group">
<label className="form-label">Einheit</label>
<select className="form-control" name="noticeUnit" value={form.noticeUnit} onChange={set('noticeUnit')}>
<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>
</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>
<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>
)
}
+36 -36
View File
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { Button } from '../ui'
import { useRef, useState } from 'react'
import { useScrollIntoView } from '../../hooks/useScrollIntoView'
import { Button, Dialog, Field } from '../ui'
import { todayIso } from './legalUi'
export default function VersionForm({ title, onSave, onClose }) {
@@ -8,6 +8,8 @@ export default function VersionForm({ title, onSave, onClose }) {
const [files, setFiles] = useState([])
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const errorRef = useRef(null)
useScrollIntoView(errorRef, error)
const set = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }))
const submit = async (e) => {
@@ -24,41 +26,39 @@ export default function VersionForm({ title, onSave, onClose }) {
}
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}><FaTimes /></span>
<div className="overlay-content">
<h2 className="mb-2">Neue Version: {title}</h2>
{error && <div className="legal-error">{error}</div>}
<form onSubmit={submit}>
<div className="form-group">
<label className="form-label">Version</label>
<input className="form-control" name="version" value={form.version} onChange={set('version')} placeholder="z. B. v6" required />
</div>
<div className="form-group">
<label className="form-label">Status</label>
<select className="form-control" name="status" value={form.status} onChange={set('status')}>
<Dialog
as="form"
title={`Neue Version: ${title}`}
busy={saving}
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>}
<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="draft">Entwurf (noch nicht live)</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Gültig ab</label>
<input className="form-control" type="date" name="validFrom" value={form.validFrom} onChange={set('validFrom')} required />
</div>
<div className="form-group">
<label className="form-label">Was hat sich geändert?</label>
<textarea className="form-control" name="changeNote" rows={4} value={form.changeNote} onChange={set('changeNote')} />
</div>
<div className="form-group">
<label className="form-label">Dateien (PDF, HTML, Bild, Word)</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>
</Field>
<Field label="Gültig ab" htmlFor="version-validFrom">
<input className="input" id="version-validFrom" type="date" name="validFrom" value={form.validFrom} onChange={set('validFrom')} required />
</Field>
<Field label="Was hat sich geändert?" htmlFor="version-changeNote">
<textarea className="textarea" id="version-changeNote" name="changeNote" rows={4} value={form.changeNote} onChange={set('changeNote')} />
</Field>
<Field label="Dateien (PDF, HTML, Bild, Word)" htmlFor="version-files">
<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])} />
</Field>
</Dialog>
)
}
+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-msg { margin: 0 0 12px; }
.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-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.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-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 .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 */