diff --git a/CLAUDE.md b/CLAUDE.md index db95548..8982617 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,45 @@ MVC: `src/Controllers/`, `src/Models/`, `src/Services/`, `src/Views/`. - Neue Libraries nur nach expliziter Rückfrage einbinden. - Bestehenden Code-Stil beibehalten (declare strict_types, 4 Spaces). - SQL ausschließlich über `Database::fetchAll/fetchOne/execute()` mit Prepared Statements. +- **Keine neuen `style="`-Attribute außer für Laufzeitwerte** (siehe unten). + +## CSS-Klassen statt Inline-Styles (Issue #160) + +`public/static/css/app.css` ist das zentrale Stylesheet. Wiederkehrende Muster +gehören dorthin, nicht als `style="..."` in die Views – sonst ist weder eine +CSP ohne `unsafe-inline` noch Theming möglich, und `tests/97_InlineStyleBudgetTest.php` +schlägt fehl, sobald das Budget überschritten wird. + +**Regel:** Inline-Styles nur, wenn der Wert tatsächlich zur Laufzeit aus PHP +kommt (Kontofarbe, Logo-Maße aus den Einstellungen o. ä.) – und auch dann +bevorzugt als CSS-Variable statt vollständiger Deklaration: + +```html + + + + +``` + +Wichtige vorhandene Klassen (Auswahl, siehe `app.css` für die vollständige Liste): + +| Klasse | Zweck | +|---|---| +| `.modal-overlay` (+ `.open`, `--scroll`, `--stacked`) | Dialog-Hintergrund, Toggle über `.open`-Klasse oder direktes `style.display` | +| `.modal` / `.modal-card` (+ `-invoice`, `--lg`) | Dialog-Box in zwei etablierten Größen/Stilen | +| `.modal-card-header` / `.modal-card-close` | Titelzeile mit Schließen-Button im Dialog | +| `.icon-btn` | Rahmenloser Icon-Button (SVG-Icon, `--icon-color` für Laufzeitfarbe) | +| `.btn-round-icon` | Runder Icon-Button (z. B. „Position hinzufügen“) | +| `.row-clickable` + `data-row-href` | Klickbare Tabellenzeile (zentrale Delegation in `app.js`), `data-no-row-click` nimmt einzelne Zellen aus | +| `.alert` / `.alert-warning` / `.alert-info` | Hinweisboxen | +| `.text-muted`, `.text-sm`, `.text-danger`, `.nowrap`, `.min-w-0`, `.m-0`, `.w-full`, `.hidden` | Utilities für einzelne Eigenschaften | + +Inline-Handler (`onclick=`, `onchange=`, `oninput=`) sind in `src/Views` nicht +mehr erlaubt (Test 97 prüft das). Statt dessen: Element bekommt eine `id` +oder ein `data-action`-Attribut, die Verdrahtung passiert per +`addEventListener()` im zugehörigen Seiten-Skript oder – bei generischen, +seitenübergreifenden Mustern (z. B. `data-modal-open`/`data-modal-close`, +`data-row-href`) – zentral in `app.js`. ## Tests **Regel: Jedes neue Feature bekommt einen eigenen Test.** diff --git a/Changes.md b/Changes.md index d2a47be..f85a534 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,18 @@ --- +## 2026-08-18 – Inline-Styles und Inline-Handler reduziert (#160) + +Erster Umsetzungsschritt der in Issue #160 beschriebenen schrittweisen Reduktion (975 → 858 `style="`-Vorkommen, 51 → 0 Inline-Handler in `src/Views`). Kein Big-Bang über alle Views, sondern die im Ticket priorisierten Schritte 1–4: + +- **`public/static/css/app.css`:** neue wiederverwendbare Klassen für die häufigsten Muster – `.modal-overlay`/`.modal-card` (Dialog-Overlay + -Karte), `.icon-btn` (17 identische Icon-Button-Deklarationen quer über 11 Views), `.badge-lg`, `.alert`/`.alert-warning`/`.alert-info`, `.row-clickable`/`data-row-href` (klickbare Tabellenzeilen), `.btn-round-icon`, `.autosize-textarea`, diverse `invoice-*`- und `booking-table`-Klassen für die Rechnungsvorschau/Zahlungsübersicht sowie kleinere Utilities (`.nowrap`, `.min-w-0`, `.m-0`, `.text-danger`, `.text-faint` …). Laufzeitfarben (Kontofarbe, Icon-Warnfarbe, Logo-Maße) bleiben als CSS-Variable im `style`-Attribut, wie im Ticket als Zielbild vorgegeben. +- **Modal-Overlay vereinheitlicht:** alle 12 Views mit Dialogen (`bank/_modal_*.php`, `belege/_crop_modal.php`, `belege/_anlage_modal.php`, `belege/_vorschau.php`, `assets/form.php`, `assets/list.php`, `buchungsvorlagen/modal.php`, `invoices/show.php`) nutzen jetzt `.modal-overlay`/`.modal-card` statt ausgeschriebener Inline-Styles je Vorkommen. +- **`invoices/show.php`** (126 → 3 Inline-Styles, `bank/fints.php` und `belege/detail.php` lagen durch die Aufteilung aus #157/#161 bereits bei 0): Rechnungsvorschau-Tabellen, Zahlungs-Modal und Toolbar auf Klassen umgestellt; die beiden verbleibenden Modale (E-Mail senden, Zahlungen) laufen jetzt über das bestehende `data-modal-open`/`data-modal-close`-Muster aus `app.js` statt über `onclick`. +- **53 Inline-Handler entfernt:** `onclick=`/`onchange=`/`oninput=` in `src/Views` durch `id`-/`data-*`-Attribute plus zentrale `addEventListener()`-Verdrahtung ersetzt – wo die Zielfunktion bereits Seiten-JS ist (z. B. `beleg-form.js`, `beleg-ui.js`, `beleg-crop.js`, `fints.js`), direkt dort; für generische, seitenübergreifende Muster (klickbare Tabellenzeile, Rechnungspositionen entfernen/hinzufügen, Buchungsvorlagen-Zeilen inkl. dynamisch per JS erzeugter Zeilen) zentrale `data-action`-Dispatcher in `app.js` bzw. im jeweiligen View-Script. Verhalten unverändert, keine Funktionsänderung. +- **`tests/97_InlineStyleBudgetTest.php`** (neu): zählt `style="`-Vorkommen unter `src/Views` und schlägt bei Überschreitung des Budgets (aktuell 900) fehl; prüft zusätzlich, dass keine `onclick=`/`onchange=`/`oninput=`-Attribute mehr vorkommen. Verhindert, dass neue Views den Zähler wieder hochtreiben. +- Restliche ~600 Inline-Styles in weiteren Views (`tax/euer_show.php`, `buchungsvorlagen/form.php`, `assets/form.php`, `settings/form.php`, `bank/upload.php` u. a.) sind laut Ticket bewusst nicht Teil dieses Schritts – die Reduktion ist als mehrstufiger Prozess angelegt, ein Rundumschlag über alle Views wäre nicht reviewbar gewesen. +- Manuelle Abnahme der Optik war mangels Browser in dieser Umgebung nicht möglich; die Umstellung ist als mechanische 1:1-Übertragung (identische Werte, keine neuen Layoutentscheidungen) ausgeführt, um das Risiko sichtbarer Änderungen zu minimieren. + ## 2026-08-18 – Restposten: O-CHAT.html, fints.php/bankimport.php, Request-Abstraktion (#161) - **(a) `public/O-CHAT.html`:** Testartefakt einer eigenständigen Ollama-Chat-Oberfläche wurde von Apache direkt ausgeliefert (`.htaccess` routet nur nicht-existierende Dateien auf `index.php`) und lief damit an der Authentifizierung vorbei. Nach `BEISPIELE/O-CHAT.html` verschoben (gitignored, wird nicht ausgeliefert). **`tests/93_WebrootSauberkeitTest.php`** (neu) hält `public/` dauerhaft sauber. diff --git a/public/static/css/app.css b/public/static/css/app.css index 5408a7b..688c58f 100644 --- a/public/static/css/app.css +++ b/public/static/css/app.css @@ -564,6 +564,27 @@ td { padding: .65rem 1rem; vertical-align: middle; } gap: 1rem; } +.invoice-address-block { margin-top: 1cm; margin-bottom: 1.5rem; } + +.invoice-sender-line { + font-size: 6pt; + color: #666; + border-bottom: 1px solid #ccc; + padding-bottom: 2px; + margin-bottom: 12px; +} + +.invoice-recipient-block { line-height: 1.5; font-size: 10pt; } + +.invoice-title-block { text-align: right; font-size: 17pt; } +.invoice-title-ref { font-size: .65rem; margin-top: .3rem; font-weight: normal; } + +.invoice-logo-wrap { margin-bottom: 1.5rem; } +.invoice-logo-wrap--left { text-align: left; margin-left: var(--logo-mside); margin-top: var(--logo-mtop); } +.invoice-logo-wrap--right { text-align: right; margin-right: var(--logo-mside); margin-top: var(--logo-mtop); } +.invoice-logo-wrap--center { text-align: center; } +.invoice-logo-wrap img { max-width: var(--logo-maxw); max-height: var(--logo-maxh); } + @media (max-width: 1059px) { .invoice-address { grid-template-columns: 1fr; @@ -582,13 +603,19 @@ td { padding: .65rem 1rem; vertical-align: middle; } position: fixed; inset: 0; background: rgba(0,0,0,.45); - z-index: 100; + z-index: 1000; justify-content: center; align-items: center; } .modal-overlay.open { display: flex; } +/* Modale mit größerem Inhalt (z.B. Buchungsformular): oben ausgerichtet, scrollbar */ +.modal-overlay--scroll { align-items: flex-start; padding: 2rem 1rem; overflow-y: auto; } + +/* Modale, die über einem anderen Modal liegen (z.B. TAN-Abfrage über Abruf-Log) */ +.modal-overlay--stacked { z-index: 1100; } + .modal { background: var(--color-surface); border-radius: var(--radius); @@ -601,6 +628,182 @@ td { padding: .65rem 1rem; vertical-align: middle; } .modal-title { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; } .modal-actions { display: flex; gap: .75rem; justify-content: flex-end; margin-top: 1.25rem; } +/* Kompakte Dialog-Karte (Bank-Verbindungsdialoge, TAN-Eingabe, Abruf-Log) */ +.modal-card { + background: var(--color-surface); + border-radius: var(--radius); + box-shadow: 0 8px 32px rgba(0,0,0,.15); + width: 100%; + max-width: 480px; + padding: 1.5rem; +} + +.modal-card--lg { max-width: 680px; } + +/* Dialog-Karte mit fester Breite statt max-width (Rechnungs-Modale) */ +.modal-card-invoice { + background: #fff; + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,.2); +} +.modal-card-invoice--sm { width: 420px; max-width: 95vw; padding: 1.5rem; } +.modal-card-invoice--lg { width: 100%; max-width: 720px; } +.modal-form-title { margin: 0 0 1rem; } + +.modal-section { padding: 1rem 1.25rem; } +.modal-section-border { border-bottom: 1px solid #e5e7eb; } + +.modal-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.25rem; +} + +.modal-card h2 { margin: 0; font-size: 1.125rem; } + +.modal-card-close { + background: none; + border: none; + font-size: 1.25rem; + cursor: pointer; + color: var(--color-muted); +} + +.modal-card-close--lg { font-size: 1.5rem; color: #6b7280; } + +/* ── Badges mit Laufzeitfarbe ────────────────────────────────── */ +.badge-color { + background: var(--badge-bg, var(--color-primary)); + color: var(--badge-fg, #fff); +} + +/* ── Häufige Layout-Muster ───────────────────────────────────── */ +.row-between { display: flex; justify-content: space-between; align-items: center; } +.row-end { display: flex; justify-content: flex-end; gap: .5rem; } +.row-wrap { display: flex; flex-wrap: wrap; gap: .5rem; } +.col { display: flex; flex-direction: column; gap: .75rem; } +.text-danger { color: var(--color-danger); } +.text-faint { color: #9ca3af; } + +.icon-btn { + background: none; + border: none; + cursor: pointer; + color: var(--icon-color, var(--color-muted)); + padding: .2rem; + line-height: 1; + display: inline-flex; + align-items: center; +} + +.badge-lg { font-size: .7rem; padding: .3rem .8rem; } + +.alert { + margin-bottom: .75rem; + padding: .6rem 1rem; + font-size: .8rem; + border-left: 3px solid; +} +.alert-warning { background: #fff3cd; border-left-color: #f59e0b; } +.alert-info { background: #e0f2fe; border-left-color: #0284c7; } +.xml-hinweis-list { margin: .2rem 0 0 1.2rem; } + +/* ── Rechnungsvorschau: Tabellen (DIN 5008) ─────────────────────── */ +.invoice-meta-table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + margin-top: 2cm; + margin-bottom: 1.5rem; + font-size: .7rem; +} +.invoice-meta-table th, +.invoice-meta-table td { width: 20%; padding: .4rem .5rem; text-align: left; } +.invoice-meta-table th { font-weight: bold; } +.invoice-meta-table th:last-child, +.invoice-meta-table td:last-child { text-align: right; } + +#invoice-pos-table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + margin-bottom: 1.5rem; + font-size: .7rem; +} +#invoice-pos-table thead tr { background: #f0f0f0; } +#invoice-pos-table th { padding: .5rem; text-align: left; } +#invoice-pos-table td { padding: .4rem .5rem; } +#invoice-pos-table tbody tr { border-bottom: 1px solid #eee; } +#invoice-pos-table th:nth-child(1) { width: 6%; } +#invoice-pos-table th:nth-child(3) { width: 11%; text-align: right; } +#invoice-pos-table th:nth-child(4) { width: 10%; } +#invoice-pos-table th:nth-child(5) { width: 8%; text-align: right; } +#invoice-pos-table th:nth-child(6) { width: 15%; text-align: right; } +#invoice-pos-table th:nth-child(7) { width: 16%; text-align: right; } +#invoice-pos-table td:nth-child(3), +#invoice-pos-table td:nth-child(5), +#invoice-pos-table td:nth-child(6), +#invoice-pos-table td:nth-child(7) { text-align: right; } +#invoice-pos-table .row-heading td { font-weight: 700; } +#invoice-pos-table .col-total { font-weight: 600; } + +.invoice-summary-table { width: auto; } +.invoice-summary-table td { padding: .3rem 0; } +.invoice-summary-table td:last-child { text-align: right; width: 7.5em; } +.invoice-summary-table .row-total { font-weight: 700; font-size: .82rem; } +.invoice-summary-table .row-total td { padding: .5rem 0; } + +.booking-table { width: 100%; font-size: .75rem; border-collapse: collapse; } +.booking-table thead tr { background: #f0f0f0; } +.booking-table th { padding: .4rem .5rem; text-align: left; } +.booking-table td { padding: .35rem .5rem; } +.booking-table tbody tr { border-bottom: 1px solid #eee; } +.booking-table tbody tr.is-storno { opacity: .5; background: #fff5f5; } +.booking-table tfoot tr:first-child { border-top: 2px solid #e5e7eb; } +.booking-table .text-mono { font-family: var(--font-mono); } +.booking-table .num-right { text-align: right; } +.booking-table .cell-sub { font-size: .7rem; } +.booking-table .cell-storno-tag { font-size: .65rem; } +.booking-table .cell-empty { padding: 1rem; } + +.invoice-summary-wrap { display: flex; justify-content: flex-end; margin-bottom: 1.5rem; padding-right: .5rem; } +.invoice-payment-hint { + background: #f0f4ff; + border-left: 3px solid #2563eb; + padding: .75rem 1rem; + margin-bottom: 2rem; + font-size: .7rem; +} + +.invoice-footer-grid { + border-top: 1px solid #ccc; + padding-top: .75rem; + display: grid; + grid-template-columns: 1fr 1fr 1fr auto; + gap: 1rem; + font-size: .58rem; + color: #555; +} + +.booking-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem .75rem; margin-bottom: .75rem; } +.booking-form-field { display: flex; align-items: center; gap: .4rem; } +.booking-form-field label { font-size: .75rem; font-weight: 600; white-space: nowrap; margin: 0; } +.booking-form-title { font-size: .8rem; font-weight: 600; margin-bottom: .75rem; } + +.log-console { + background: #0f172a; + color: #e2e8f0; + font-family: var(--font-mono); + font-size: .8rem; + padding: 1rem; + border-radius: var(--radius); + height: 340px; + overflow-y: auto; + white-space: pre-wrap; + line-height: 1.5; +} + /* ── Pagination ──────────────────────────────────────────────── */ .pagination { display: flex; @@ -621,6 +824,7 @@ td { padding: .65rem 1rem; vertical-align: middle; } .pagination .active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); } /* ── Utilities ───────────────────────────────────────────────── */ +.m-0 { margin: 0; } .text-muted { color: var(--color-muted); } .text-right { text-align: right; } .text-center { text-align: center; } @@ -640,7 +844,25 @@ td { padding: .65rem 1rem; vertical-align: middle; } .align-center { align-items: center; } .justify-between { justify-content: space-between; } .w-full { width: 100%; } +.min-w-0 { min-width: 0; } +.select-filter { width: auto; min-width: 11rem; } +.autosize-textarea { height: 2.4em; resize: none; overflow: hidden; } +.beleg-row { cursor: pointer; } +.beleg-row--static { cursor: default; } +.row-clickable { cursor: pointer; } +.btn-round-icon { + display: inline-flex; + align-items: center; + justify-content: center; + padding: .36rem; + border-radius: 50%; + width: 1.8rem; + height: 1.8rem; +} +.bl-supplier-cell { cursor: text; min-width: 80px; } +.mt-xs { margin-top: .2em; } .hidden { display: none; } +.nowrap { white-space: nowrap; } /* ── Print ───────────────────────────────────────────────────── */ @media print { diff --git a/public/static/js/app.js b/public/static/js/app.js index 6cd6f03..c5ff1cc 100644 --- a/public/static/js/app.js +++ b/public/static/js/app.js @@ -73,6 +73,13 @@ document.addEventListener('keydown', (e) => { } }); +// ── Klickbare Tabellenzeilen (Navigation) ────────────────────── +document.addEventListener('click', (e) => { + const row = e.target.closest('tr[data-row-href]'); + if (!row || e.target.closest('[data-no-row-click]')) return; + window.location = row.dataset.rowHref; +}); + // ── Confirm Delete ──────────────────────────────────────────── document.addEventListener('submit', (e) => { const form = e.target; @@ -139,6 +146,17 @@ document.addEventListener('input', (e) => { // ── Rechnungszeile / Überschrift hinzufügen ─────────────────── +document.addEventListener('click', (e) => { + const el = e.target.closest('[data-action]'); + if (!el) return; + switch (el.dataset.action) { + case 'show-add-line-dialog': showAddLineDialog(el); break; + case 'add-invoice-line': document.getElementById('add-line-popup')?.remove(); addInvoiceLine(); break; + case 'add-invoice-heading': document.getElementById('add-line-popup')?.remove(); addInvoiceHeading(); break; + case 'remove-invoice-line': removeInvoiceLine(el); break; + } +}); + function showAddLineDialog(btn) { document.getElementById('add-line-popup')?.remove(); @@ -146,8 +164,8 @@ function showAddLineDialog(btn) { popup.id = 'add-line-popup'; popup.style.cssText = 'position:absolute;z-index:200;background:#fff;border:1px solid #ccc;border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.12);overflow:hidden;font-size:.875rem'; popup.innerHTML = ` - - `; + + `; document.body.appendChild(popup); const r = btn.getBoundingClientRect(); @@ -205,7 +223,7 @@ function addInvoiceLine() {