Merge pull request 'Fix #160: Inline-Styles und Inline-Handler reduziert (Schritt 1)' (#192) from fds2610-bot/Steuerkram:main into main

Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/192
This commit is contained in:
fds2610 2026-08-19 08:43:02 +02:00
commit 98fc301195
32 changed files with 715 additions and 266 deletions

View file

@ -9,6 +9,45 @@ MVC: `src/Controllers/`, `src/Models/`, `src/Services/`, `src/Views/`.
- Neue Libraries nur nach expliziter Rückfrage einbinden. - Neue Libraries nur nach expliziter Rückfrage einbinden.
- Bestehenden Code-Stil beibehalten (declare strict_types, 4 Spaces). - Bestehenden Code-Stil beibehalten (declare strict_types, 4 Spaces).
- SQL ausschließlich über `Database::fetchAll/fetchOne/execute()` mit Prepared Statements. - 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
<!-- statt -->
<span style="background:<?= $h($k['farbe']) ?>;color:<?= $h($textfarbe) ?>">
<!-- besser -->
<span class="konto-badge" style="--badge-bg: <?= $h($k['farbe']) ?>">
```
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 ## Tests
**Regel: Jedes neue Feature bekommt einen eigenen Test.** **Regel: Jedes neue Feature bekommt einen eigenen Test.**

View file

@ -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) ## 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. - **(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.

View file

@ -564,6 +564,27 @@ td { padding: .65rem 1rem; vertical-align: middle; }
gap: 1rem; 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) { @media (max-width: 1059px) {
.invoice-address { .invoice-address {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -582,13 +603,19 @@ td { padding: .65rem 1rem; vertical-align: middle; }
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0,0,0,.45); background: rgba(0,0,0,.45);
z-index: 100; z-index: 1000;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.modal-overlay.open { display: flex; } .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 { .modal {
background: var(--color-surface); background: var(--color-surface);
border-radius: var(--radius); 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-title { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; }
.modal-actions { display: flex; gap: .75rem; justify-content: flex-end; margin-top: 1.25rem; } .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 ──────────────────────────────────────────────── */
.pagination { .pagination {
display: flex; 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); } .pagination .active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
/* ── Utilities ───────────────────────────────────────────────── */ /* ── Utilities ───────────────────────────────────────────────── */
.m-0 { margin: 0; }
.text-muted { color: var(--color-muted); } .text-muted { color: var(--color-muted); }
.text-right { text-align: right; } .text-right { text-align: right; }
.text-center { text-align: center; } .text-center { text-align: center; }
@ -640,7 +844,25 @@ td { padding: .65rem 1rem; vertical-align: middle; }
.align-center { align-items: center; } .align-center { align-items: center; }
.justify-between { justify-content: space-between; } .justify-between { justify-content: space-between; }
.w-full { width: 100%; } .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; } .hidden { display: none; }
.nowrap { white-space: nowrap; }
/* ── Print ───────────────────────────────────────────────────── */ /* ── Print ───────────────────────────────────────────────────── */
@media print { @media print {

View file

@ -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 ──────────────────────────────────────────── // ── Confirm Delete ────────────────────────────────────────────
document.addEventListener('submit', (e) => { document.addEventListener('submit', (e) => {
const form = e.target; const form = e.target;
@ -139,6 +146,17 @@ document.addEventListener('input', (e) => {
// ── Rechnungszeile / Überschrift hinzufügen ─────────────────── // ── 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) { function showAddLineDialog(btn) {
document.getElementById('add-line-popup')?.remove(); document.getElementById('add-line-popup')?.remove();
@ -146,8 +164,8 @@ function showAddLineDialog(btn) {
popup.id = 'add-line-popup'; 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.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 = ` popup.innerHTML = `
<button class="add-line-opt" onclick="document.getElementById('add-line-popup').remove();addInvoiceHeading()">Überschrift</button> <button class="add-line-opt" data-action="add-invoice-heading">Überschrift</button>
<button class="add-line-opt" onclick="document.getElementById('add-line-popup').remove();addInvoiceLine()">Neue Position</button>`; <button class="add-line-opt" data-action="add-invoice-line">Neue Position</button>`;
document.body.appendChild(popup); document.body.appendChild(popup);
const r = btn.getBoundingClientRect(); const r = btn.getBoundingClientRect();
@ -205,7 +223,7 @@ function addInvoiceLine() {
<td class="td-right line-netto text-muted">0,00 €</td> <td class="td-right line-netto text-muted">0,00 €</td>
<td class="td-right line-brutto font-bold">0,00 €</td> <td class="td-right line-brutto font-bold">0,00 €</td>
<td> <td>
<button type="button" class="btn btn-danger btn-sm" onclick="removeInvoiceLine(this)">✕</button> <button type="button" class="btn btn-danger btn-sm" data-action="remove-invoice-line">✕</button>
</td>`; </td>`;
tbody.appendChild(row); tbody.appendChild(row);
renumberLines(); renumberLines();
@ -445,6 +463,7 @@ function initBezTextarea(el) {
document.addEventListener('input', (e) => { document.addEventListener('input', (e) => {
if (e.target.classList.contains('line-bez-input')) initBezTextarea(e.target); if (e.target.classList.contains('line-bez-input')) initBezTextarea(e.target);
if (e.target.classList.contains('autosize-textarea')) initBezTextarea(e.target);
}); });
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {

View file

@ -149,4 +149,8 @@
overlay.addEventListener('click', function(e) { overlay.addEventListener('click', function(e) {
if (e.target === overlay) closeCropModal(); if (e.target === overlay) closeCropModal();
}); });
confirmBtn.addEventListener('click', function () { confirmCrop(); });
const cancelBtn = document.getElementById('crop-cancel-btn');
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeCropModal(); });
}()); }());

View file

@ -136,6 +136,67 @@ function updateEigenanteilState() {
if (!aktiv) cb.checked = false; if (!aktiv) cb.checked = false;
} }
// ── Info-Bar: Zuschneiden-Button, Metadaten-Formular ein-/ausblenden ──
const _cropOpenBtn = document.getElementById('beleg-crop-open-btn');
if (_cropOpenBtn) _cropOpenBtn.addEventListener('click', function () { openCropModal(); });
const _metaToggleBtn = document.getElementById('beleg-meta-toggle-btn');
if (_metaToggleBtn) {
_metaToggleBtn.addEventListener('click', function () {
const metaForm = document.getElementById('beleg-meta-form');
if (metaForm) metaForm.style.display = metaForm.style.display === 'none' ? 'block' : 'none';
});
}
const _metaCancelBtn = document.getElementById('beleg-meta-cancel-btn');
if (_metaCancelBtn) {
_metaCancelBtn.addEventListener('click', function () {
const metaForm = document.getElementById('beleg-meta-form');
if (metaForm) metaForm.style.display = 'none';
});
}
// ── Inline-Handler der Buchungszeile: zentrale Listener statt onchange/onclick/oninput ──
document.querySelectorAll('input[name="buchungstyp"]').forEach(function (radio) {
radio.addEventListener('change', function () { filterKategorieKonten(this.value); });
});
const _ustSatzSelect = document.getElementById('ust_satz_select');
if (_ustSatzSelect) {
_ustSatzSelect.addEventListener('change', function () {
updateSteuerartVisibility();
updateNettoAnzeige();
const hidden = document.getElementById('ust_satz_hidden');
if (hidden) hidden.value = this.value;
});
}
const _kategorieKontoSelect = document.getElementById('kategorie_konto_id');
if (_kategorieKontoSelect) {
_kategorieKontoSelect.addEventListener('change', function () {
showKontoArt('kategorie_konto_id', 'kategorie_konto_art', this);
updateEigenanteilState();
updateBewirtungsState();
updateAutoUst();
checkAnlagevermoegen();
});
}
const _zahlungskontoSelect = document.getElementById('zahlungskonto_id');
if (_zahlungskontoSelect) {
_zahlungskontoSelect.addEventListener('change', function () {
showKontoArt('zahlungskonto_id', 'zahlungskonto_art', this);
});
}
const _betragModusNettoBtn = document.getElementById('betrag_modus_netto_btn');
const _betragModusBruttoBtn = document.getElementById('betrag_modus_brutto_btn');
if (_betragModusNettoBtn) _betragModusNettoBtn.addEventListener('click', function () { setBetragModus('netto'); });
if (_betragModusBruttoBtn) _betragModusBruttoBtn.addEventListener('click', function () { setBetragModus('brutto'); });
const _betragNettoInput = document.getElementById('betrag_netto_input');
if (_betragNettoInput) _betragNettoInput.addEventListener('input', updateNettoAnzeige);
// Initial filtern // Initial filtern
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const checked = document.querySelector('input[name="buchungstyp"]:checked'); const checked = document.querySelector('input[name="buchungstyp"]:checked');

View file

@ -143,4 +143,11 @@ document.addEventListener('DOMContentLoaded', function() {
if (e.key === '-') { e.preventDefault(); imgZoom(-1); } if (e.key === '-') { e.preventDefault(); imgZoom(-1); }
if (e.key === '0') { e.preventDefault(); imgZoomReset(); } if (e.key === '0') { e.preventDefault(); imgZoomReset(); }
}); });
const zoomOutBtn = document.getElementById('img-zoom-out-btn');
const zoomInBtn = document.getElementById('img-zoom-in-btn');
const zoomResetBtn = document.getElementById('img-zoom-reset-btn');
if (zoomOutBtn) zoomOutBtn.addEventListener('click', function () { imgZoom(-1); });
if (zoomInBtn) zoomInBtn.addEventListener('click', function () { imgZoom(+1); });
if (zoomResetBtn) zoomResetBtn.addEventListener('click', function () { imgZoomReset(); });
}()); }());

View file

@ -85,8 +85,7 @@ $assetsJson = json_encode(array_map(fn($a) => [
<div style="display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:var(--color-muted)"> <div style="display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:var(--color-muted)">
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer"> <label style="display:flex;align-items:center;gap:.35rem;cursor:pointer">
<input type="checkbox" id="filter-unzugeordnet" <input type="checkbox" id="filter-unzugeordnet"
<?= $nurUnzugeordnet ? 'checked' : '' ?> <?= $nurUnzugeordnet ? 'checked' : '' ?>>
onchange="location.href='/assets?filter='+(this.checked?'unzugeordnet':'alle')">
Nur nicht zugeordnete anzeigen Nur nicht zugeordnete anzeigen
</label> </label>
</div> </div>
@ -215,6 +214,10 @@ $assetsJson = json_encode(array_map(fn($a) => [
</div> </div>
<script> <script>
document.getElementById('filter-unzugeordnet')?.addEventListener('change', function () {
location.href = '/assets?filter=' + (this.checked ? 'unzugeordnet' : 'alle');
});
const _anlageAssets = <?= $assetsJson ?>; const _anlageAssets = <?= $assetsJson ?>;
let _assignAccountId = 0; let _assignAccountId = 0;

View file

@ -1,12 +1,11 @@
<!-- Modal: Konto-Auswahl bei mehreren Konten --> <!-- Modal: Konto-Auswahl bei mehreren Konten -->
<div id="modal-account-select" <div id="modal-account-select" class="modal-overlay modal-overlay--stacked">
style="display:none;position:fixed;inset:0;z-index:1100;background:rgba(0,0,0,.4);align-items:center;justify-content:center"> <div class="modal-card col">
<div style="background:#fff;border-radius:var(--radius);box-shadow:0 8px 32px rgba(0,0,0,.15);width:100%;max-width:480px;padding:1.5rem;display:flex;flex-direction:column;gap:.75rem"> <h2>Konten auswählen</h2>
<h2 style="margin:0;font-size:1.125rem">Konten auswählen</h2>
<div class="text-muted text-sm">Wähle die Konten, die regelmäßig importiert werden sollen. Die Auswahl wird gespeichert und kann später im Bearbeiten-Dialog zurückgesetzt werden.</div> <div class="text-muted text-sm">Wähle die Konten, die regelmäßig importiert werden sollen. Die Auswahl wird gespeichert und kann später im Bearbeiten-Dialog zurückgesetzt werden.</div>
<div id="account-list" style="display:flex;flex-direction:column;gap:.4rem;max-height:280px;overflow-y:auto;padding:.25rem"></div> <div id="account-list" style="display:flex;flex-direction:column;gap:.4rem;max-height:280px;overflow-y:auto;padding:.25rem"></div>
<div id="account-error" class="text-sm" style="color:#dc2626;display:none"></div> <div id="account-error" class="text-sm text-danger hidden"></div>
<div style="display:flex;justify-content:flex-end;gap:.5rem"> <div class="row-end">
<button type="button" id="btn-account-cancel" class="btn btn-secondary">Abbrechen</button> <button type="button" id="btn-account-cancel" class="btn btn-secondary">Abbrechen</button>
<button type="button" id="btn-account-submit" class="btn btn-primary">Auswahl speichern</button> <button type="button" id="btn-account-submit" class="btn btn-primary">Auswahl speichern</button>
</div> </div>

View file

@ -2,13 +2,11 @@
<form id="form-edit-delete" method="post" action="" style="display:none"></form> <form id="form-edit-delete" method="post" action="" style="display:none"></form>
<!-- Modal: Verbindung bearbeiten --> <!-- Modal: Verbindung bearbeiten -->
<div id="modal-edit-connection" <div id="modal-edit-connection" class="modal-overlay">
style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.4);align-items:center;justify-content:center"> <div class="modal-inline-form modal-card">
<div class="modal-inline-form" style="background:#fff;border-radius:var(--radius);box-shadow:0 8px 32px rgba(0,0,0,.15);width:100%;max-width:480px;padding:1.5rem"> <div class="modal-card-header">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem"> <h2>Verbindung bearbeiten</h2>
<h2 style="margin:0;font-size:1.125rem">Verbindung bearbeiten</h2> <button type="button" id="btn-edit-close" class="modal-card-close">&times;</button>
<button type="button" id="btn-edit-close"
style="background:none;border:none;font-size:1.25rem;cursor:pointer;color:var(--color-muted)">&times;</button>
</div> </div>
<form id="form-edit-connection" method="post" action=""> <form id="form-edit-connection" method="post" action="">

View file

@ -1,11 +1,9 @@
<!-- Modal: Neue Verbindung --> <!-- Modal: Neue Verbindung -->
<div id="modal-new-connection" <div id="modal-new-connection" class="modal-overlay">
style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.4);align-items:center;justify-content:center"> <div class="modal-inline-form modal-card">
<div class="modal-inline-form" style="background:#fff;border-radius:var(--radius);box-shadow:0 8px 32px rgba(0,0,0,.15);width:100%;max-width:480px;padding:1.5rem"> <div class="modal-card-header">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem"> <h2>Neue Direktverbindung</h2>
<h2 style="margin:0;font-size:1.125rem">Neue Direktverbindung</h2> <button type="button" id="btn-modal-close" class="modal-card-close">&times;</button>
<button type="button" id="btn-modal-close"
style="background:none;border:none;font-size:1.25rem;cursor:pointer;color:var(--color-muted)">&times;</button>
</div> </div>
<form method="post" action="/bank/direkt"> <form method="post" action="/bank/direkt">

View file

@ -1,16 +1,13 @@
<!-- Modal: Abruf-Log --> <!-- Modal: Abruf-Log -->
<div id="modal-run-log" <div id="modal-run-log" class="modal-overlay">
style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.4);align-items:center;justify-content:center"> <div class="modal-card modal-card--lg col gap-2">
<div style="background:#fff;border-radius:var(--radius);box-shadow:0 8px 32px rgba(0,0,0,.15);width:100%;max-width:680px;padding:1.5rem;display:flex;flex-direction:column;gap:1rem"> <div class="row-between">
<div style="display:flex;justify-content:space-between;align-items:center"> <h2 id="run-log-title">Abruf läuft…</h2>
<h2 style="margin:0;font-size:1.125rem" id="run-log-title">Abruf läuft…</h2> <span id="run-log-spinner" class="text-sm text-muted">⏳ wird abgerufen…</span>
<span id="run-log-spinner" style="font-size:.8rem;color:var(--color-muted)">⏳ wird abgerufen…</span>
</div> </div>
<div id="run-log-area" <div id="run-log-area" class="log-console">
style="background:#0f172a;color:#e2e8f0;font-family:monospace;font-size:.8rem;
padding:1rem;border-radius:var(--radius);height:340px;overflow-y:auto;white-space:pre-wrap;line-height:1.5">
</div> </div>
<div style="display:flex;justify-content:flex-end"> <div class="row-end">
<button type="button" id="btn-run-close" class="btn btn-primary" disabled>Schließen</button> <button type="button" id="btn-run-close" class="btn btn-primary" disabled>Schließen</button>
</div> </div>
</div> </div>

View file

@ -1,8 +1,7 @@
<!-- Modal: TAN-Eingabe (photoTAN/chipTAN) --> <!-- Modal: TAN-Eingabe (photoTAN/chipTAN) -->
<div id="modal-tan-input" <div id="modal-tan-input" class="modal-overlay modal-overlay--stacked">
style="display:none;position:fixed;inset:0;z-index:1100;background:rgba(0,0,0,.4);align-items:center;justify-content:center"> <div class="modal-inline-form modal-card col">
<div class="modal-inline-form" style="background:#fff;border-radius:var(--radius);box-shadow:0 8px 32px rgba(0,0,0,.15);width:100%;max-width:480px;padding:1.5rem;display:flex;flex-direction:column;gap:.75rem"> <h2>TAN eingeben</h2>
<h2 style="margin:0;font-size:1.125rem">TAN eingeben</h2>
<div id="tan-context" class="text-muted text-sm"></div> <div id="tan-context" class="text-muted text-sm"></div>
<div id="tan-challenge" class="text-sm" style="white-space:pre-wrap"></div> <div id="tan-challenge" class="text-sm" style="white-space:pre-wrap"></div>
<div id="tan-image-wrap" style="text-align:center"></div> <div id="tan-image-wrap" style="text-align:center"></div>
@ -12,8 +11,8 @@
<input type="text" id="tan-input" autocomplete="off" inputmode="numeric" <input type="text" id="tan-input" autocomplete="off" inputmode="numeric"
style="font-size:1.25rem;letter-spacing:.25em;text-align:center"> style="font-size:1.25rem;letter-spacing:.25em;text-align:center">
</div> </div>
<div id="tan-error" class="text-sm" style="color:#dc2626;display:none"></div> <div id="tan-error" class="text-sm text-danger hidden"></div>
<div style="display:flex;justify-content:flex-end;gap:.5rem"> <div class="row-end">
<button type="button" id="btn-tan-cancel" class="btn btn-secondary">Abbrechen</button> <button type="button" id="btn-tan-cancel" class="btn btn-secondary">Abbrechen</button>
<button type="button" id="btn-tan-submit" class="btn btn-primary">TAN absenden</button> <button type="button" id="btn-tan-submit" class="btn btn-primary">TAN absenden</button>
</div> </div>

View file

@ -134,7 +134,7 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
$ziel = '/belege/' . (int)$row['beleg_id']; $ziel = '/belege/' . (int)$row['beleg_id'];
} }
?> ?>
<tr style="cursor:pointer" onclick="window.location='<?= $ziel ?>'"> <tr class="row-clickable" data-row-href="<?= $h($ziel) ?>">
<td class="td-mono"><?= $h(View::formatDate($row['wertstellung'])) ?></td> <td class="td-mono"><?= $h(View::formatDate($row['wertstellung'])) ?></td>
<td> <td>
<div style="display:flex;align-items:baseline;gap:.4rem;flex-wrap:wrap"> <div style="display:flex;align-items:baseline;gap:.4rem;flex-wrap:wrap">
@ -171,7 +171,7 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
</div> </div>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td onclick="event.stopPropagation()"> <td data-no-row-click>
<?php if ($row['status'] !== 'ignoriert'): ?> <?php if ($row['status'] !== 'ignoriert'): ?>
<form method="post" action="/bank/<?= (int)$row['id'] ?>/status"> <form method="post" action="/bank/<?= (int)$row['id'] ?>/status">
<input type="hidden" name="status" value="ignoriert"> <input type="hidden" name="status" value="ignoriert">

View file

@ -231,11 +231,10 @@ $restFormatted = number_format($remaining, 2, ',', '.');
<tbody> <tbody>
<?php foreach ($offeneBelege as $b): ?> <?php foreach ($offeneBelege as $b): ?>
<?php $hatBeleg = $b['id'] !== null; ?> <?php $hatBeleg = $b['id'] !== null; ?>
<tr class="beleg-row" <tr class="beleg-row <?= $hatBeleg ? '' : 'beleg-row--static' ?>"
data-beleg-id="<?= (int)$b['id'] ?>" data-beleg-id="<?= (int)$b['id'] ?>"
style="cursor:<?= $hatBeleg ? 'pointer' : 'default' ?>"
<?= $hatBeleg ? 'title="Klicken zum Vorschau anzeigen"' : '' ?>> <?= $hatBeleg ? 'title="Klicken zum Vorschau anzeigen"' : '' ?>>
<td onclick="event.stopPropagation()" style="white-space:nowrap"> <td data-no-row-click class="nowrap">
<div class="td-mono" style="font-size:.8rem"> <div class="td-mono" style="font-size:.8rem">
<?php if ($hatBeleg): ?> <?php if ($hatBeleg): ?>
<a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a> <a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
@ -249,10 +248,10 @@ $restFormatted = number_format($remaining, 2, ',', '.');
<td> <td>
<?= $b['supplier_name'] ? $h($b['supplier_name']) : '<span class="text-muted">–</span>' ?> <?= $b['supplier_name'] ? $h($b['supplier_name']) : '<span class="text-muted">–</span>' ?>
</td> </td>
<td class="td-right td-mono" style="white-space:nowrap"> <td class="td-right td-mono nowrap">
<?= $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) : '<span class="text-muted">–</span>' ?> <?= $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) : '<span class="text-muted">–</span>' ?>
</td> </td>
<td onclick="event.stopPropagation()"> <td data-no-row-click>
<?php if ($hatBeleg): ?> <?php if ($hatBeleg): ?>
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link"> <form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link">
<input type="hidden" name="beleg_id" value="<?= (int)$b['id'] ?>"> <input type="hidden" name="beleg_id" value="<?= (int)$b['id'] ?>">
@ -307,7 +306,9 @@ $restFormatted = number_format($remaining, 2, ',', '.');
let activeRow = null; let activeRow = null;
document.querySelectorAll('.beleg-row').forEach(function (row) { document.querySelectorAll('.beleg-row').forEach(function (row) {
row.addEventListener('click', function () { row.addEventListener('click', function (e) {
if (e.target.closest('[data-no-row-click]')) return;
const id = row.dataset.belegId; const id = row.dataset.belegId;
if (!id) return; if (!id) return;

View file

@ -47,14 +47,12 @@
<div style="display:flex;gap:1rem"> <div style="display:flex;gap:1rem">
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"> <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer">
<input type="radio" name="buchungstyp" value="Ausgabe" <input type="radio" name="buchungstyp" value="Ausgabe"
<?= $defaultTyp === 'Ausgabe' ? 'checked' : '' ?> <?= $defaultTyp === 'Ausgabe' ? 'checked' : '' ?>>
onchange="filterKategorieKonten(this.value)">
<span class="badge badge-red">Betriebsausgabe</span> <span class="badge badge-red">Betriebsausgabe</span>
</label> </label>
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"> <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer">
<input type="radio" name="buchungstyp" value="Einnahme" <input type="radio" name="buchungstyp" value="Einnahme"
<?= $defaultTyp === 'Einnahme' ? 'checked' : '' ?> <?= $defaultTyp === 'Einnahme' ? 'checked' : '' ?>>
onchange="filterKategorieKonten(this.value)">
<span class="badge badge-green">Betriebseinnahme</span> <span class="badge badge-green">Betriebseinnahme</span>
</label> </label>
</div> </div>
@ -70,7 +68,7 @@
<div class="form-group"> <div class="form-group">
<label>USt-Satz (%)</label> <label>USt-Satz (%)</label>
<input type="hidden" name="ust_satz" id="ust_satz_hidden" value="<?= $h($formData['ust_satz'] ?? '19') ?>"> <input type="hidden" name="ust_satz" id="ust_satz_hidden" value="<?= $h($formData['ust_satz'] ?? '19') ?>">
<select id="ust_satz_select" onchange="updateSteuerartVisibility();updateNettoAnzeige();document.getElementById('ust_satz_hidden').value=this.value"> <select id="ust_satz_select">
<?php foreach (['0','7','19'] as $s): ?> <?php foreach (['0','7','19'] as $s): ?>
<option value="<?= $s ?>"<?= ($formData['ust_satz'] ?? '19') === $s ? ' selected' : '' ?>><?= $s ?> %</option> <option value="<?= $s ?>"<?= ($formData['ust_satz'] ?? '19') === $s ? ' selected' : '' ?>><?= $s ?> %</option>
<?php endforeach; ?> <?php endforeach; ?>
@ -107,8 +105,7 @@
<input type="search" name="kategorie_kontonummer" placeholder="Kontonr. oder Bezeichnung filtern – oder direkt eingeben" class="konto-filter" <input type="search" name="kategorie_kontonummer" placeholder="Kontonr. oder Bezeichnung filtern – oder direkt eingeben" class="konto-filter"
data-target="kategorie_konto_id" autocomplete="off" style="margin-bottom:.3rem" data-target="kategorie_konto_id" autocomplete="off" style="margin-bottom:.3rem"
value="<?= $h($formData['kategorie_kontonummer'] ?? '') ?>"> value="<?= $h($formData['kategorie_kontonummer'] ?? '') ?>">
<select id="kategorie_konto_id" name="kategorie_konto_id" size="6" <select id="kategorie_konto_id" name="kategorie_konto_id" size="6">
onchange="showKontoArt('kategorie_konto_id','kategorie_konto_art',this);updateEigenanteilState();updateBewirtungsState();updateAutoUst();checkAnlagevermoegen()">
<option value="">– Konto wählen –</option> <option value="">– Konto wählen –</option>
<?php foreach (['Betriebsausgabe', 'Betriebseinnahme', 'Anlagevermögen', 'Privat', 'Verbindlichkeit'] as $art): ?> <?php foreach (['Betriebsausgabe', 'Betriebseinnahme', 'Anlagevermögen', 'Privat', 'Verbindlichkeit'] as $art): ?>
<?php if (empty($kontenGruppen[$art])) continue; ?> <?php if (empty($kontenGruppen[$art])) continue; ?>
@ -135,8 +132,7 @@
<input type="search" name="zahlungs_kontonummer" placeholder="Kontonr. oder Bezeichnung filtern – oder direkt eingeben" class="konto-filter" <input type="search" name="zahlungs_kontonummer" placeholder="Kontonr. oder Bezeichnung filtern – oder direkt eingeben" class="konto-filter"
data-target="zahlungskonto_id" autocomplete="off" style="margin-bottom:.3rem" data-target="zahlungskonto_id" autocomplete="off" style="margin-bottom:.3rem"
value="<?= $h($formData['zahlungs_kontonummer'] ?? '') ?>"> value="<?= $h($formData['zahlungs_kontonummer'] ?? '') ?>">
<select id="zahlungskonto_id" name="zahlungskonto_id" size="4" <select id="zahlungskonto_id" name="zahlungskonto_id" size="4">
onchange="showKontoArt('zahlungskonto_id','zahlungskonto_art',this)">
<option value="">– Konto wählen –</option> <option value="">– Konto wählen –</option>
<?php foreach (['Zahlungskonto', 'Verbindlichkeit'] as $art): ?> <?php foreach (['Zahlungskonto', 'Verbindlichkeit'] as $art): ?>
<?php if (empty($kontenGruppen[$art])) continue; ?> <?php if (empty($kontenGruppen[$art])) continue; ?>
@ -157,15 +153,14 @@
<div class="form-group"> <div class="form-group">
<div class="betrag-modus-toggle" role="group" aria-label="Eingabemodus Betrag" style="display:flex;align-items:center;gap:.4rem;min-height:2.1rem;margin-bottom:.4rem"> <div class="betrag-modus-toggle" role="group" aria-label="Eingabemodus Betrag" style="display:flex;align-items:center;gap:.4rem;min-height:2.1rem;margin-bottom:.4rem">
<button type="button" class="betrag-modus-btn active" id="betrag_modus_netto_btn" <button type="button" class="betrag-modus-btn active" id="betrag_modus_netto_btn"
aria-pressed="true" onclick="setBetragModus('netto')">NETTOBETRAG (€)</button> aria-pressed="true">NETTOBETRAG (€)</button>
<button type="button" class="betrag-modus-btn" id="betrag_modus_brutto_btn" <button type="button" class="betrag-modus-btn" id="betrag_modus_brutto_btn"
aria-pressed="false" onclick="setBetragModus('brutto')">BRUTTOBETRAG (€)</button> aria-pressed="false">BRUTTOBETRAG (€)</button>
</div> </div>
<input type="number" name="betrag_netto" id="betrag_netto_input" step="0.01" required <input type="number" name="betrag_netto" id="betrag_netto_input" step="0.01" required
placeholder="0.00" placeholder="0.00"
value="<?= $h($formData['betrag_netto'] ?? '') ?>" value="<?= $h($formData['betrag_netto'] ?? '') ?>"
class="<?= isset($formErrors['betrag_netto']) ? 'input-error' : '' ?>" class="<?= isset($formErrors['betrag_netto']) ? 'input-error' : '' ?>">
oninput="updateNettoAnzeige()">
<p id="ust_brutto_anzeige" class="text-muted text-sm" style="margin-top:.25rem;line-height:1.6"></p> <p id="ust_brutto_anzeige" class="text-muted text-sm" style="margin-top:.25rem;line-height:1.6"></p>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -192,8 +187,7 @@
<div class="form-group"> <div class="form-group">
<label for="bewirtungsgrund_input">Bewirtungsgrund *</label> <label for="bewirtungsgrund_input">Bewirtungsgrund *</label>
<textarea name="bewirtungsgrund" id="bewirtungsgrund_input" <textarea name="bewirtungsgrund" id="bewirtungsgrund_input"
style="height:2.4em;resize:none;overflow:hidden" class="autosize-textarea"
oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"
placeholder="Anlass/Grund der Bewirtung"><?= $h($formData['bewirtungsgrund'] ?? $beleg['bewirtungsgrund'] ?? '') ?></textarea> placeholder="Anlass/Grund der Bewirtung"><?= $h($formData['bewirtungsgrund'] ?? $beleg['bewirtungsgrund'] ?? '') ?></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -1,5 +1,5 @@
<!-- Crop-Modal --> <!-- Crop-Modal -->
<div class="modal-overlay" id="crop-modal-overlay" style="display:none;align-items:center;justify-content:center" <div class="modal-overlay" id="crop-modal-overlay"
data-beleg-id="<?= $h($beleg['id']) ?>"> data-beleg-id="<?= $h($beleg['id']) ?>">
<div class="modal" style="max-width:95vw;width:auto;padding:1.25rem"> <div class="modal" style="max-width:95vw;width:auto;padding:1.25rem">
<div class="modal-title" style="margin-bottom:.75rem">Beleg zuschneiden</div> <div class="modal-title" style="margin-bottom:.75rem">Beleg zuschneiden</div>
@ -17,8 +17,8 @@
</div> </div>
<style>@keyframes crop-spin{to{transform:rotate(360deg)}}</style> <style>@keyframes crop-spin{to{transform:rotate(360deg)}}</style>
<div style="margin-top:.75rem;display:flex;gap:.5rem;align-items:center"> <div style="margin-top:.75rem;display:flex;gap:.5rem;align-items:center">
<button type="button" class="btn btn-primary btn-sm" id="crop-confirm-btn" disabled onclick="confirmCrop()">Zuschnitt übernehmen</button> <button type="button" class="btn btn-primary btn-sm" id="crop-confirm-btn" disabled>Zuschnitt übernehmen</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="closeCropModal()">Abbrechen</button> <button type="button" class="btn btn-secondary btn-sm" id="crop-cancel-btn">Abbrechen</button>
<span id="crop-size-hint" class="text-muted text-sm"></span> <span id="crop-size-hint" class="text-muted text-sm"></span>
</div> </div>
</div> </div>

View file

@ -97,11 +97,10 @@
<div style="margin-left:auto;display:flex;gap:.5rem"> <div style="margin-left:auto;display:flex;gap:.5rem">
<a href="/belege/<?= $h($beleg['id']) ?>/file" target="_blank" class="btn btn-secondary btn-sm">Datei öffnen</a> <a href="/belege/<?= $h($beleg['id']) ?>/file" target="_blank" class="btn btn-secondary btn-sm">Datei öffnen</a>
<?php if (!$beleg['locked'] && in_array($beleg['mime_type'], ['image/jpeg', 'image/png'])): ?> <?php if (!$beleg['locked'] && in_array($beleg['mime_type'], ['image/jpeg', 'image/png'])): ?>
<button type="button" class="btn btn-secondary btn-sm" onclick="openCropModal()">Zuschneiden</button> <button type="button" class="btn btn-secondary btn-sm" id="beleg-crop-open-btn">Zuschneiden</button>
<?php endif; ?> <?php endif; ?>
<?php if (!$beleg['locked']): ?> <?php if (!$beleg['locked']): ?>
<button type="button" class="btn btn-secondary btn-sm" <button type="button" class="btn btn-secondary btn-sm" id="beleg-meta-toggle-btn">
onclick="document.getElementById('beleg-meta-form').style.display = document.getElementById('beleg-meta-form').style.display === 'none' ? 'block' : 'none'">
Metadaten bearbeiten Metadaten bearbeiten
</button> </button>
<?php endif; ?> <?php endif; ?>
@ -160,8 +159,7 @@
</datalist> </datalist>
</div> </div>
<button type="submit" class="btn btn-primary btn-sm">Speichern</button> <button type="submit" class="btn btn-primary btn-sm">Speichern</button>
<button type="button" class="btn btn-secondary btn-sm" <button type="button" class="btn btn-secondary btn-sm" id="beleg-meta-cancel-btn">Abbrechen</button>
onclick="document.getElementById('beleg-meta-form').style.display='none'">Abbrechen</button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -8,9 +8,9 @@
<iframe src="/belege/<?= $h($beleg['id']) ?>/file" title="Belegvorschau"></iframe> <iframe src="/belege/<?= $h($beleg['id']) ?>/file" title="Belegvorschau"></iframe>
<?php elseif (str_starts_with($beleg['mime_type'], 'image/')): ?> <?php elseif (str_starts_with($beleg['mime_type'], 'image/')): ?>
<div class="img-zoom-controls"> <div class="img-zoom-controls">
<button type="button" class="btn btn-secondary btn-sm" onclick="imgZoom(-1)" title="Verkleinern (Strg+–)">−</button> <button type="button" class="btn btn-secondary btn-sm" id="img-zoom-out-btn" title="Verkleinern (Strg+–)">−</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="imgZoom(+1)" title="Vergrößern (Strg++)">+</button> <button type="button" class="btn btn-secondary btn-sm" id="img-zoom-in-btn" title="Vergrößern (Strg++)">+</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="imgZoomReset()" title="Zurücksetzen (Strg+0)">↺</button> <button type="button" class="btn btn-secondary btn-sm" id="img-zoom-reset-btn" title="Zurücksetzen (Strg+0)">↺</button>
<span id="img-zoom-level">100 %</span> <span id="img-zoom-level">100 %</span>
</div> </div>
<div id="img-zoom-wrap" class="img-zoom-wrap"> <div id="img-zoom-wrap" class="img-zoom-wrap">

View file

@ -147,11 +147,10 @@
<?php foreach ($rows as $b): ?> <?php foreach ($rows as $b): ?>
<tr class="beleg-row" <tr class="beleg-row"
data-beleg-id="<?= (int)$b['id'] ?>" data-beleg-id="<?= (int)$b['id'] ?>"
style="cursor:pointer"
title="Klicken für Vorschau"> title="Klicken für Vorschau">
<td onclick="event.stopPropagation()"> <td data-no-row-click>
<a href="/belege/<?= $h($b['id']) ?>" class="font-mono"><?= $h($b['beleg_nr']) ?></a> <a href="/belege/<?= $h($b['id']) ?>" class="font-mono"><?= $h($b['beleg_nr']) ?></a>
<div style="margin-top:.2em"> <div class="mt-xs">
<?php if ($b['locked']): ?> <?php if ($b['locked']): ?>
<span class="badge badge-purple">Gesperrt</span> <span class="badge badge-purple">Gesperrt</span>
<?php elseif ($b['booked']): ?> <?php elseif ($b['booked']): ?>
@ -165,15 +164,14 @@
<td class="td-muted bl-supplier-cell" <td class="td-muted bl-supplier-cell"
data-beleg-id="<?= (int)$b['id'] ?>" data-beleg-id="<?= (int)$b['id'] ?>"
data-supplier="<?= $h($b['supplier_name'] ?? '') ?>" data-supplier="<?= $h($b['supplier_name'] ?? '') ?>"
onclick="event.stopPropagation(); blInlineSupplier(this)" data-no-row-click
style="cursor:text;min-width:80px"
title="Klicken zum Lieferant bearbeiten" title="Klicken zum Lieferant bearbeiten"
><?= $h($b['supplier_name'] ?? '–') ?></td> ><?= $h($b['supplier_name'] ?? '–') ?></td>
<td class="td-muted"><?= $h($b['rechnungs_nr'] ?? '–') ?></td> <td class="td-muted"><?= $h($b['rechnungs_nr'] ?? '–') ?></td>
<td class="td-right td-mono"><?= $b['betrag_brutto'] !== null ? View::formatMoney($b['betrag_brutto']) : '–' ?></td> <td class="td-right td-mono"><?= $b['betrag_brutto'] !== null ? View::formatMoney($b['betrag_brutto']) : '–' ?></td>
<td onclick="event.stopPropagation()"> <td data-no-row-click>
<a href="/belege/<?= $h($b['id']) ?>" <a href="/belege/<?= $h($b['id']) ?>"
style="background:none;border:none;cursor:pointer;color:var(--color-muted);padding:.2rem;line-height:1;display:inline-flex;align-items:center" class="icon-btn"
title="Bearbeiten"> title="Bearbeiten">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -243,7 +241,9 @@
let activeRow = null; let activeRow = null;
document.querySelectorAll('.beleg-row').forEach(function (row) { document.querySelectorAll('.beleg-row').forEach(function (row) {
row.addEventListener('click', function () { row.addEventListener('click', function (e) {
if (e.target.closest('[data-no-row-click]')) return;
const id = row.dataset.belegId; const id = row.dataset.belegId;
if (!id) return; if (!id) return;
@ -256,6 +256,10 @@
frame.src = '/belege/' + id + '/file'; frame.src = '/belege/' + id + '/file';
}); });
}); });
document.querySelectorAll('.bl-supplier-cell').forEach(function (td) {
td.addEventListener('click', function () { blInlineSupplier(td); });
});
})(); })();
function blInlineSupplier(td) { function blInlineSupplier(td) {

View file

@ -53,7 +53,7 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
<div class="card" style="margin-bottom:1rem"> <div class="card" style="margin-bottom:1rem">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between"> <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
<div class="card-title">Eingabefelder</div> <div class="card-title">Eingabefelder</div>
<button type="button" class="btn btn-secondary" style="font-size:.8rem;padding:.25rem .6rem" onclick="vorlageAddFeld()">+ Feld</button> <button type="button" class="btn btn-secondary" style="font-size:.8rem;padding:.25rem .6rem" data-action="vorlage-add-feld">+ Feld</button>
</div> </div>
<div class="card-body" style="padding:0"> <div class="card-body" style="padding:0">
<table class="data-table" id="vorlage-felder-table"> <table class="data-table" id="vorlage-felder-table">
@ -85,7 +85,7 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
</td> </td>
<td><input type="number" name="felder[<?= $i ?>][ust_satz]" value="<?= $h($feld['ust_satz'] ?? '') ?>" step="0.01" min="0" class="form-control form-control-sm" style="width:70px"></td> <td><input type="number" name="felder[<?= $i ?>][ust_satz]" value="<?= $h($feld['ust_satz'] ?? '') ?>" step="0.01" min="0" class="form-control form-control-sm" style="width:70px"></td>
<td><input type="text" name="felder[<?= $i ?>][meta_key]" value="<?= $h($feld['meta_key'] ?? '') ?>" class="form-control form-control-sm" style="width:130px"></td> <td><input type="text" name="felder[<?= $i ?>][meta_key]" value="<?= $h($feld['meta_key'] ?? '') ?>" class="form-control form-control-sm" style="width:130px"></td>
<td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" onclick="this.closest('tr').remove()">✕</button></td> <td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" data-action="remove-tr">✕</button></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
@ -97,7 +97,7 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
<div class="card" style="margin-bottom:1rem"> <div class="card" style="margin-bottom:1rem">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between"> <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
<div class="card-title">Buchungsregeln</div> <div class="card-title">Buchungsregeln</div>
<button type="button" class="btn btn-secondary" style="font-size:.8rem;padding:.25rem .6rem" onclick="vorlageAddRegel()">+ Regel</button> <button type="button" class="btn btn-secondary" style="font-size:.8rem;padding:.25rem .6rem" data-action="vorlage-add-regel">+ Regel</button>
</div> </div>
<div class="card-body" style="padding:0"> <div class="card-body" style="padding:0">
<div id="vorlage-regeln-container"> <div id="vorlage-regeln-container">
@ -115,14 +115,14 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
</select> </select>
<input type="number" name="regeln[<?= $ri ?>][sort_order]" value="<?= (int)($regel['sort_order'] ?? ($ri * 10)) ?>" <input type="number" name="regeln[<?= $ri ?>][sort_order]" value="<?= (int)($regel['sort_order'] ?? ($ri * 10)) ?>"
class="form-control form-control-sm" style="width:70px" placeholder="Sortierung"> class="form-control form-control-sm" style="width:70px" placeholder="Sortierung">
<button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem;margin-left:auto" onclick="this.closest('.vorlage-regel-block').remove()">✕</button> <button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem;margin-left:auto" data-action="remove-regel-block">✕</button>
</div> </div>
<table class="data-table" style="margin:0"> <table class="data-table" style="margin:0">
<thead> <thead>
<tr> <tr>
<th>Betrag-Referenz</th> <th>Betrag-Referenz</th>
<th>Ziel-Kontonummer</th> <th>Ziel-Kontonummer</th>
<th><button type="button" class="btn btn-secondary" style="font-size:.75rem;padding:.15rem .4rem" onclick="vorlageAddBuchung(this, <?= $ri ?>)">+ Buchung</button></th> <th><button type="button" class="btn btn-secondary" style="font-size:.75rem;padding:.15rem .4rem" data-action="vorlage-add-buchung" data-ri="<?= $ri ?>">+ Buchung</button></th>
</tr> </tr>
</thead> </thead>
<tbody id="vorlage-buchungen-<?= $ri ?>"> <tbody id="vorlage-buchungen-<?= $ri ?>">
@ -132,7 +132,7 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
value="<?= $h($buch['betrag_ref']) ?>" class="form-control form-control-sm" placeholder="z.B. netto_7"></td> value="<?= $h($buch['betrag_ref']) ?>" class="form-control form-control-sm" placeholder="z.B. netto_7"></td>
<td><input type="text" name="regeln[<?= $ri ?>][buchungen][<?= $bi ?>][nach_konto]" <td><input type="text" name="regeln[<?= $ri ?>][buchungen][<?= $bi ?>][nach_konto]"
value="<?= $h($buch['nach_konto']) ?>" class="form-control form-control-sm" placeholder="z.B. 4656"></td> value="<?= $h($buch['nach_konto']) ?>" class="form-control form-control-sm" placeholder="z.B. 4656"></td>
<td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" onclick="this.closest('tr').remove()">✕</button></td> <td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" data-action="remove-tr">✕</button></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
@ -167,7 +167,7 @@ function vorlageAddFeld() {
</select></td> </select></td>
<td><input type="number" name="felder[${i}][ust_satz]" value="" step="0.01" min="0" class="form-control form-control-sm" style="width:70px"></td> <td><input type="number" name="felder[${i}][ust_satz]" value="" step="0.01" min="0" class="form-control form-control-sm" style="width:70px"></td>
<td><input type="text" name="felder[${i}][meta_key]" value="" class="form-control form-control-sm" style="width:130px"></td> <td><input type="text" name="felder[${i}][meta_key]" value="" class="form-control form-control-sm" style="width:130px"></td>
<td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" onclick="this.closest('tr').remove()">✕</button></td>`; <td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" data-action="remove-tr">✕</button></td>`;
document.getElementById('vorlage-felder-body').appendChild(row); document.getElementById('vorlage-felder-body').appendChild(row);
} }
@ -188,18 +188,18 @@ function vorlageAddRegel() {
</select> </select>
<input type="number" name="regeln[${ri}][sort_order]" value="${ri*10}" <input type="number" name="regeln[${ri}][sort_order]" value="${ri*10}"
class="form-control form-control-sm" style="width:70px"> class="form-control form-control-sm" style="width:70px">
<button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem;margin-left:auto" onclick="this.closest('.vorlage-regel-block').remove()">✕</button> <button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem;margin-left:auto" data-action="remove-regel-block">✕</button>
</div> </div>
<table class="data-table" style="margin:0"> <table class="data-table" style="margin:0">
<thead><tr> <thead><tr>
<th>Betrag-Referenz</th> <th>Betrag-Referenz</th>
<th>Ziel-Kontonummer</th> <th>Ziel-Kontonummer</th>
<th><button type="button" class="btn btn-secondary" style="font-size:.75rem;padding:.15rem .4rem" onclick="vorlageAddBuchung(this,${ri})">+ Buchung</button></th> <th><button type="button" class="btn btn-secondary" style="font-size:.75rem;padding:.15rem .4rem" data-action="vorlage-add-buchung" data-ri="${ri}">+ Buchung</button></th>
</tr></thead> </tr></thead>
<tbody id="vorlage-buchungen-${ri}"></tbody> <tbody id="vorlage-buchungen-${ri}"></tbody>
</table>`; </table>`;
document.getElementById('vorlage-regeln-container').appendChild(div); document.getElementById('vorlage-regeln-container').appendChild(div);
vorlageAddBuchung(div.querySelector('button[onclick*="vorlageAddBuchung"]'), ri); vorlageAddBuchung(div.querySelector('[data-action="vorlage-add-buchung"]'), ri);
} }
function vorlageAddBuchung(btn, ri) { function vorlageAddBuchung(btn, ri) {
@ -212,7 +212,20 @@ function vorlageAddBuchung(btn, ri) {
value="" class="form-control form-control-sm" placeholder="z.B. netto_7"></td> value="" class="form-control form-control-sm" placeholder="z.B. netto_7"></td>
<td><input type="text" name="regeln[${ri}][buchungen][${bi}][nach_konto]" <td><input type="text" name="regeln[${ri}][buchungen][${bi}][nach_konto]"
value="" class="form-control form-control-sm" placeholder="z.B. 4656"></td> value="" class="form-control form-control-sm" placeholder="z.B. 4656"></td>
<td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" onclick="this.closest('tr').remove()">✕</button></td>`; <td><button type="button" class="btn btn-danger" style="font-size:.75rem;padding:.2rem .5rem" data-action="remove-tr">✕</button></td>`;
tbody.appendChild(tr); tbody.appendChild(tr);
} }
// ── Zentrale Event-Delegation statt Inline-Handler (auch für dynamisch erzeugte Zeilen) ──
document.addEventListener('click', function (e) {
var el = e.target.closest('[data-action]');
if (!el) return;
switch (el.dataset.action) {
case 'vorlage-add-feld': vorlageAddFeld(); break;
case 'vorlage-add-regel': vorlageAddRegel(); break;
case 'vorlage-add-buchung': vorlageAddBuchung(el, parseInt(el.dataset.ri, 10)); break;
case 'remove-tr': el.closest('tr').remove(); break;
case 'remove-regel-block': el.closest('.vorlage-regel-block').remove(); break;
}
});
</script> </script>

View file

@ -52,7 +52,7 @@
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/> <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg> </svg>
</a> </a>
<a href="/invoices/new?customer_id=<?= $h($c['id']) ?>" class="btn btn-primary" title="Neue Rechnung für diesen Kunden" style="display:inline-flex;align-items:center;justify-content:center;padding:.36rem;border-radius:50%;width:1.8rem;height:1.8rem"> <a href="/invoices/new?customer_id=<?= $h($c['id']) ?>" class="btn btn-primary btn-round-icon" title="Neue Rechnung für diesen Kunden">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg> </svg>

View file

@ -32,7 +32,7 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="card-title">Rechnungen</div> <div class="card-title">Rechnungen</div>
<a href="/invoices/new?customer_id=<?= $h($customer['id']) ?>" class="btn btn-primary" title="Neue Rechnung für diesen Kunden" style="display:inline-flex;align-items:center;justify-content:center;padding:.36rem;border-radius:50%;width:1.8rem;height:1.8rem"> <a href="/invoices/new?customer_id=<?= $h($customer['id']) ?>" class="btn btn-primary btn-round-icon" title="Neue Rechnung für diesen Kunden">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg> </svg>

View file

@ -30,13 +30,13 @@ $levelLabels = [
<div class="card"> <div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;padding:.75rem 1rem;border-bottom:1px solid var(--color-border)"> <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;padding:.75rem 1rem;border-bottom:1px solid var(--color-border)">
<form method="get" action="/ereignisse" id="events-filter" style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap"> <form method="get" action="/ereignisse" id="events-filter" style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
<select name="channel" onchange="this.form.submit()" style="width:auto;min-width:11rem"> <select name="channel" class="select-filter" data-autosubmit>
<option value="">Alle Bereiche</option> <option value="">Alle Bereiche</option>
<?php foreach ($channels as $c): ?> <?php foreach ($channels as $c): ?>
<option value="<?= $h($c) ?>"<?= $c === $aktiverKanal ? ' selected' : '' ?>><?= $h($c) ?></option> <option value="<?= $h($c) ?>"<?= $c === $aktiverKanal ? ' selected' : '' ?>><?= $h($c) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<select name="level" onchange="this.form.submit()" style="width:auto;min-width:11rem"> <select name="level" class="select-filter" data-autosubmit>
<option value="">Alle Schweregrade</option> <option value="">Alle Schweregrade</option>
<?php foreach ($levelLabels as $value => $label): ?> <?php foreach ($levelLabels as $value => $label): ?>
<option value="<?= $h($value) ?>"<?= $value === $aktivesLevel ? ' selected' : '' ?>><?= $h($label) ?></option> <option value="<?= $h($value) ?>"<?= $value === $aktivesLevel ? ' selected' : '' ?>><?= $h($label) ?></option>

View file

@ -13,8 +13,7 @@ $preCustomer = (int)($_GET['customer_id'] ?? 0);
<div class="form-group"> <div class="form-group">
<label for="customer_id">Kunde *</label> <label for="customer_id">Kunde *</label>
<select id="customer_id" name="customer_id" <select id="customer_id" name="customer_id"
class="<?= isset($errors['customer_id']) ? 'input-error' : '' ?>" required class="<?= isset($errors['customer_id']) ? 'input-error' : '' ?>" required>
onchange="updatePaymentDays(this)">
<option value="">– Kunde wählen –</option> <option value="">– Kunde wählen –</option>
<?php foreach ($customers as $c): ?> <?php foreach ($customers as $c): ?>
<?php $selected = ($invoice['customer_id'] ?? $preCustomer) == $c['id'] ? ' selected' : ''; ?> <?php $selected = ($invoice['customer_id'] ?? $preCustomer) == $c['id'] ? ' selected' : ''; ?>
@ -59,7 +58,7 @@ $preCustomer = (int)($_GET['customer_id'] ?? 0);
<div class="mb-2"> <div class="mb-2">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem">
<strong>Rechnungspositionen</strong> <strong>Rechnungspositionen</strong>
<button type="button" class="btn btn-primary" title="Position hinzufügen" onclick="showAddLineDialog(this)" style="display:inline-flex;align-items:center;justify-content:center;padding:.36rem;border-radius:50%;width:1.8rem;height:1.8rem"> <button type="button" class="btn btn-primary btn-round-icon" title="Position hinzufügen" data-action="show-add-line-dialog">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg> </svg>
@ -108,7 +107,7 @@ $preCustomer = (int)($_GET['customer_id'] ?? 0);
<input type="hidden" name="lines[<?= $i ?>][type]" value="heading"> <input type="hidden" name="lines[<?= $i ?>][type]" value="heading">
<textarea name="lines[<?= $i ?>][bezeichnung]" required placeholder="Überschrift" rows="1" class="line-bez-input" style="width:100%;font-weight:bold"><?= $h($line['bezeichnung']) ?></textarea> <textarea name="lines[<?= $i ?>][bezeichnung]" required placeholder="Überschrift" rows="1" class="line-bez-input" style="width:100%;font-weight:bold"><?= $h($line['bezeichnung']) ?></textarea>
</td> </td>
<td><button type="button" class="btn btn-danger btn-sm" onclick="removeInvoiceLine(this)">✕</button></td> <td><button type="button" class="btn btn-danger btn-sm" data-action="remove-invoice-line">✕</button></td>
</tr> </tr>
<?php else: ?> <?php else: ?>
<tr class="invoice-line-row"> <tr class="invoice-line-row">
@ -135,7 +134,7 @@ $preCustomer = (int)($_GET['customer_id'] ?? 0);
<td><input type="number" name="lines[<?= $i ?>][einzelpreis]" value="<?= $h($line['einzelpreis']) ?>" step="0.01" min="0" style="width:100%" required></td> <td><input type="number" name="lines[<?= $i ?>][einzelpreis]" value="<?= $h($line['einzelpreis']) ?>" step="0.01" min="0" style="width:100%" required></td>
<td class="td-right line-netto text-muted"><?= View::formatMoney($line['betrag_netto']) ?></td> <td class="td-right line-netto text-muted"><?= View::formatMoney($line['betrag_netto']) ?></td>
<td class="td-right line-brutto font-bold"><?= View::formatMoney($line['betrag_brutto']) ?></td> <td class="td-right line-brutto font-bold"><?= View::formatMoney($line['betrag_brutto']) ?></td>
<td><button type="button" class="btn btn-danger btn-sm" onclick="removeInvoiceLine(this)">✕</button></td> <td><button type="button" class="btn btn-danger btn-sm" data-action="remove-invoice-line">✕</button></td>
</tr> </tr>
<?php endif; ?> <?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
@ -175,6 +174,7 @@ function updatePaymentDays(sel) {
const days = opt.dataset.payment; const days = opt.dataset.payment;
if (days) document.getElementById('payment_days').value = days; if (days) document.getElementById('payment_days').value = days;
} }
document.getElementById('customer_id')?.addEventListener('change', function () { updatePaymentDays(this); });
<?php if (!$isEdit): ?> <?php if (!$isEdit): ?>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const sel = document.getElementById('customer_id'); const sel = document.getElementById('customer_id');

View file

@ -25,25 +25,25 @@ $badgeClass = match($invoice['status']) {
<!-- Querverweise Storno --> <!-- Querverweise Storno -->
<?php if (!empty($originalrechnung)): ?> <?php if (!empty($originalrechnung)): ?>
<div class="alert alert-warning" style="margin-bottom:.75rem;padding:.6rem 1rem;background:#fff3cd;border-left:3px solid #f59e0b;font-size:.8rem"> <div class="alert alert-warning">
Stornorechnung zu: <a href="/invoices/<?= $h($originalrechnung['id']) ?>"><strong><?= $h($originalrechnung['rechnungs_nr']) ?></strong></a> Stornorechnung zu: <a href="/invoices/<?= $h($originalrechnung['id']) ?>"><strong><?= $h($originalrechnung['rechnungs_nr']) ?></strong></a>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($stornorechnung)): ?> <?php if (!empty($stornorechnung)): ?>
<div class="alert alert-info" style="margin-bottom:.75rem;padding:.6rem 1rem;background:#e0f2fe;border-left:3px solid #0284c7;font-size:.8rem"> <div class="alert alert-info">
Wurde storniert durch: <a href="/invoices/<?= $h($stornorechnung['id']) ?>"><strong><?= $h($stornorechnung['rechnungs_nr']) ?></strong></a> Wurde storniert durch: <a href="/invoices/<?= $h($stornorechnung['id']) ?>"><strong><?= $h($stornorechnung['rechnungs_nr']) ?></strong></a>
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Hinweis auf fehlende Daten für die XML-Rechnung (EN 16931) --> <!-- Hinweis auf fehlende Daten für die XML-Rechnung (EN 16931) -->
<?php if (!empty($xmlHinweise)): ?> <?php if (!empty($xmlHinweise)): ?>
<div class="alert alert-warning" style="margin-bottom:.75rem;padding:.6rem 1rem;background:#fff3cd;border-left:3px solid #f59e0b;font-size:.8rem"> <div class="alert alert-warning">
<strong>XML-Rechnung (EN 16931 / XRechnung): unvollständige Daten.</strong> <strong>XML-Rechnung (EN 16931 / XRechnung): unvollständige Daten.</strong>
Der Download ist trotzdem möglich, die Datei erfüllt die Norm aber nur eingeschränkt. Der Download ist trotzdem möglich, die Datei erfüllt die Norm aber nur eingeschränkt.
<?php foreach ($xmlHinweise as $bereich => $felder): ?> <?php foreach ($xmlHinweise as $bereich => $felder): ?>
<div style="margin-top:.4rem"> <div class="mt-1">
<em><?= $h($bereich) ?>:</em> <em><?= $h($bereich) ?>:</em>
<ul style="margin:.2rem 0 0 1.2rem"> <ul class="xml-hinweis-list">
<?php foreach ($felder as $feld): ?> <?php foreach ($felder as $feld): ?>
<li><?= $h($feld) ?></li> <li><?= $h($feld) ?></li>
<?php endforeach; ?> <?php endforeach; ?>
@ -51,7 +51,7 @@ $badgeClass = match($invoice['status']) {
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (isset($xmlHinweise['Stammdaten'])): ?> <?php if (isset($xmlHinweise['Stammdaten'])): ?>
<div style="margin-top:.4rem">Stammdaten pflegen unter <a href="/settings">Einstellungen</a>.</div> <div class="mt-1">Stammdaten pflegen unter <a href="/settings">Einstellungen</a>.</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
@ -59,12 +59,12 @@ $badgeClass = match($invoice['status']) {
<!-- Aktionsleiste --> <!-- Aktionsleiste -->
<div class="toolbar mb-2"> <div class="toolbar mb-2">
<div class="toolbar-left"> <div class="toolbar-left">
<span class="badge <?= $badgeClass ?>" style="font-size:.7rem;padding:.3rem .8rem"><?= $h($invoice['status']) ?></span> <span class="badge badge-lg <?= $badgeClass ?>"><?= $h($invoice['status']) ?></span>
</div> </div>
<div class="toolbar-right" style="gap:.5rem"> <div class="toolbar-right">
<?php if ($invoice['status'] === 'Entwurf'): ?> <?php if ($invoice['status'] === 'Entwurf'): ?>
<a href="/invoices/<?= $h($invoice['id']) ?>/edit" <a href="/invoices/<?= $h($invoice['id']) ?>/edit"
style="background:none;border:none;cursor:pointer;color:var(--color-muted);padding:.2rem;line-height:1;display:inline-flex;align-items:center" class="icon-btn"
title="Bearbeiten"> title="Bearbeiten">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -79,7 +79,7 @@ $badgeClass = match($invoice['status']) {
<?php endif; ?> <?php endif; ?>
<?php if ($invoice['status'] !== 'Entwurf'): ?> <?php if ($invoice['status'] !== 'Entwurf'): ?>
<a href="/invoices/<?= $h($invoice['id']) ?>/pdf" <a href="/invoices/<?= $h($invoice['id']) ?>/pdf"
style="background:none;border:none;cursor:pointer;color:var(--color-muted);padding:.2rem;line-height:1;display:inline-flex;align-items:center" class="icon-btn"
title="PDF ansehen"> title="PDF ansehen">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -90,7 +90,8 @@ $badgeClass = match($invoice['status']) {
</a> </a>
<?php endif; ?> <?php endif; ?>
<a href="/invoices/<?= $h($invoice['id']) ?>/xml" <a href="/invoices/<?= $h($invoice['id']) ?>/xml"
style="background:none;border:none;cursor:pointer;color:<?= empty($xmlHinweise) ? 'var(--color-muted)' : 'var(--color-warning)' ?>;padding:.2rem;line-height:1;display:inline-flex;align-items:center" class="icon-btn"
<?= empty($xmlHinweise) ? '' : 'style="--icon-color: var(--color-warning)"' ?>
title="<?= $h($xmlTooltip) ?>"> title="<?= $h($xmlTooltip) ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -101,12 +102,10 @@ $badgeClass = match($invoice['status']) {
</svg> </svg>
</a> </a>
<?php if (!in_array($invoice['status'], ['Entwurf', 'Storniert'])): ?> <?php if (!in_array($invoice['status'], ['Entwurf', 'Storniert'])): ?>
<button type="button" class="btn btn-primary" <button type="button" class="btn btn-primary" data-modal-open="send-modal">Per E-Mail senden</button>
onclick="document.getElementById('send-modal').style.display='flex'">Per E-Mail senden</button>
<?php endif; ?> <?php endif; ?>
<?php if (!in_array($invoice['status'], ['Entwurf', 'Storniert']) && !empty($invoice['beleg_id'])): ?> <?php if (!in_array($invoice['status'], ['Entwurf', 'Storniert']) && !empty($invoice['beleg_id'])): ?>
<button type="button" class="btn btn-success" <button type="button" class="btn btn-success" data-modal-open="buchungs-modal">Zahlungen</button>
onclick="document.getElementById('buchungs-modal').style.display='flex'">Zahlungen</button>
<?php endif; ?> <?php endif; ?>
<?php if (in_array($invoice['status'], ['Entwurf','Finalisiert','Gesendet']) && empty($invoice['storno_of_id'])): ?> <?php if (in_array($invoice['status'], ['Entwurf','Finalisiert','Gesendet']) && empty($invoice['storno_of_id'])): ?>
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/cancel" <form method="post" action="/invoices/<?= $h($invoice['id']) ?>/cancel"
@ -127,40 +126,34 @@ $badgeClass = match($invoice['status']) {
<?php <?php
$logoMaxW = max(50, (int)($settings['logo_max_width'] ?? 600)); $logoMaxW = max(50, (int)($settings['logo_max_width'] ?? 600));
$logoMaxH = max(20, (int)($settings['logo_max_height'] ?? 100)); $logoMaxH = max(20, (int)($settings['logo_max_height'] ?? 100));
$logoPos = $settings['logo_position'] ?? 'center'; $logoPosRaw = $settings['logo_position'] ?? 'center';
$logoPos = in_array($logoPosRaw, ['left', 'right'], true) ? $logoPosRaw : 'center';
$logoMSide = max(0, (int)($settings['logo_margin_side'] ?? 75)); $logoMSide = max(0, (int)($settings['logo_margin_side'] ?? 75));
$logoMTop = max(0, (int)($settings['logo_margin_top'] ?? 6)); $logoMTop = max(0, (int)($settings['logo_margin_top'] ?? 6));
if ($logoPos === 'left') {
$logoWrapStyle = "text-align:left;margin-left:{$logoMSide}px;margin-top:{$logoMTop}px;margin-bottom:1.5rem";
} elseif ($logoPos === 'right') {
$logoWrapStyle = "text-align:right;margin-right:{$logoMSide}px;margin-top:{$logoMTop}px;margin-bottom:1.5rem";
} else {
$logoWrapStyle = 'text-align:center;margin-bottom:1.5rem';
}
?> ?>
<div style="<?= $logoWrapStyle ?>"> <div class="invoice-logo-wrap invoice-logo-wrap--<?= $logoPos ?>" style="--logo-mside: <?= $logoMSide ?>px; --logo-mtop: <?= $logoMTop ?>px">
<img src="/settings/logo/file" alt="Logo" <img src="/settings/logo/file" alt="Logo"
style="max-width:<?= $logoMaxW ?>px;max-height:<?= $logoMaxH ?>px"> style="--logo-maxw: <?= $logoMaxW ?>px; --logo-maxh: <?= $logoMaxH ?>px">
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Adressbereich (DIN 5008: 50% = 85mm bei vollem A4-Preview) --> <!-- Adressbereich (DIN 5008: 50% = 85mm bei vollem A4-Preview) -->
<div class="invoice-address" style="margin-top:1cm;margin-bottom:1.5rem"> <div class="invoice-address invoice-address-block">
<div> <div>
<div style="font-size:6pt;color:#666;border-bottom:1px solid #ccc;padding-bottom:2px;margin-bottom:12px"> <div class="invoice-sender-line">
<?= $h($settings['firma'] ?? '') ?> · <?= $h($settings['strasse'] ?? '') ?> · <?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?> <?= $h($settings['firma'] ?? '') ?> · <?= $h($settings['strasse'] ?? '') ?> · <?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?>
</div> </div>
<div style="line-height:1.5;font-size:10pt"> <div class="invoice-recipient-block">
<?= $h($customerName) ?><br> <?= $h($customerName) ?><br>
<?= $h($invoice['strasse']) ?><br> <?= $h($invoice['strasse']) ?><br>
<?= $h($invoice['plz']) ?> <?= $h($invoice['ort']) ?> <?= $h($invoice['plz']) ?> <?= $h($invoice['ort']) ?>
<?php if ($invoice['land'] !== 'DE'): ?><br><?= $h($invoice['land']) ?><?php endif; ?> <?php if ($invoice['land'] !== 'DE'): ?><br><?= $h($invoice['land']) ?><?php endif; ?>
</div> </div>
</div> </div>
<div style="text-align:right;font-size:17pt"> <div class="invoice-title-block">
<?= !empty($invoice['storno_of_id']) ? 'STORNORECHNUNG' : 'Rechnung' ?> <?= !empty($invoice['storno_of_id']) ? 'STORNORECHNUNG' : 'Rechnung' ?>
<?php if (!empty($invoice['storno_of_id']) && !empty($originalrechnung)): ?> <?php if (!empty($invoice['storno_of_id']) && !empty($originalrechnung)): ?>
<div style="font-size:.65rem;margin-top:.3rem;font-weight:normal"> <div class="invoice-title-ref">
zu Rechnung Nr. <strong><?= $h($originalrechnung['rechnungs_nr']) ?></strong> zu Rechnung Nr. <strong><?= $h($originalrechnung['rechnungs_nr']) ?></strong>
vom <?= $h(View::formatDate($originalrechnung['rechnungs_datum'] ?? '')) ?> vom <?= $h(View::formatDate($originalrechnung['rechnungs_datum'] ?? '')) ?>
</div> </div>
@ -169,55 +162,55 @@ $badgeClass = match($invoice['status']) {
</div> </div>
<!-- Meta-Tabelle --> <!-- Meta-Tabelle -->
<table style="width:100%;table-layout:fixed;border-collapse:collapse;margin-top:2cm;margin-bottom:1.5rem;font-size:.7rem"> <table class="invoice-meta-table">
<thead> <thead>
<tr> <tr>
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Betreff</th> <th>Betreff</th>
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Rechnungsnummer</th> <th>Rechnungsnummer</th>
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Kundennummer</th> <th>Kundennummer</th>
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Leistungszeitraum</th> <th>Leistungszeitraum</th>
<th style="width:20%;padding:.4rem .5rem;text-align:right;font-weight:bold">Datum</th> <th>Datum</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td style="padding:.4rem .5rem;white-space:nowrap"><?= $h($invoice['betreff']) ?></td> <td class="nowrap"><?= $h($invoice['betreff']) ?></td>
<td style="padding:.4rem .5rem;white-space:nowrap;font-weight:700"><?= $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?></td> <td class="nowrap font-bold"><?= $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?></td>
<td style="padding:.4rem .5rem;white-space:nowrap"><?= $h($invoice['kundennummer']) ?></td> <td class="nowrap"><?= $h($invoice['kundennummer']) ?></td>
<td style="padding:.4rem .5rem"><?= $lzHtml ?: '–' ?></td> <td><?= $lzHtml ?: '–' ?></td>
<td style="padding:.4rem .5rem;text-align:right"><?= $h(View::formatDate($invoice['rechnungs_datum'])) ?></td> <td><?= $h(View::formatDate($invoice['rechnungs_datum'])) ?></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- Positionen --> <!-- Positionen -->
<table id="invoice-pos-table" style="width:100%;table-layout:fixed;border-collapse:collapse;margin-bottom:1.5rem;font-size:.7rem"> <table id="invoice-pos-table">
<thead> <thead>
<tr style="background:#f0f0f0"> <tr>
<th style="padding:.5rem;text-align:left;width:6%">Pos.</th> <th>Pos.</th>
<th style="padding:.5rem;text-align:left">Bezeichnung</th> <th>Bezeichnung</th>
<th style="padding:.5rem;text-align:right;width:11%">Menge</th> <th>Menge</th>
<th style="padding:.5rem;text-align:left;width:10%">Einheit</th> <th>Einheit</th>
<th style="padding:.5rem;text-align:right;width:8%">USt.</th> <th>USt.</th>
<th style="padding:.5rem;text-align:right;width:15%">Einzelpreis</th> <th>Einzelpreis</th>
<th style="padding:.5rem;text-align:right;width:16%">Gesamt</th> <th>Gesamt</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($lines as $line): ?> <?php foreach ($lines as $line): ?>
<?php if (($line['type'] ?? 'position') === 'heading'): ?> <?php if (($line['type'] ?? 'position') === 'heading'): ?>
<tr style="border-bottom:1px solid #eee"> <tr class="row-heading">
<td colspan="7" style="padding:.4rem .5rem;font-weight:700"><?= nl2br($h($line['bezeichnung'])) ?></td> <td colspan="7"><?= nl2br($h($line['bezeichnung'])) ?></td>
</tr> </tr>
<?php else: ?> <?php else: ?>
<tr style="border-bottom:1px solid #eee"> <tr>
<td style="padding:.4rem .5rem"><?= $h($line['position']) ?></td> <td><?= $h($line['position']) ?></td>
<td style="padding:.4rem .5rem"><?= nl2br($h($line['bezeichnung'])) ?></td> <td><?= nl2br($h($line['bezeichnung'])) ?></td>
<td style="padding:.4rem .5rem;text-align:right"><?= number_format((float)$line['menge'], 1, ',', '.') ?></td> <td><?= number_format((float)$line['menge'], 1, ',', '.') ?></td>
<td style="padding:.4rem .5rem"><?= $h($line['einheit']) ?></td> <td><?= $h($line['einheit']) ?></td>
<td style="padding:.4rem .5rem;text-align:right"><?= number_format((float)$line['ust_satz'], 0) ?> %</td> <td><?= number_format((float)$line['ust_satz'], 0) ?> %</td>
<td style="padding:.4rem .5rem;text-align:right"><?= $fmt($line['einzelpreis']) ?></td> <td><?= $fmt($line['einzelpreis']) ?></td>
<td style="padding:.4rem .5rem;text-align:right;font-weight:600"><?= $fmt($line['betrag_netto']) ?></td> <td class="col-total"><?= $fmt($line['betrag_netto']) ?></td>
</tr> </tr>
<?php endif; ?> <?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
@ -226,18 +219,18 @@ $badgeClass = match($invoice['status']) {
<!-- Summen + Zahlungshinweis --> <!-- Summen + Zahlungshinweis -->
<div id="invoice-summary"> <div id="invoice-summary">
<div style="display:flex;justify-content:flex-end;margin-bottom:1.5rem;padding-right:.5rem"> <div class="invoice-summary-wrap">
<table style="width:auto"> <table class="invoice-summary-table">
<tr><td style="padding:.3rem 0">Nettobetrag:</td><td style="text-align:right;padding:.3rem 0;width:7.5em"><?= $fmt($invoice['betrag_netto']) ?></td></tr> <tr><td>Nettobetrag:</td><td><?= $fmt($invoice['betrag_netto']) ?></td></tr>
<tr><td style="padding:.3rem 0">zzgl. Umsatzsteuer:</td><td style="text-align:right;padding:.3rem 0"><?= $fmt($invoice['betrag_ust']) ?></td></tr> <tr><td>zzgl. Umsatzsteuer:</td><td><?= $fmt($invoice['betrag_ust']) ?></td></tr>
<tr style="font-weight:700;font-size:.82rem"> <tr class="row-total">
<td style="padding:.5rem 0">Rechnungsbetrag:</td> <td>Rechnungsbetrag:</td>
<td style="text-align:right;padding:.5rem 0"><?= $fmt($invoice['betrag_brutto']) ?></td> <td><?= $fmt($invoice['betrag_brutto']) ?></td>
</tr> </tr>
</table> </table>
</div> </div>
<?php if (empty($invoice['storno_of_id'])): ?> <?php if (empty($invoice['storno_of_id'])): ?>
<div style="background:#f0f4ff;border-left:3px solid #2563eb;padding:.75rem 1rem;margin-bottom:2rem;font-size:.7rem"> <div class="invoice-payment-hint">
Bitte überweisen Sie den Rechnungsbetrag innerhalb <?= $h($invoice['payment_days'] ?? 14) ?> Tagen auf das unten genannte Konto. Bitte überweisen Sie den Rechnungsbetrag innerhalb <?= $h($invoice['payment_days'] ?? 14) ?> Tagen auf das unten genannte Konto.
<strong>Fällig am: <?= View::formatDate($invoice['faellig_datum']) ?></strong> <strong>Fällig am: <?= View::formatDate($invoice['faellig_datum']) ?></strong>
</div> </div>
@ -248,25 +241,25 @@ $badgeClass = match($invoice['status']) {
<!-- Fußzeile --> <!-- Fußzeile -->
<div class="invoice-footer-bar"> <div class="invoice-footer-bar">
<div style="border-top:1px solid #ccc;padding-top:.75rem;display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:1rem;font-size:.58rem;color:#555"> <div class="invoice-footer-grid">
<div style="min-width:0"> <div class="min-w-0">
<strong><?= $h($settings['firma'] ?? '') ?></strong><br> <strong><?= $h($settings['firma'] ?? '') ?></strong><br>
<?php if (trim((string)($settings['inhaber'] ?? '')) !== ''): ?>Inh. <?= $h($settings['inhaber']) ?><br><?php endif; ?> <?php if (trim((string)($settings['inhaber'] ?? '')) !== ''): ?>Inh. <?= $h($settings['inhaber']) ?><br><?php endif; ?>
<?= $h($settings['strasse'] ?? '') ?><br> <?= $h($settings['strasse'] ?? '') ?><br>
<?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?><br> <?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?><br>
USt-ID: <?= $h($settings['ust_id'] ?? '') ?> USt-ID: <?= $h($settings['ust_id'] ?? '') ?>
</div> </div>
<div style="white-space:nowrap"> <div class="nowrap">
Tel: <?= $h($settings['telefon'] ?? '') ?><br> Tel: <?= $h($settings['telefon'] ?? '') ?><br>
E-Mail: <?= $h($settings['email'] ?? '') ?><br> E-Mail: <?= $h($settings['email'] ?? '') ?><br>
Web: <?= $h($settings['website'] ?? '') ?> Web: <?= $h($settings['website'] ?? '') ?>
</div> </div>
<div style="white-space:nowrap"> <div class="nowrap">
<?= $h($settings['bank_name'] ?? '') ?><br> <?= $h($settings['bank_name'] ?? '') ?><br>
IBAN: <?= $h($settings['iban'] ?? '') ?><br> IBAN: <?= $h($settings['iban'] ?? '') ?><br>
BIC: <?= $h($settings['bic'] ?? '') ?> BIC: <?= $h($settings['bic'] ?? '') ?>
</div> </div>
<div style="text-align:right;white-space:nowrap">Seite <span class="page-num">1</span> von <span class="page-total">1</span></div> <div class="text-right nowrap">Seite <span class="page-num">1</span> von <span class="page-total">1</span></div>
</div> </div>
</div> </div>
@ -395,9 +388,9 @@ $badgeClass = match($invoice['status']) {
</script> </script>
<!-- E-Mail-Senden-Modal --> <!-- E-Mail-Senden-Modal -->
<div id="send-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;align-items:center;justify-content:center"> <div id="send-modal" class="modal-overlay">
<div class="modal-inline-form" style="background:#fff;border-radius:8px;padding:1.5rem;width:420px;max-width:95vw;box-shadow:0 8px 32px rgba(0,0,0,.2)"> <div class="modal-inline-form modal-card-invoice modal-card-invoice--sm">
<h3 style="margin:0 0 1rem">Rechnung per E-Mail senden</h3> <h3 class="modal-form-title">Rechnung per E-Mail senden</h3>
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/send"> <form method="post" action="/invoices/<?= $h($invoice['id']) ?>/send">
<div class="form-group"> <div class="form-group">
<label>Empfänger *</label> <label>Empfänger *</label>
@ -415,9 +408,8 @@ $badgeClass = match($invoice['status']) {
<input type="email" name="email_cc2" placeholder="cc2@example.com" <input type="email" name="email_cc2" placeholder="cc2@example.com"
value="<?= $h($customer['email_cc2'] ?? '') ?>"> value="<?= $h($customer['email_cc2'] ?? '') ?>">
</div> </div>
<div style="display:flex;gap:.75rem;justify-content:flex-end;margin-top:1.25rem"> <div class="modal-actions">
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
onclick="document.getElementById('send-modal').style.display='none'">Abbrechen</button>
<button type="submit" class="btn btn-primary">Senden</button> <button type="submit" class="btn btn-primary">Senden</button>
</div> </div>
</form> </form>
@ -429,48 +421,47 @@ $badgeClass = match($invoice['status']) {
<?php <?php
$kontenGruppen = \Steuerkram\View::gruppiereKonten($accounts, true); $kontenGruppen = \Steuerkram\View::gruppiereKonten($accounts, true);
?> ?>
<div id="buchungs-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;align-items:flex-start;justify-content:center;padding:2rem 1rem;overflow-y:auto"> <div id="buchungs-modal" class="modal-overlay modal-overlay--scroll">
<div style="background:#fff;border-radius:8px;width:100%;max-width:720px;box-shadow:0 8px 32px rgba(0,0,0,.2)"> <div class="modal-card-invoice modal-card-invoice--lg">
<div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid #e5e7eb"> <div class="modal-section modal-section-border row-between">
<h3 style="margin:0">Zahlungen – <?= $h($invoice['rechnungs_nr']) ?></h3> <h3 class="m-0">Zahlungen – <?= $h($invoice['rechnungs_nr']) ?></h3>
<button onclick="document.getElementById('buchungs-modal').style.display='none'" <button data-modal-close class="modal-card-close modal-card-close--lg">&times;</button>
style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1">&times;</button>
</div> </div>
<!-- Vorhandene Buchungen --> <!-- Vorhandene Buchungen -->
<div style="padding:1rem 1.25rem;border-bottom:1px solid #e5e7eb;overflow-x:auto"> <div class="modal-section modal-section-border table-wrap">
<table style="width:100%;font-size:.75rem;border-collapse:collapse"> <table class="booking-table">
<thead> <thead>
<tr style="background:#f0f0f0"> <tr>
<th style="padding:.4rem .5rem;text-align:left">Datum</th> <th>Datum</th>
<th style="padding:.4rem .5rem;text-align:left">Kategorie</th> <th>Kategorie</th>
<th style="padding:.4rem .5rem;text-align:left">Zahlungskonto</th> <th>Zahlungskonto</th>
<th style="padding:.4rem .5rem;text-align:right">Netto</th> <th class="num-right">Netto</th>
<th style="padding:.4rem .5rem;text-align:right">USt</th> <th class="num-right">USt</th>
<th style="padding:.4rem .5rem;text-align:right">Brutto</th> <th class="num-right">Brutto</th>
<th style="padding:.4rem .5rem">Text</th> <th>Text</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($bookings as $bk): <?php foreach ($bookings as $bk):
$bkStorno = (bool)$bk['is_reversal'] || !empty($bk['reversed_by']); $bkStorno = (bool)$bk['is_reversal'] || !empty($bk['reversed_by']);
?> ?>
<tr style="border-bottom:1px solid #eee;<?= $bkStorno ? 'opacity:.5;background:#fff5f5' : '' ?>"> <tr<?= $bkStorno ? ' class="is-storno"' : '' ?>>
<td style="padding:.35rem .5rem;white-space:nowrap"><?= $h(View::formatDate($bk['buchungs_datum'])) ?></td> <td class="nowrap"><?= $h(View::formatDate($bk['buchungs_datum'])) ?></td>
<td style="padding:.35rem .5rem;font-size:.7rem"><?= $h($bk['kategorie_nr']) ?><br><span style="color:#6b7280"><?= $h($bk['kategorie_name']) ?></span></td> <td class="cell-sub"><?= $h($bk['kategorie_nr']) ?><br><span class="text-muted"><?= $h($bk['kategorie_name']) ?></span></td>
<td style="padding:.35rem .5rem;font-size:.7rem"><?= $h($bk['zahlungs_nr']) ?><br><span style="color:#6b7280"><?= $h($bk['zahlungs_name']) ?></span></td> <td class="cell-sub"><?= $h($bk['zahlungs_nr']) ?><br><span class="text-muted"><?= $h($bk['zahlungs_name']) ?></span></td>
<td style="padding:.35rem .5rem;text-align:right;font-family:monospace"><?= $fmt($bk['betrag_netto']) ?></td> <td class="num-right text-mono"><?= $fmt($bk['betrag_netto']) ?></td>
<td style="padding:.35rem .5rem;text-align:right"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td> <td class="num-right"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td>
<td style="padding:.35rem .5rem;text-align:right;font-family:monospace;font-weight:600"><?= $fmt($bk['betrag_brutto']) ?></td> <td class="num-right text-mono font-bold"><?= $fmt($bk['betrag_brutto']) ?></td>
<td style="padding:.35rem .5rem;color:#6b7280;font-size:.7rem"> <td class="text-muted cell-sub">
<?= $h($bk['buchungstext']) ?> <?= $h($bk['buchungstext']) ?>
<?php if ((bool)$bk['is_reversal']): ?><span style="color:#dc2626;font-size:.65rem">[Storno]</span> <?php if ((bool)$bk['is_reversal']): ?><span class="text-danger cell-storno-tag">[Storno]</span>
<?php elseif (!empty($bk['reversed_by'])): ?><span style="color:#dc2626;font-size:.65rem">[Storniert]</span><?php endif; ?> <?php elseif (!empty($bk['reversed_by'])): ?><span class="text-danger cell-storno-tag">[Storniert]</span><?php endif; ?>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($bookings)): ?> <?php if (empty($bookings)): ?>
<tr><td colspan="7" style="padding:1rem;text-align:center;color:#9ca3af">Noch keine Buchungen.</td></tr> <tr><td colspan="7" class="text-center text-faint cell-empty">Noch keine Buchungen.</td></tr>
<?php endif; ?> <?php endif; ?>
</tbody> </tbody>
<?php <?php
@ -478,19 +469,19 @@ $badgeClass = match($invoice['status']) {
$offen = round((float)$invoice['betrag_brutto'] - $gezahlt, 2); $offen = round((float)$invoice['betrag_brutto'] - $gezahlt, 2);
?> ?>
<tfoot> <tfoot>
<tr style="border-top:2px solid #e5e7eb"> <tr>
<td colspan="5" style="padding:.4rem .5rem;text-align:right">Rechnungsbetrag</td> <td colspan="5" class="num-right">Rechnungsbetrag</td>
<td style="padding:.4rem .5rem;text-align:right;font-family:monospace"><?= $fmt($invoice['betrag_brutto']) ?></td> <td class="num-right text-mono"><?= $fmt($invoice['betrag_brutto']) ?></td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:.4rem .5rem;text-align:right">Gezahlt</td> <td colspan="5" class="num-right">Gezahlt</td>
<td style="padding:.4rem .5rem;text-align:right;font-family:monospace"><?= $fmt($gezahlt) ?></td> <td class="num-right text-mono"><?= $fmt($gezahlt) ?></td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td colspan="5" style="padding:.4rem .5rem;text-align:right;font-weight:600"><?= $offen < 0 ? 'Überzahlt' : 'Offen' ?></td> <td colspan="5" class="num-right font-bold"><?= $offen < 0 ? 'Überzahlt' : 'Offen' ?></td>
<td style="padding:.4rem .5rem;text-align:right;font-family:monospace;font-weight:600"><?= $fmt(abs($offen)) ?></td> <td class="num-right text-mono font-bold"><?= $fmt(abs($offen)) ?></td>
<td></td> <td></td>
</tr> </tr>
</tfoot> </tfoot>
@ -509,35 +500,35 @@ $badgeClass = match($invoice['status']) {
$ustVorgabe = (string)(int)round($ustQuote * 100); $ustVorgabe = (string)(int)round($ustQuote * 100);
if (!in_array($ustVorgabe, ['0', '7', '19'], true)) $ustVorgabe = '19'; if (!in_array($ustVorgabe, ['0', '7', '19'], true)) $ustVorgabe = '19';
?> ?>
<div style="padding:1rem 1.25rem"> <div class="modal-section">
<div style="font-size:.8rem;font-weight:600;margin-bottom:.75rem">Neue Buchung erfassen</div> <div class="booking-form-title">Neue Buchung erfassen</div>
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/bookings"> <form method="post" action="/invoices/<?= $h($invoice['id']) ?>/bookings">
<input type="hidden" name="_from_modal" value="1"> <input type="hidden" name="_from_modal" value="1">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem .75rem;margin-bottom:.75rem"> <div class="booking-form-row">
<div style="display:flex;align-items:center;gap:.4rem"> <div class="booking-form-field">
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Zahldatum *</label> <label>Zahldatum *</label>
<input type="date" name="buchungs_datum" value="<?= date('Y-m-d') ?>" required style="width:100%;min-width:0"> <input type="date" name="buchungs_datum" value="<?= date('Y-m-d') ?>" required class="w-full min-w-0">
</div> </div>
<div style="display:flex;align-items:center;gap:.4rem"> <div class="booking-form-field">
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Nettobetrag (€) *</label> <label>Nettobetrag (€) *</label>
<input type="number" name="betrag_netto" step="0.01" min="0.01" required <input type="number" name="betrag_netto" step="0.01" min="0.01" required
value="<?= $h(number_format(abs($vorschlagNet), 2, '.', '')) ?>" value="<?= $h(number_format(abs($vorschlagNet), 2, '.', '')) ?>"
style="width:100%;min-width:0"> class="w-full min-w-0">
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem .75rem;margin-bottom:.75rem"> <div class="booking-form-row">
<div style="display:flex;align-items:center;gap:.4rem"> <div class="booking-form-field">
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Kategorie-Konto *</label> <label>Kategorie-Konto *</label>
<select name="kategorie_konto_id" required style="width:100%;min-width:0"> <select name="kategorie_konto_id" required class="w-full min-w-0">
<option value="">– Konto wählen –</option> <option value="">– Konto wählen –</option>
<?php foreach ($kontenGruppen['Betriebseinnahme'] ?? [] as $acc): ?> <?php foreach ($kontenGruppen['Betriebseinnahme'] ?? [] as $acc): ?>
<option value="<?= $h($acc['id']) ?>"><?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?></option> <option value="<?= $h($acc['id']) ?>"><?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div style="display:flex;align-items:center;gap:.4rem"> <div class="booking-form-field">
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Zahlungskonto *</label> <label>Zahlungskonto *</label>
<select name="zahlungskonto_id" required style="width:100%;min-width:0"> <select name="zahlungskonto_id" required class="w-full min-w-0">
<option value="">– Konto wählen –</option> <option value="">– Konto wählen –</option>
<?php foreach ($kontenGruppen['Zahlungskonto'] ?? [] as $acc): ?> <?php foreach ($kontenGruppen['Zahlungskonto'] ?? [] as $acc): ?>
<option value="<?= $h($acc['id']) ?>"><?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?></option> <option value="<?= $h($acc['id']) ?>"><?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?></option>
@ -545,25 +536,24 @@ $badgeClass = match($invoice['status']) {
</select> </select>
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem .75rem;margin-bottom:.75rem"> <div class="booking-form-row">
<div style="display:flex;align-items:center;gap:.4rem"> <div class="booking-form-field">
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">USt-Satz (%)</label> <label>USt-Satz (%)</label>
<select name="ust_satz" style="width:100%;min-width:0"> <select name="ust_satz" class="w-full min-w-0">
<?php foreach (['0','7','19'] as $s): ?> <?php foreach (['0','7','19'] as $s): ?>
<option value="<?= $s ?>"<?= $s === $ustVorgabe ? ' selected' : '' ?>><?= $s ?> %</option> <option value="<?= $s ?>"<?= $s === $ustVorgabe ? ' selected' : '' ?>><?= $s ?> %</option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div style="display:flex;align-items:center;gap:.4rem"> <div class="booking-form-field">
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Buchungstext *</label> <label>Buchungstext *</label>
<input type="text" name="buchungstext" required <input type="text" name="buchungstext" required
value="Ausgangsrechnung <?= $h($invoice['rechnungs_nr'] ?? '') ?>" value="Ausgangsrechnung <?= $h($invoice['rechnungs_nr'] ?? '') ?>"
style="width:100%;min-width:0"> class="w-full min-w-0">
</div> </div>
</div> </div>
<div style="display:flex;gap:.75rem;justify-content:flex-end"> <div class="row-end">
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary" data-modal-close>Schließen</button>
onclick="document.getElementById('buchungs-modal').style.display='none'">Schließen</button>
<button type="submit" class="btn btn-primary">Buchung erfassen</button> <button type="submit" class="btn btn-primary">Buchung erfassen</button>
</div> </div>
</form> </form>
@ -576,7 +566,7 @@ $badgeClass = match($invoice['status']) {
if (flash) { if (flash) {
var ref = document.referrer || ''; var ref = document.referrer || '';
if (ref.indexOf('/invoices/<?= (int)$invoice['id'] ?>/bookings') !== -1) { if (ref.indexOf('/invoices/<?= (int)$invoice['id'] ?>/bookings') !== -1) {
document.getElementById('buchungs-modal').style.display = 'flex'; document.getElementById('buchungs-modal').classList.add('open');
} }
} }
}()); }());

View file

@ -144,8 +144,7 @@
<div style="position:relative;width:48px;height:26px;flex-shrink:0"> <div style="position:relative;width:48px;height:26px;flex-shrink:0">
<input type="checkbox" name="test_mode" id="test_mode_toggle" <input type="checkbox" name="test_mode" id="test_mode_toggle"
<?= ($settings['test_mode'] ?? 1) ? 'checked' : '' ?> <?= ($settings['test_mode'] ?? 1) ? 'checked' : '' ?>
style="opacity:0;width:0;height:0;position:absolute" style="opacity:0;width:0;height:0;position:absolute">
onchange="updateModeLabel(this)">
<span id="toggle_track" style=" <span id="toggle_track" style="
position:absolute;inset:0;border-radius:13px;transition:background .2s; position:absolute;inset:0;border-radius:13px;transition:background .2s;
background:<?= ($settings['test_mode'] ?? 1) ? '#f59e0b' : '#22c55e' ?>; background:<?= ($settings['test_mode'] ?? 1) ? '#f59e0b' : '#22c55e' ?>;
@ -454,8 +453,7 @@
<?php foreach (['center' => 'Zentriert', 'left' => 'Links', 'right' => 'Rechts'] as $val => $label): ?> <?php foreach (['center' => 'Zentriert', 'left' => 'Links', 'right' => 'Rechts'] as $val => $label): ?>
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer"> <label style="display:flex;align-items:center;gap:.35rem;cursor:pointer">
<input type="radio" name="logo_position" value="<?= $val ?>" form="settings-form" <input type="radio" name="logo_position" value="<?= $val ?>" form="settings-form"
<?= ($settings['logo_position'] ?? 'center') === $val ? 'checked' : '' ?> <?= ($settings['logo_position'] ?? 'center') === $val ? 'checked' : '' ?>>
onchange="logoPositionChanged()">
<?= $label ?> <?= $label ?>
</label> </label>
<?php endforeach; ?> <?php endforeach; ?>
@ -618,6 +616,11 @@ function logoPositionChanged() {
(pos === 'left' || pos === 'right') ? 'block' : 'none'; (pos === 'left' || pos === 'right') ? 'block' : 'none';
} }
document.getElementById('test_mode_toggle')?.addEventListener('change', function () { updateModeLabel(this); });
document.querySelectorAll('input[name="logo_position"]').forEach(function (radio) {
radio.addEventListener('change', logoPositionChanged);
});
document.getElementById('btn-generate-app-key')?.addEventListener('click', function () { document.getElementById('btn-generate-app-key')?.addEventListener('click', function () {
const btn = this; const btn = this;
const result = document.getElementById('app-key-result'); const result = document.getElementById('app-key-result');

View file

@ -24,14 +24,17 @@
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<button type="button" class="btn btn-primary w-full" onclick=" <button type="button" class="btn btn-primary w-full" id="ustva-nav-btn">UStVA anzeigen</button>
var y=document.getElementById('ustva-year').value;
var m=document.getElementById('ustva-month').value;
window.location='/tax/ustva/'+y+'/'+m;
">UStVA anzeigen</button>
</form> </form>
</div> </div>
</div> </div>
<script>
document.getElementById('ustva-nav-btn')?.addEventListener('click', function () {
var y = document.getElementById('ustva-year').value;
var m = document.getElementById('ustva-month').value;
window.location = '/tax/ustva/' + y + '/' + m;
});
</script>
<!-- Liste vergangener Einreichungen --> <!-- Liste vergangener Einreichungen -->
<div class="card"> <div class="card">

View file

@ -34,8 +34,7 @@ $editId = $formData['edit_id'] ?? null;
<?= $editId ? 'Speichern' : 'Anlegen' ?> <?= $editId ? 'Speichern' : 'Anlegen' ?>
</button> </button>
<button type="button" id="cancel-btn" class="btn btn-secondary" <button type="button" id="cancel-btn" class="btn btn-secondary"
style="<?= $editId ? '' : 'display:none' ?>" style="<?= $editId ? '' : 'display:none' ?>">Abbrechen</button>
onclick="resetForm()">Abbrechen</button>
</div> </div>
</div> </div>
</form> </form>
@ -70,9 +69,10 @@ $editId = $formData['edit_id'] ?? null;
<td><?= $h(View::formatDate(substr($u['created_at'], 0, 10))) ?></td> <td><?= $h(View::formatDate(substr($u['created_at'], 0, 10))) ?></td>
<td style="text-align:right"> <td style="text-align:right">
<button type="button" <button type="button"
style="background:none;border:none;cursor:pointer;color:var(--color-muted);padding:.2rem;line-height:1;display:inline-flex;align-items:center" class="icon-btn edit-user-btn"
title="Bearbeiten" title="Bearbeiten"
onclick="editUser(<?= (int)$u['id'] ?>, <?= $h(json_encode($u['username'])) ?>)"> data-user-id="<?= (int)$u['id'] ?>"
data-username="<?= $h($u['username']) ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
@ -130,4 +130,11 @@ function resetForm() {
document.getElementById('submit-btn').textContent = 'Anlegen'; document.getElementById('submit-btn').textContent = 'Anlegen';
document.getElementById('cancel-btn').style.display = 'none'; document.getElementById('cancel-btn').style.display = 'none';
} }
document.getElementById('cancel-btn')?.addEventListener('click', resetForm);
document.querySelectorAll('.edit-user-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
editUser(parseInt(this.dataset.userId, 10), this.dataset.username);
});
});
</script> </script>

View file

@ -41,25 +41,28 @@ class UstBruttoAnzeigeTest extends TestCase
'Anzeigebereich id="ust_brutto_anzeige" vorhanden' 'Anzeigebereich id="ust_brutto_anzeige" vorhanden'
); );
// Kein Inline-Handler mehr im Markup (CSP) – die Verdrahtung läuft über
// zentrale addEventListener()-Aufrufe in beleg-form.js.
$this->assert( $this->assert(
str_contains($content, 'oninput="updateNettoAnzeige()"'), !str_contains($viewContent, 'oninput=') && !str_contains($viewContent, 'onchange='),
'Nettobetrag-Input ruft updateNettoAnzeige() bei Eingabe auf' 'Nettobetrag-Input/USt-Satz-Select haben keine Inline-Handler mehr'
); );
$this->assert( $this->assert(
str_contains($content, 'function updateNettoAnzeige()'), str_contains($jsContent, 'function updateNettoAnzeige()'),
'JavaScript-Funktion updateNettoAnzeige() ist definiert' 'JavaScript-Funktion updateNettoAnzeige() ist definiert'
); );
$this->assert( $this->assert(
str_contains($content, 'updateNettoAnzeige();updateNettoAnzeige()') === false str_contains($jsContent, "_betragNettoInput.addEventListener('input', updateNettoAnzeige)"),
&& str_contains($content, 'updateNettoAnzeige()'), 'Nettobetrag-Input ruft updateNettoAnzeige() bei Eingabe auf'
'updateNettoAnzeige() wird an mehreren Stellen aufgerufen'
); );
// Prüfe dass USt-Satz-Änderung auch updateNettoAnzeige aufruft // Prüfe dass USt-Satz-Änderung auch updateNettoAnzeige aufruft
$this->assert( $this->assert(
str_contains($content, 'updateSteuerartVisibility();updateNettoAnzeige()'), str_contains($jsContent, '_ustSatzSelect.addEventListener')
&& str_contains($jsContent, 'updateSteuerartVisibility();')
&& str_contains($jsContent, 'updateNettoAnzeige();'),
'USt-Satz-Änderung löst auch updateNettoAnzeige() aus' 'USt-Satz-Änderung löst auch updateNettoAnzeige() aus'
); );
} }

View file

@ -38,9 +38,16 @@ class BetragModusToggleTest extends TestCase
'Beide Umschalt-Buttons (Netto/Brutto) sind vorhanden' 'Beide Umschalt-Buttons (Netto/Brutto) sind vorhanden'
); );
// Kein Inline-Handler mehr im Markup (CSP) – die Verdrahtung läuft über
// zentrale addEventListener()-Aufrufe in beleg-form.js.
$this->assert( $this->assert(
str_contains($content, "onclick=\"setBetragModus('netto')\"") !str_contains($viewContent, 'onclick='),
&& str_contains($content, "onclick=\"setBetragModus('brutto')\""), 'Umschalt-Buttons haben keinen Inline-Handler mehr'
);
$this->assert(
str_contains($jsContent, "_betragModusNettoBtn.addEventListener('click', function () { setBetragModus('netto'); });")
&& str_contains($jsContent, "_betragModusBruttoBtn.addEventListener('click', function () { setBetragModus('brutto'); });"),
'Buttons rufen setBetragModus() mit dem jeweiligen Modus auf' 'Buttons rufen setBetragModus() mit dem jeweiligen Modus auf'
); );

View file

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* Test 97: Inline-Style-Budget für src/Views (Fix #160)
*
* Zählt alle style="-Vorkommen unter src/Views und schlägt fehl, wenn die
* Zahl über dem Budget liegt. Das Budget wird mit jedem weiteren
* Umsetzungsschritt aus Issue #160 gesenkt und verhindert, dass neue Views
* den Zähler wieder hochtreiben.
*
* Zusätzlich: keine onclick=/onchange=/oninput=-Attribute mehr in src/Views
* (Vorbedingung für eine CSP ohne unsafe-inline, siehe Issue #160).
*/
class InlineStyleBudgetTest extends TestCase
{
protected string $testName = 'Test 97: Inline-Style-Budget für src/Views';
/** Aktueller Stand nach Fix #160 (vorher: 1024). Bei weiteren Umsetzungsschritten senken. */
private const STYLE_BUDGET = 900;
public function run(): void
{
$viewDir = __DIR__ . '/../src/Views';
$files = $this->phpFilesIn($viewDir);
$this->assertGreaterThan(0, count($files), 'src/Views enthält PHP-Dateien');
$styleCount = 0;
$handlerFiles = [];
foreach ($files as $file) {
$content = file_get_contents($file);
if ($content === false) {
continue;
}
$styleCount += substr_count($content, 'style="');
if (preg_match('/\bon(click|change|input)=/', $content)) {
$handlerFiles[] = substr($file, strlen($viewDir) + 1);
}
}
$this->assert(
$styleCount <= self::STYLE_BUDGET,
"style=\"-Vorkommen unter src/Views bleiben im Budget (erhalten: $styleCount, Budget: " . self::STYLE_BUDGET . ')'
);
$this->assert(
$handlerFiles === [],
'Keine onclick=/onchange=/oninput=-Attribute mehr in src/Views (gefunden in: ' . implode(', ', $handlerFiles) . ')'
);
}
/** @return string[] */
private function phpFilesIn(string $dir): array
{
$files = [];
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS));
foreach ($it as $fileInfo) {
if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') {
$files[] = $fileInfo->getPathname();
}
}
return $files;
}
}