Design: Anbieter-Vertraege als Tabelle, Dringlichkeit nur im Terminfeld
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f5013aafc
commit
a9fa9f58e6
2 files changed
+62
-64
No files matched your search
@@ -1,39 +1,12 @@
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Button, EmptyState } from '../ui'
|
||||
import { Badge, Button, EmptyState } from '../ui'
|
||||
import { deadlineTone, formatDate, nextCancellationDate } from '../../lib/legal'
|
||||
import { costLabel, termLabel, noticeLabel, deadlineView } from '../../lib/legalView'
|
||||
import { isRowActivation } from '../../lib/rowClick'
|
||||
import ContractForm from './ContractForm'
|
||||
import { FileLinks, todayIso } from './legalUi'
|
||||
|
||||
const TONE_ORDER = { red: 0, orange: 1, normal: 2, missing: 3, grey: 4 }
|
||||
const euro = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' })
|
||||
|
||||
function costLabel(c) {
|
||||
if (c.cost === null || c.cost === undefined) return '–'
|
||||
return `${euro.format(c.cost)}/${c.costInterval === 'year' ? 'Jahr' : 'Monat'}`
|
||||
}
|
||||
|
||||
function termLabel(c) {
|
||||
if (c.termMonths === null || c.termMonths === undefined) return '–'
|
||||
if (c.termMonths === 0) return 'jederzeit kündbar'
|
||||
const renew = c.renewalMonths ? `, dann je ${c.renewalMonths} Monate` : ', endet danach'
|
||||
return `${c.termMonths} Monate${renew}`
|
||||
}
|
||||
|
||||
function noticeLabel(c) {
|
||||
if (c.noticeValue === null || c.noticeValue === undefined) return '–'
|
||||
return `${c.noticeValue} ${c.noticeUnit === 'days' ? 'Tage' : 'Monate'}`
|
||||
}
|
||||
|
||||
function deadlineLabel(r) {
|
||||
switch (r.state) {
|
||||
case 'active': return `kündbar bis ${formatDate(r.noticeBy)} (Periode endet ${formatDate(r.periodEnd)})`
|
||||
case 'anytime': return 'jederzeit kündbar'
|
||||
case 'ends': return `endet am ${formatDate(r.periodEnd)}`
|
||||
case 'ended': return `beendet seit ${formatDate(r.periodEnd)}`
|
||||
case 'cancelled': return r.periodEnd ? `gekündigt zum ${formatDate(r.periodEnd)}` : 'gekündigt'
|
||||
default: return 'Fristen fehlen'
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProviderContractsTab({ contracts, fileNames, saveContract, deleteContract }) {
|
||||
const [open, setOpen] = useState(null)
|
||||
@@ -77,55 +50,83 @@ export default function ProviderContractsTab({ contracts, fileNames, saveContrac
|
||||
<div className="toolbar">
|
||||
<Button variant="primary" onClick={() => setEditing('new')}>Neuer Vertrag</Button>
|
||||
</div>
|
||||
{actionError && <div className="legal-error">{actionError}</div>}
|
||||
{actionError && <div className="msg err legal-msg" role="alert">{actionError}</div>}
|
||||
{rows.length === 0 ? (
|
||||
<EmptyState title="Noch keine Verträge eingetragen" />
|
||||
) : (
|
||||
<table className="ui-table legal-contracts">
|
||||
<table className="table legal-contracts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Anbieter</th><th>Wofür</th><th>Kosten</th><th>Laufzeit</th><th>Kündigungsfrist</th><th>Nächster Termin</th>
|
||||
<th>Anbieter</th>
|
||||
<th className="hide-sm">Wofür</th>
|
||||
<th className="hide-sm">Kosten</th>
|
||||
<th className="hide-sm">Laufzeit</th>
|
||||
<th className="hide-sm">Kündigungsfrist</th>
|
||||
<th>Nächster Termin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(({ c, result, tone }) => (
|
||||
{rows.map(({ c, result }) => {
|
||||
const view = deadlineView(result, today)
|
||||
const toggle = () => setOpen(open === c.$id ? null : c.$id)
|
||||
return (
|
||||
<Fragment key={c.$id}>
|
||||
<tr className={`legal-tone-${tone}`} onClick={() => setOpen(open === c.$id ? null : c.$id)}>
|
||||
<td><strong>{c.provider}</strong></td>
|
||||
<td>{c.purpose || '–'}</td>
|
||||
<td>{costLabel(c)}</td>
|
||||
<td>{termLabel(c)}</td>
|
||||
<td>{noticeLabel(c)}</td>
|
||||
<td className="legal-deadline">{deadlineLabel(result)}</td>
|
||||
<tr className="row" onClick={(e) => { if (isRowActivation(e)) toggle() }}>
|
||||
<td>
|
||||
<div className="cell-title">
|
||||
<button type="button" className="link" aria-expanded={open === c.$id} onClick={toggle}>{c.provider}</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hide-sm">{c.purpose || '–'}</td>
|
||||
<td className="hide-sm">{costLabel(c)}</td>
|
||||
<td className="hide-sm">{termLabel(c)}</td>
|
||||
<td className="hide-sm">{noticeLabel(c)}</td>
|
||||
<td>
|
||||
{view.badge ? (
|
||||
<>
|
||||
<Badge tone={view.badge}>{view.lead}</Badge>
|
||||
<span className="muted"> {view.rest}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className={view.muted ? 'muted' : undefined}>{view.text}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{open === c.$id && (
|
||||
<tr className="legal-detail">
|
||||
<td colSpan={6}>
|
||||
<dl className="kv legal-facts">
|
||||
<dt>Wofür</dt><dd>{c.purpose || '–'}</dd>
|
||||
<dt>Kosten</dt><dd>{costLabel(c)}</dd>
|
||||
<dt>Laufzeit</dt><dd>{termLabel(c)}</dd>
|
||||
<dt>Kündigungsfrist</dt><dd>{noticeLabel(c)}</dd>
|
||||
</dl>
|
||||
{c.startDate && <div>Beginn: {formatDate(c.startDate)}</div>}
|
||||
{c.note && <div className="legal-note">{c.note}</div>}
|
||||
<div><FileLinks fileIds={c.fileIds} fileNames={fileNames} /></div>
|
||||
<div className="flex gap-2 legal-actions">
|
||||
<Button size="sm" onClick={() => setEditing(c)}>Bearbeiten</Button>
|
||||
<div className="legal-actions">
|
||||
<button type="button" className="link" onClick={() => setEditing(c)}>Bearbeiten</button>
|
||||
{!c.cancelledAt && cancelling !== c.$id && (
|
||||
<Button size="sm" onClick={() => { setCancelling(c.$id); setEndsAt(result.periodEnd || '') }}>
|
||||
<button type="button" className="link" onClick={() => { setCancelling(c.$id); setEndsAt(result.periodEnd || '') }}>
|
||||
Als gekündigt markieren
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
{cancelling === c.$id && (
|
||||
<span className="flex gap-2 items-center">
|
||||
<label>Endet am</label>
|
||||
<input type="date" className="form-control" value={endsAt} onChange={(e) => setEndsAt(e.target.value)} />
|
||||
<Button size="sm" variant="primary" onClick={() => confirmCancel(c)}>Speichern</Button>
|
||||
<Button size="sm" onClick={() => setCancelling(null)}>Abbrechen</Button>
|
||||
<span className="legal-cancel">
|
||||
<label htmlFor={`cancel-${c.$id}`}>Endet am</label>
|
||||
<input id={`cancel-${c.$id}`} type="date" className="input" value={endsAt} onChange={(e) => setEndsAt(e.target.value)} />
|
||||
<Button variant="primary" onClick={() => confirmCancel(c)}>Speichern</Button>
|
||||
<Button onClick={() => setCancelling(null)}>Abbrechen</Button>
|
||||
</span>
|
||||
)}
|
||||
<Button size="sm" variant="danger" onClick={() => remove(c)}>Löschen</Button>
|
||||
<Button variant="danger" onClick={() => remove(c)}>Löschen</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
+9
-12
@@ -618,18 +618,15 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
|
||||
.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-contracts { margin-top: var(--sp-2); }
|
||||
.legal-contracts tbody tr { cursor: pointer; }
|
||||
.legal-tone-red td { background: rgba(239, 68, 68, 0.10); }
|
||||
.legal-tone-red .legal-deadline { color: var(--danger); font-weight: 700; }
|
||||
.legal-tone-orange td { background: rgba(245, 158, 11, 0.10); }
|
||||
.legal-tone-orange .legal-deadline { color: var(--orange-fg); font-weight: 600; }
|
||||
.legal-tone-missing .legal-deadline { color: var(--text-faint); font-style: italic; }
|
||||
.legal-tone-grey td { color: var(--text-faint); }
|
||||
.legal-detail td { background: var(--surface-2); cursor: default; }
|
||||
.legal-actions { margin-top: 8px; flex-wrap: wrap; }
|
||||
@media (max-width: 700px) {
|
||||
.legal-form-row { grid-template-columns: 1fr; }
|
||||
.legal-contracts { display: block; overflow-x: auto; }
|
||||
.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-detail td { padding: 0 0 16px; }
|
||||
.legal-facts { display: none; margin-bottom: 8px; }
|
||||
.legal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 8px; }
|
||||
.legal-cancel { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.legal-cancel .input { width: 160px; }
|
||||
@media (max-width: 860px) {
|
||||
.legal-facts { display: grid; }
|
||||
}
|
||||
|
||||
/* ===== FINANZEN (P7) ===== */
|
||||
|
||||
Reference in new issue
Block a user