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:
1 parent
a9fa9f58e6
commit
84186d1429
3 files changed
+96
-95
No files matched your search
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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 */
|
||||||
|
|||||||
Reference in new issue
Block a user