Fix #160: Inline-Styles und Inline-Handler reduziert (Schritt 1: Modals, invoices/show.php, alle Inline-Handler)
This commit is contained in:
parent
8eca05424f
commit
3f4eb1c734
32 changed files with 715 additions and 266 deletions
39
CLAUDE.md
39
CLAUDE.md
|
|
@ -9,6 +9,45 @@ MVC: `src/Controllers/`, `src/Models/`, `src/Services/`, `src/Views/`.
|
|||
- Neue Libraries nur nach expliziter Rückfrage einbinden.
|
||||
- Bestehenden Code-Stil beibehalten (declare strict_types, 4 Spaces).
|
||||
- SQL ausschließlich über `Database::fetchAll/fetchOne/execute()` mit Prepared Statements.
|
||||
- **Keine neuen `style="`-Attribute außer für Laufzeitwerte** (siehe unten).
|
||||
|
||||
## CSS-Klassen statt Inline-Styles (Issue #160)
|
||||
|
||||
`public/static/css/app.css` ist das zentrale Stylesheet. Wiederkehrende Muster
|
||||
gehören dorthin, nicht als `style="..."` in die Views – sonst ist weder eine
|
||||
CSP ohne `unsafe-inline` noch Theming möglich, und `tests/97_InlineStyleBudgetTest.php`
|
||||
schlägt fehl, sobald das Budget überschritten wird.
|
||||
|
||||
**Regel:** Inline-Styles nur, wenn der Wert tatsächlich zur Laufzeit aus PHP
|
||||
kommt (Kontofarbe, Logo-Maße aus den Einstellungen o. ä.) – und auch dann
|
||||
bevorzugt als CSS-Variable statt vollständiger Deklaration:
|
||||
|
||||
```html
|
||||
<!-- 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
|
||||
**Regel: Jedes neue Feature bekommt einen eigenen Test.**
|
||||
|
|
|
|||
12
Changes.md
12
Changes.md
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-08-18 – Inline-Styles und Inline-Handler reduziert (#160)
|
||||
|
||||
Erster Umsetzungsschritt der in Issue #160 beschriebenen schrittweisen Reduktion (975 → 858 `style="`-Vorkommen, 51 → 0 Inline-Handler in `src/Views`). Kein Big-Bang über alle Views, sondern die im Ticket priorisierten Schritte 1–4:
|
||||
|
||||
- **`public/static/css/app.css`:** neue wiederverwendbare Klassen für die häufigsten Muster – `.modal-overlay`/`.modal-card` (Dialog-Overlay + -Karte), `.icon-btn` (17 identische Icon-Button-Deklarationen quer über 11 Views), `.badge-lg`, `.alert`/`.alert-warning`/`.alert-info`, `.row-clickable`/`data-row-href` (klickbare Tabellenzeilen), `.btn-round-icon`, `.autosize-textarea`, diverse `invoice-*`- und `booking-table`-Klassen für die Rechnungsvorschau/Zahlungsübersicht sowie kleinere Utilities (`.nowrap`, `.min-w-0`, `.m-0`, `.text-danger`, `.text-faint` …). Laufzeitfarben (Kontofarbe, Icon-Warnfarbe, Logo-Maße) bleiben als CSS-Variable im `style`-Attribut, wie im Ticket als Zielbild vorgegeben.
|
||||
- **Modal-Overlay vereinheitlicht:** alle 12 Views mit Dialogen (`bank/_modal_*.php`, `belege/_crop_modal.php`, `belege/_anlage_modal.php`, `belege/_vorschau.php`, `assets/form.php`, `assets/list.php`, `buchungsvorlagen/modal.php`, `invoices/show.php`) nutzen jetzt `.modal-overlay`/`.modal-card` statt ausgeschriebener Inline-Styles je Vorkommen.
|
||||
- **`invoices/show.php`** (126 → 3 Inline-Styles, `bank/fints.php` und `belege/detail.php` lagen durch die Aufteilung aus #157/#161 bereits bei 0): Rechnungsvorschau-Tabellen, Zahlungs-Modal und Toolbar auf Klassen umgestellt; die beiden verbleibenden Modale (E-Mail senden, Zahlungen) laufen jetzt über das bestehende `data-modal-open`/`data-modal-close`-Muster aus `app.js` statt über `onclick`.
|
||||
- **53 Inline-Handler entfernt:** `onclick=`/`onchange=`/`oninput=` in `src/Views` durch `id`-/`data-*`-Attribute plus zentrale `addEventListener()`-Verdrahtung ersetzt – wo die Zielfunktion bereits Seiten-JS ist (z. B. `beleg-form.js`, `beleg-ui.js`, `beleg-crop.js`, `fints.js`), direkt dort; für generische, seitenübergreifende Muster (klickbare Tabellenzeile, Rechnungspositionen entfernen/hinzufügen, Buchungsvorlagen-Zeilen inkl. dynamisch per JS erzeugter Zeilen) zentrale `data-action`-Dispatcher in `app.js` bzw. im jeweiligen View-Script. Verhalten unverändert, keine Funktionsänderung.
|
||||
- **`tests/97_InlineStyleBudgetTest.php`** (neu): zählt `style="`-Vorkommen unter `src/Views` und schlägt bei Überschreitung des Budgets (aktuell 900) fehl; prüft zusätzlich, dass keine `onclick=`/`onchange=`/`oninput=`-Attribute mehr vorkommen. Verhindert, dass neue Views den Zähler wieder hochtreiben.
|
||||
- Restliche ~600 Inline-Styles in weiteren Views (`tax/euer_show.php`, `buchungsvorlagen/form.php`, `assets/form.php`, `settings/form.php`, `bank/upload.php` u. a.) sind laut Ticket bewusst nicht Teil dieses Schritts – die Reduktion ist als mehrstufiger Prozess angelegt, ein Rundumschlag über alle Views wäre nicht reviewbar gewesen.
|
||||
- Manuelle Abnahme der Optik war mangels Browser in dieser Umgebung nicht möglich; die Umstellung ist als mechanische 1:1-Übertragung (identische Werte, keine neuen Layoutentscheidungen) ausgeführt, um das Risiko sichtbarer Änderungen zu minimieren.
|
||||
|
||||
## 2026-08-18 – Restposten: O-CHAT.html, fints.php/bankimport.php, Request-Abstraktion (#161)
|
||||
|
||||
- **(a) `public/O-CHAT.html`:** Testartefakt einer eigenständigen Ollama-Chat-Oberfläche wurde von Apache direkt ausgeliefert (`.htaccess` routet nur nicht-existierende Dateien auf `index.php`) und lief damit an der Authentifizierung vorbei. Nach `BEISPIELE/O-CHAT.html` verschoben (gitignored, wird nicht ausgeliefert). **`tests/93_WebrootSauberkeitTest.php`** (neu) hält `public/` dauerhaft sauber.
|
||||
|
|
|
|||
|
|
@ -564,6 +564,27 @@ td { padding: .65rem 1rem; vertical-align: middle; }
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
.invoice-address-block { margin-top: 1cm; margin-bottom: 1.5rem; }
|
||||
|
||||
.invoice-sender-line {
|
||||
font-size: 6pt;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 2px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.invoice-recipient-block { line-height: 1.5; font-size: 10pt; }
|
||||
|
||||
.invoice-title-block { text-align: right; font-size: 17pt; }
|
||||
.invoice-title-ref { font-size: .65rem; margin-top: .3rem; font-weight: normal; }
|
||||
|
||||
.invoice-logo-wrap { margin-bottom: 1.5rem; }
|
||||
.invoice-logo-wrap--left { text-align: left; margin-left: var(--logo-mside); margin-top: var(--logo-mtop); }
|
||||
.invoice-logo-wrap--right { text-align: right; margin-right: var(--logo-mside); margin-top: var(--logo-mtop); }
|
||||
.invoice-logo-wrap--center { text-align: center; }
|
||||
.invoice-logo-wrap img { max-width: var(--logo-maxw); max-height: var(--logo-maxh); }
|
||||
|
||||
@media (max-width: 1059px) {
|
||||
.invoice-address {
|
||||
grid-template-columns: 1fr;
|
||||
|
|
@ -582,13 +603,19 @@ td { padding: .65rem 1rem; vertical-align: middle; }
|
|||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,.45);
|
||||
z-index: 100;
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-overlay.open { display: flex; }
|
||||
|
||||
/* Modale mit größerem Inhalt (z.B. Buchungsformular): oben ausgerichtet, scrollbar */
|
||||
.modal-overlay--scroll { align-items: flex-start; padding: 2rem 1rem; overflow-y: auto; }
|
||||
|
||||
/* Modale, die über einem anderen Modal liegen (z.B. TAN-Abfrage über Abruf-Log) */
|
||||
.modal-overlay--stacked { z-index: 1100; }
|
||||
|
||||
.modal {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius);
|
||||
|
|
@ -601,6 +628,182 @@ td { padding: .65rem 1rem; vertical-align: middle; }
|
|||
.modal-title { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; }
|
||||
.modal-actions { display: flex; gap: .75rem; justify-content: flex-end; margin-top: 1.25rem; }
|
||||
|
||||
/* Kompakte Dialog-Karte (Bank-Verbindungsdialoge, TAN-Eingabe, Abruf-Log) */
|
||||
.modal-card {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.15);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-card--lg { max-width: 680px; }
|
||||
|
||||
/* Dialog-Karte mit fester Breite statt max-width (Rechnungs-Modale) */
|
||||
.modal-card-invoice {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.2);
|
||||
}
|
||||
.modal-card-invoice--sm { width: 420px; max-width: 95vw; padding: 1.5rem; }
|
||||
.modal-card-invoice--lg { width: 100%; max-width: 720px; }
|
||||
.modal-form-title { margin: 0 0 1rem; }
|
||||
|
||||
.modal-section { padding: 1rem 1.25rem; }
|
||||
.modal-section-border { border-bottom: 1px solid #e5e7eb; }
|
||||
|
||||
.modal-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-card h2 { margin: 0; font-size: 1.125rem; }
|
||||
|
||||
.modal-card-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.modal-card-close--lg { font-size: 1.5rem; color: #6b7280; }
|
||||
|
||||
/* ── Badges mit Laufzeitfarbe ────────────────────────────────── */
|
||||
.badge-color {
|
||||
background: var(--badge-bg, var(--color-primary));
|
||||
color: var(--badge-fg, #fff);
|
||||
}
|
||||
|
||||
/* ── Häufige Layout-Muster ───────────────────────────────────── */
|
||||
.row-between { display: flex; justify-content: space-between; align-items: center; }
|
||||
.row-end { display: flex; justify-content: flex-end; gap: .5rem; }
|
||||
.row-wrap { display: flex; flex-wrap: wrap; gap: .5rem; }
|
||||
.col { display: flex; flex-direction: column; gap: .75rem; }
|
||||
.text-danger { color: var(--color-danger); }
|
||||
.text-faint { color: #9ca3af; }
|
||||
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--icon-color, var(--color-muted));
|
||||
padding: .2rem;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge-lg { font-size: .7rem; padding: .3rem .8rem; }
|
||||
|
||||
.alert {
|
||||
margin-bottom: .75rem;
|
||||
padding: .6rem 1rem;
|
||||
font-size: .8rem;
|
||||
border-left: 3px solid;
|
||||
}
|
||||
.alert-warning { background: #fff3cd; border-left-color: #f59e0b; }
|
||||
.alert-info { background: #e0f2fe; border-left-color: #0284c7; }
|
||||
.xml-hinweis-list { margin: .2rem 0 0 1.2rem; }
|
||||
|
||||
/* ── Rechnungsvorschau: Tabellen (DIN 5008) ─────────────────────── */
|
||||
.invoice-meta-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
margin-top: 2cm;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
.invoice-meta-table th,
|
||||
.invoice-meta-table td { width: 20%; padding: .4rem .5rem; text-align: left; }
|
||||
.invoice-meta-table th { font-weight: bold; }
|
||||
.invoice-meta-table th:last-child,
|
||||
.invoice-meta-table td:last-child { text-align: right; }
|
||||
|
||||
#invoice-pos-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
#invoice-pos-table thead tr { background: #f0f0f0; }
|
||||
#invoice-pos-table th { padding: .5rem; text-align: left; }
|
||||
#invoice-pos-table td { padding: .4rem .5rem; }
|
||||
#invoice-pos-table tbody tr { border-bottom: 1px solid #eee; }
|
||||
#invoice-pos-table th:nth-child(1) { width: 6%; }
|
||||
#invoice-pos-table th:nth-child(3) { width: 11%; text-align: right; }
|
||||
#invoice-pos-table th:nth-child(4) { width: 10%; }
|
||||
#invoice-pos-table th:nth-child(5) { width: 8%; text-align: right; }
|
||||
#invoice-pos-table th:nth-child(6) { width: 15%; text-align: right; }
|
||||
#invoice-pos-table th:nth-child(7) { width: 16%; text-align: right; }
|
||||
#invoice-pos-table td:nth-child(3),
|
||||
#invoice-pos-table td:nth-child(5),
|
||||
#invoice-pos-table td:nth-child(6),
|
||||
#invoice-pos-table td:nth-child(7) { text-align: right; }
|
||||
#invoice-pos-table .row-heading td { font-weight: 700; }
|
||||
#invoice-pos-table .col-total { font-weight: 600; }
|
||||
|
||||
.invoice-summary-table { width: auto; }
|
||||
.invoice-summary-table td { padding: .3rem 0; }
|
||||
.invoice-summary-table td:last-child { text-align: right; width: 7.5em; }
|
||||
.invoice-summary-table .row-total { font-weight: 700; font-size: .82rem; }
|
||||
.invoice-summary-table .row-total td { padding: .5rem 0; }
|
||||
|
||||
.booking-table { width: 100%; font-size: .75rem; border-collapse: collapse; }
|
||||
.booking-table thead tr { background: #f0f0f0; }
|
||||
.booking-table th { padding: .4rem .5rem; text-align: left; }
|
||||
.booking-table td { padding: .35rem .5rem; }
|
||||
.booking-table tbody tr { border-bottom: 1px solid #eee; }
|
||||
.booking-table tbody tr.is-storno { opacity: .5; background: #fff5f5; }
|
||||
.booking-table tfoot tr:first-child { border-top: 2px solid #e5e7eb; }
|
||||
.booking-table .text-mono { font-family: var(--font-mono); }
|
||||
.booking-table .num-right { text-align: right; }
|
||||
.booking-table .cell-sub { font-size: .7rem; }
|
||||
.booking-table .cell-storno-tag { font-size: .65rem; }
|
||||
.booking-table .cell-empty { padding: 1rem; }
|
||||
|
||||
.invoice-summary-wrap { display: flex; justify-content: flex-end; margin-bottom: 1.5rem; padding-right: .5rem; }
|
||||
.invoice-payment-hint {
|
||||
background: #f0f4ff;
|
||||
border-left: 3px solid #2563eb;
|
||||
padding: .75rem 1rem;
|
||||
margin-bottom: 2rem;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
.invoice-footer-grid {
|
||||
border-top: 1px solid #ccc;
|
||||
padding-top: .75rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr auto;
|
||||
gap: 1rem;
|
||||
font-size: .58rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.booking-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem .75rem; margin-bottom: .75rem; }
|
||||
.booking-form-field { display: flex; align-items: center; gap: .4rem; }
|
||||
.booking-form-field label { font-size: .75rem; font-weight: 600; white-space: nowrap; margin: 0; }
|
||||
.booking-form-title { font-size: .8rem; font-weight: 600; margin-bottom: .75rem; }
|
||||
|
||||
.log-console {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: .8rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius);
|
||||
height: 340px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Pagination ──────────────────────────────────────────────── */
|
||||
.pagination {
|
||||
display: flex;
|
||||
|
|
@ -621,6 +824,7 @@ td { padding: .65rem 1rem; vertical-align: middle; }
|
|||
.pagination .active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
|
||||
|
||||
/* ── Utilities ───────────────────────────────────────────────── */
|
||||
.m-0 { margin: 0; }
|
||||
.text-muted { color: var(--color-muted); }
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
|
|
@ -640,7 +844,25 @@ td { padding: .65rem 1rem; vertical-align: middle; }
|
|||
.align-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.w-full { width: 100%; }
|
||||
.min-w-0 { min-width: 0; }
|
||||
.select-filter { width: auto; min-width: 11rem; }
|
||||
.autosize-textarea { height: 2.4em; resize: none; overflow: hidden; }
|
||||
.beleg-row { cursor: pointer; }
|
||||
.beleg-row--static { cursor: default; }
|
||||
.row-clickable { cursor: pointer; }
|
||||
.btn-round-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: .36rem;
|
||||
border-radius: 50%;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
}
|
||||
.bl-supplier-cell { cursor: text; min-width: 80px; }
|
||||
.mt-xs { margin-top: .2em; }
|
||||
.hidden { display: none; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
|
||||
/* ── Print ───────────────────────────────────────────────────── */
|
||||
@media print {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,13 @@ document.addEventListener('keydown', (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
// ── Klickbare Tabellenzeilen (Navigation) ──────────────────────
|
||||
document.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr[data-row-href]');
|
||||
if (!row || e.target.closest('[data-no-row-click]')) return;
|
||||
window.location = row.dataset.rowHref;
|
||||
});
|
||||
|
||||
// ── Confirm Delete ────────────────────────────────────────────
|
||||
document.addEventListener('submit', (e) => {
|
||||
const form = e.target;
|
||||
|
|
@ -139,6 +146,17 @@ document.addEventListener('input', (e) => {
|
|||
|
||||
// ── Rechnungszeile / Überschrift hinzufügen ───────────────────
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const el = e.target.closest('[data-action]');
|
||||
if (!el) return;
|
||||
switch (el.dataset.action) {
|
||||
case 'show-add-line-dialog': showAddLineDialog(el); break;
|
||||
case 'add-invoice-line': document.getElementById('add-line-popup')?.remove(); addInvoiceLine(); break;
|
||||
case 'add-invoice-heading': document.getElementById('add-line-popup')?.remove(); addInvoiceHeading(); break;
|
||||
case 'remove-invoice-line': removeInvoiceLine(el); break;
|
||||
}
|
||||
});
|
||||
|
||||
function showAddLineDialog(btn) {
|
||||
document.getElementById('add-line-popup')?.remove();
|
||||
|
||||
|
|
@ -146,8 +164,8 @@ function showAddLineDialog(btn) {
|
|||
popup.id = 'add-line-popup';
|
||||
popup.style.cssText = 'position:absolute;z-index:200;background:#fff;border:1px solid #ccc;border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.12);overflow:hidden;font-size:.875rem';
|
||||
popup.innerHTML = `
|
||||
<button class="add-line-opt" onclick="document.getElementById('add-line-popup').remove();addInvoiceHeading()">Ü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-heading">Überschrift</button>
|
||||
<button class="add-line-opt" data-action="add-invoice-line">Neue Position</button>`;
|
||||
document.body.appendChild(popup);
|
||||
|
||||
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-brutto font-bold">0,00 €</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>`;
|
||||
tbody.appendChild(row);
|
||||
renumberLines();
|
||||
|
|
@ -445,6 +463,7 @@ function initBezTextarea(el) {
|
|||
|
||||
document.addEventListener('input', (e) => {
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -149,4 +149,8 @@
|
|||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) closeCropModal();
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener('click', function () { confirmCrop(); });
|
||||
const cancelBtn = document.getElementById('crop-cancel-btn');
|
||||
if (cancelBtn) cancelBtn.addEventListener('click', function () { closeCropModal(); });
|
||||
}());
|
||||
|
|
|
|||
|
|
@ -136,6 +136,67 @@ function updateEigenanteilState() {
|
|||
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
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const checked = document.querySelector('input[name="buchungstyp"]:checked');
|
||||
|
|
|
|||
|
|
@ -143,4 +143,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (e.key === '-') { e.preventDefault(); imgZoom(-1); }
|
||||
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(); });
|
||||
}());
|
||||
|
|
|
|||
|
|
@ -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)">
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer">
|
||||
<input type="checkbox" id="filter-unzugeordnet"
|
||||
<?= $nurUnzugeordnet ? 'checked' : '' ?>
|
||||
onchange="location.href='/assets?filter='+(this.checked?'unzugeordnet':'alle')">
|
||||
<?= $nurUnzugeordnet ? 'checked' : '' ?>>
|
||||
Nur nicht zugeordnete anzeigen
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -215,6 +214,10 @@ $assetsJson = json_encode(array_map(fn($a) => [
|
|||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('filter-unzugeordnet')?.addEventListener('change', function () {
|
||||
location.href = '/assets?filter=' + (this.checked ? 'unzugeordnet' : 'alle');
|
||||
});
|
||||
|
||||
const _anlageAssets = <?= $assetsJson ?>;
|
||||
|
||||
let _assignAccountId = 0;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<!-- Modal: Konto-Auswahl bei mehreren Konten -->
|
||||
<div id="modal-account-select"
|
||||
style="display:none;position:fixed;inset:0;z-index:1100;background:rgba(0,0,0,.4);align-items:center;justify-content:center">
|
||||
<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 style="margin:0;font-size:1.125rem">Konten auswählen</h2>
|
||||
<div id="modal-account-select" class="modal-overlay modal-overlay--stacked">
|
||||
<div class="modal-card col">
|
||||
<h2>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 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 style="display:flex;justify-content:flex-end;gap:.5rem">
|
||||
<div id="account-error" class="text-sm text-danger hidden"></div>
|
||||
<div class="row-end">
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@
|
|||
<form id="form-edit-delete" method="post" action="" style="display:none"></form>
|
||||
|
||||
<!-- Modal: Verbindung bearbeiten -->
|
||||
<div id="modal-edit-connection"
|
||||
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" 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 style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem">
|
||||
<h2 style="margin:0;font-size:1.125rem">Verbindung bearbeiten</h2>
|
||||
<button type="button" id="btn-edit-close"
|
||||
style="background:none;border:none;font-size:1.25rem;cursor:pointer;color:var(--color-muted)">×</button>
|
||||
<div id="modal-edit-connection" class="modal-overlay">
|
||||
<div class="modal-inline-form modal-card">
|
||||
<div class="modal-card-header">
|
||||
<h2>Verbindung bearbeiten</h2>
|
||||
<button type="button" id="btn-edit-close" class="modal-card-close">×</button>
|
||||
</div>
|
||||
|
||||
<form id="form-edit-connection" method="post" action="">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
<!-- Modal: Neue Verbindung -->
|
||||
<div id="modal-new-connection"
|
||||
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" 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 style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem">
|
||||
<h2 style="margin:0;font-size:1.125rem">Neue Direktverbindung</h2>
|
||||
<button type="button" id="btn-modal-close"
|
||||
style="background:none;border:none;font-size:1.25rem;cursor:pointer;color:var(--color-muted)">×</button>
|
||||
<div id="modal-new-connection" class="modal-overlay">
|
||||
<div class="modal-inline-form modal-card">
|
||||
<div class="modal-card-header">
|
||||
<h2>Neue Direktverbindung</h2>
|
||||
<button type="button" id="btn-modal-close" class="modal-card-close">×</button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/bank/direkt">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
<!-- Modal: Abruf-Log -->
|
||||
<div id="modal-run-log"
|
||||
style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.4);align-items:center;justify-content:center">
|
||||
<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 style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h2 style="margin:0;font-size:1.125rem" id="run-log-title">Abruf läuft…</h2>
|
||||
<span id="run-log-spinner" style="font-size:.8rem;color:var(--color-muted)">⏳ wird abgerufen…</span>
|
||||
<div id="modal-run-log" class="modal-overlay">
|
||||
<div class="modal-card modal-card--lg col gap-2">
|
||||
<div class="row-between">
|
||||
<h2 id="run-log-title">Abruf läuft…</h2>
|
||||
<span id="run-log-spinner" class="text-sm text-muted">⏳ wird abgerufen…</span>
|
||||
</div>
|
||||
<div id="run-log-area"
|
||||
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 id="run-log-area" class="log-console">
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<!-- Modal: TAN-Eingabe (photoTAN/chipTAN) -->
|
||||
<div id="modal-tan-input"
|
||||
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" 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 style="margin:0;font-size:1.125rem">TAN eingeben</h2>
|
||||
<div id="modal-tan-input" class="modal-overlay modal-overlay--stacked">
|
||||
<div class="modal-inline-form modal-card col">
|
||||
<h2>TAN eingeben</h2>
|
||||
<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-image-wrap" style="text-align:center"></div>
|
||||
|
|
@ -12,8 +11,8 @@
|
|||
<input type="text" id="tan-input" autocomplete="off" inputmode="numeric"
|
||||
style="font-size:1.25rem;letter-spacing:.25em;text-align:center">
|
||||
</div>
|
||||
<div id="tan-error" class="text-sm" style="color:#dc2626;display:none"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:.5rem">
|
||||
<div id="tan-error" class="text-sm text-danger hidden"></div>
|
||||
<div class="row-end">
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
|
|||
$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>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<td data-no-row-click>
|
||||
<?php if ($row['status'] !== 'ignoriert'): ?>
|
||||
<form method="post" action="/bank/<?= (int)$row['id'] ?>/status">
|
||||
<input type="hidden" name="status" value="ignoriert">
|
||||
|
|
|
|||
|
|
@ -231,11 +231,10 @@ $restFormatted = number_format($remaining, 2, ',', '.');
|
|||
<tbody>
|
||||
<?php foreach ($offeneBelege as $b): ?>
|
||||
<?php $hatBeleg = $b['id'] !== null; ?>
|
||||
<tr class="beleg-row"
|
||||
<tr class="beleg-row <?= $hatBeleg ? '' : 'beleg-row--static' ?>"
|
||||
data-beleg-id="<?= (int)$b['id'] ?>"
|
||||
style="cursor:<?= $hatBeleg ? 'pointer' : 'default' ?>"
|
||||
<?= $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">
|
||||
<?php if ($hatBeleg): ?>
|
||||
<a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
|
||||
|
|
@ -249,10 +248,10 @@ $restFormatted = number_format($remaining, 2, ',', '.');
|
|||
<td>
|
||||
<?= $b['supplier_name'] ? $h($b['supplier_name']) : '<span class="text-muted">–</span>' ?>
|
||||
</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>' ?>
|
||||
</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<td data-no-row-click>
|
||||
<?php if ($hatBeleg): ?>
|
||||
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link">
|
||||
<input type="hidden" name="beleg_id" value="<?= (int)$b['id'] ?>">
|
||||
|
|
@ -307,7 +306,9 @@ $restFormatted = number_format($remaining, 2, ',', '.');
|
|||
let activeRow = null;
|
||||
|
||||
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;
|
||||
if (!id) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -47,14 +47,12 @@
|
|||
<div style="display:flex;gap:1rem">
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer">
|
||||
<input type="radio" name="buchungstyp" value="Ausgabe"
|
||||
<?= $defaultTyp === 'Ausgabe' ? 'checked' : '' ?>
|
||||
onchange="filterKategorieKonten(this.value)">
|
||||
<?= $defaultTyp === 'Ausgabe' ? 'checked' : '' ?>>
|
||||
<span class="badge badge-red">Betriebsausgabe</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer">
|
||||
<input type="radio" name="buchungstyp" value="Einnahme"
|
||||
<?= $defaultTyp === 'Einnahme' ? 'checked' : '' ?>
|
||||
onchange="filterKategorieKonten(this.value)">
|
||||
<?= $defaultTyp === 'Einnahme' ? 'checked' : '' ?>>
|
||||
<span class="badge badge-green">Betriebseinnahme</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -70,7 +68,7 @@
|
|||
<div class="form-group">
|
||||
<label>USt-Satz (%)</label>
|
||||
<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): ?>
|
||||
<option value="<?= $s ?>"<?= ($formData['ust_satz'] ?? '19') === $s ? ' selected' : '' ?>><?= $s ?> %</option>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -107,8 +105,7 @@
|
|||
<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"
|
||||
value="<?= $h($formData['kategorie_kontonummer'] ?? '') ?>">
|
||||
<select id="kategorie_konto_id" name="kategorie_konto_id" size="6"
|
||||
onchange="showKontoArt('kategorie_konto_id','kategorie_konto_art',this);updateEigenanteilState();updateBewirtungsState();updateAutoUst();checkAnlagevermoegen()">
|
||||
<select id="kategorie_konto_id" name="kategorie_konto_id" size="6">
|
||||
<option value="">– Konto wählen –</option>
|
||||
<?php foreach (['Betriebsausgabe', 'Betriebseinnahme', 'Anlagevermögen', 'Privat', 'Verbindlichkeit'] as $art): ?>
|
||||
<?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"
|
||||
data-target="zahlungskonto_id" autocomplete="off" style="margin-bottom:.3rem"
|
||||
value="<?= $h($formData['zahlungs_kontonummer'] ?? '') ?>">
|
||||
<select id="zahlungskonto_id" name="zahlungskonto_id" size="4"
|
||||
onchange="showKontoArt('zahlungskonto_id','zahlungskonto_art',this)">
|
||||
<select id="zahlungskonto_id" name="zahlungskonto_id" size="4">
|
||||
<option value="">– Konto wählen –</option>
|
||||
<?php foreach (['Zahlungskonto', 'Verbindlichkeit'] as $art): ?>
|
||||
<?php if (empty($kontenGruppen[$art])) continue; ?>
|
||||
|
|
@ -157,15 +153,14 @@
|
|||
<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">
|
||||
<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"
|
||||
aria-pressed="false" onclick="setBetragModus('brutto')">BRUTTOBETRAG (€)</button>
|
||||
aria-pressed="false">BRUTTOBETRAG (€)</button>
|
||||
</div>
|
||||
<input type="number" name="betrag_netto" id="betrag_netto_input" step="0.01" required
|
||||
placeholder="0.00"
|
||||
value="<?= $h($formData['betrag_netto'] ?? '') ?>"
|
||||
class="<?= isset($formErrors['betrag_netto']) ? 'input-error' : '' ?>"
|
||||
oninput="updateNettoAnzeige()">
|
||||
class="<?= isset($formErrors['betrag_netto']) ? 'input-error' : '' ?>">
|
||||
<p id="ust_brutto_anzeige" class="text-muted text-sm" style="margin-top:.25rem;line-height:1.6"></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
@ -192,8 +187,7 @@
|
|||
<div class="form-group">
|
||||
<label for="bewirtungsgrund_input">Bewirtungsgrund *</label>
|
||||
<textarea name="bewirtungsgrund" id="bewirtungsgrund_input"
|
||||
style="height:2.4em;resize:none;overflow:hidden"
|
||||
oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"
|
||||
class="autosize-textarea"
|
||||
placeholder="Anlass/Grund der Bewirtung"><?= $h($formData['bewirtungsgrund'] ?? $beleg['bewirtungsgrund'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- 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']) ?>">
|
||||
<div class="modal" style="max-width:95vw;width:auto;padding:1.25rem">
|
||||
<div class="modal-title" style="margin-bottom:.75rem">Beleg zuschneiden</div>
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
</div>
|
||||
<style>@keyframes crop-spin{to{transform:rotate(360deg)}}</style>
|
||||
<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-secondary btn-sm" onclick="closeCropModal()">Abbrechen</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" id="crop-cancel-btn">Abbrechen</button>
|
||||
<span id="crop-size-hint" class="text-muted text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -97,11 +97,10 @@
|
|||
<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>
|
||||
<?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 if (!$beleg['locked']): ?>
|
||||
<button type="button" class="btn btn-secondary btn-sm"
|
||||
onclick="document.getElementById('beleg-meta-form').style.display = document.getElementById('beleg-meta-form').style.display === 'none' ? 'block' : 'none'">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="beleg-meta-toggle-btn">
|
||||
Metadaten bearbeiten
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
|
@ -160,8 +159,7 @@
|
|||
</datalist>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Speichern</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm"
|
||||
onclick="document.getElementById('beleg-meta-form').style.display='none'">Abbrechen</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="beleg-meta-cancel-btn">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
<iframe src="/belege/<?= $h($beleg['id']) ?>/file" title="Belegvorschau"></iframe>
|
||||
<?php elseif (str_starts_with($beleg['mime_type'], 'image/')): ?>
|
||||
<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" onclick="imgZoom(+1)" 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-out-btn" title="Verkleinern (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" id="img-zoom-reset-btn" title="Zurücksetzen (Strg+0)">↺</button>
|
||||
<span id="img-zoom-level">100 %</span>
|
||||
</div>
|
||||
<div id="img-zoom-wrap" class="img-zoom-wrap">
|
||||
|
|
|
|||
|
|
@ -147,11 +147,10 @@
|
|||
<?php foreach ($rows as $b): ?>
|
||||
<tr class="beleg-row"
|
||||
data-beleg-id="<?= (int)$b['id'] ?>"
|
||||
style="cursor:pointer"
|
||||
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>
|
||||
<div style="margin-top:.2em">
|
||||
<div class="mt-xs">
|
||||
<?php if ($b['locked']): ?>
|
||||
<span class="badge badge-purple">Gesperrt</span>
|
||||
<?php elseif ($b['booked']): ?>
|
||||
|
|
@ -165,15 +164,14 @@
|
|||
<td class="td-muted bl-supplier-cell"
|
||||
data-beleg-id="<?= (int)$b['id'] ?>"
|
||||
data-supplier="<?= $h($b['supplier_name'] ?? '') ?>"
|
||||
onclick="event.stopPropagation(); blInlineSupplier(this)"
|
||||
style="cursor:text;min-width:80px"
|
||||
data-no-row-click
|
||||
title="Klicken zum Lieferant bearbeiten"
|
||||
><?= $h($b['supplier_name'] ?? '–') ?></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 onclick="event.stopPropagation()">
|
||||
<td data-no-row-click>
|
||||
<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">
|
||||
<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">
|
||||
|
|
@ -243,7 +241,9 @@
|
|||
let activeRow = null;
|
||||
|
||||
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;
|
||||
if (!id) return;
|
||||
|
||||
|
|
@ -256,6 +256,10 @@
|
|||
frame.src = '/belege/' + id + '/file';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.bl-supplier-cell').forEach(function (td) {
|
||||
td.addEventListener('click', function () { blInlineSupplier(td); });
|
||||
});
|
||||
})();
|
||||
|
||||
function blInlineSupplier(td) {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
|
|||
<div class="card" style="margin-bottom:1rem">
|
||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<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 class="card-body" style="padding:0">
|
||||
<table class="data-table" id="vorlage-felder-table">
|
||||
|
|
@ -85,7 +85,7 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
|
|||
</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><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>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
|
@ -97,7 +97,7 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
|
|||
<div class="card" style="margin-bottom:1rem">
|
||||
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<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 class="card-body" style="padding:0">
|
||||
<div id="vorlage-regeln-container">
|
||||
|
|
@ -115,14 +115,14 @@ $bedOptn = ['neq_zero' => 'wenn ≠ 0', 'always' => 'immer'];
|
|||
</select>
|
||||
<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">
|
||||
<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>
|
||||
<table class="data-table" style="margin:0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Betrag-Referenz</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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
|
@ -167,7 +167,7 @@ function vorlageAddFeld() {
|
|||
</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="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);
|
||||
}
|
||||
|
||||
|
|
@ -188,18 +188,18 @@ function vorlageAddRegel() {
|
|||
</select>
|
||||
<input type="number" name="regeln[${ri}][sort_order]" value="${ri*10}"
|
||||
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>
|
||||
<table class="data-table" style="margin:0">
|
||||
<thead><tr>
|
||||
<th>Betrag-Referenz</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>
|
||||
<tbody id="vorlage-buchungen-${ri}"></tbody>
|
||||
</table>`;
|
||||
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) {
|
||||
|
|
@ -212,7 +212,20 @@ function vorlageAddBuchung(btn, ri) {
|
|||
value="" class="form-control form-control-sm" placeholder="z.B. netto_7"></td>
|
||||
<td><input type="text" name="regeln[${ri}][buchungen][${bi}][nach_konto]"
|
||||
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);
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
</svg>
|
||||
</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">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
<div class="card">
|
||||
<div class="card-header">
|
||||
<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">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ $levelLabels = [
|
|||
<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)">
|
||||
<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>
|
||||
<?php foreach ($channels as $c): ?>
|
||||
<option value="<?= $h($c) ?>"<?= $c === $aktiverKanal ? ' selected' : '' ?>><?= $h($c) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</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>
|
||||
<?php foreach ($levelLabels as $value => $label): ?>
|
||||
<option value="<?= $h($value) ?>"<?= $value === $aktivesLevel ? ' selected' : '' ?>><?= $h($label) ?></option>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ $preCustomer = (int)($_GET['customer_id'] ?? 0);
|
|||
<div class="form-group">
|
||||
<label for="customer_id">Kunde *</label>
|
||||
<select id="customer_id" name="customer_id"
|
||||
class="<?= isset($errors['customer_id']) ? 'input-error' : '' ?>" required
|
||||
onchange="updatePaymentDays(this)">
|
||||
class="<?= isset($errors['customer_id']) ? 'input-error' : '' ?>" required>
|
||||
<option value="">– Kunde wählen –</option>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
<?php $selected = ($invoice['customer_id'] ?? $preCustomer) == $c['id'] ? ' selected' : ''; ?>
|
||||
|
|
@ -59,7 +58,7 @@ $preCustomer = (int)($_GET['customer_id'] ?? 0);
|
|||
<div class="mb-2">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem">
|
||||
<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">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
|
|
@ -108,7 +107,7 @@ $preCustomer = (int)($_GET['customer_id'] ?? 0);
|
|||
<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>
|
||||
</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>
|
||||
<?php else: ?>
|
||||
<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 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><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>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -175,6 +174,7 @@ function updatePaymentDays(sel) {
|
|||
const days = opt.dataset.payment;
|
||||
if (days) document.getElementById('payment_days').value = days;
|
||||
}
|
||||
document.getElementById('customer_id')?.addEventListener('change', function () { updatePaymentDays(this); });
|
||||
<?php if (!$isEdit): ?>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sel = document.getElementById('customer_id');
|
||||
|
|
|
|||
|
|
@ -25,25 +25,25 @@ $badgeClass = match($invoice['status']) {
|
|||
|
||||
<!-- Querverweise Storno -->
|
||||
<?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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Hinweis auf fehlende Daten für die XML-Rechnung (EN 16931) -->
|
||||
<?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>
|
||||
Der Download ist trotzdem möglich, die Datei erfüllt die Norm aber nur eingeschränkt.
|
||||
<?php foreach ($xmlHinweise as $bereich => $felder): ?>
|
||||
<div style="margin-top:.4rem">
|
||||
<div class="mt-1">
|
||||
<em><?= $h($bereich) ?>:</em>
|
||||
<ul style="margin:.2rem 0 0 1.2rem">
|
||||
<ul class="xml-hinweis-list">
|
||||
<?php foreach ($felder as $feld): ?>
|
||||
<li><?= $h($feld) ?></li>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -51,7 +51,7 @@ $badgeClass = match($invoice['status']) {
|
|||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?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; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
|
@ -59,12 +59,12 @@ $badgeClass = match($invoice['status']) {
|
|||
<!-- Aktionsleiste -->
|
||||
<div class="toolbar mb-2">
|
||||
<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 class="toolbar-right" style="gap:.5rem">
|
||||
<div class="toolbar-right">
|
||||
<?php if ($invoice['status'] === 'Entwurf'): ?>
|
||||
<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">
|
||||
<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">
|
||||
|
|
@ -79,7 +79,7 @@ $badgeClass = match($invoice['status']) {
|
|||
<?php endif; ?>
|
||||
<?php if ($invoice['status'] !== 'Entwurf'): ?>
|
||||
<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">
|
||||
<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">
|
||||
|
|
@ -90,7 +90,8 @@ $badgeClass = match($invoice['status']) {
|
|||
</a>
|
||||
<?php endif; ?>
|
||||
<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) ?>">
|
||||
<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">
|
||||
|
|
@ -101,12 +102,10 @@ $badgeClass = match($invoice['status']) {
|
|||
</svg>
|
||||
</a>
|
||||
<?php if (!in_array($invoice['status'], ['Entwurf', 'Storniert'])): ?>
|
||||
<button type="button" class="btn btn-primary"
|
||||
onclick="document.getElementById('send-modal').style.display='flex'">Per E-Mail senden</button>
|
||||
<button type="button" class="btn btn-primary" data-modal-open="send-modal">Per E-Mail senden</button>
|
||||
<?php endif; ?>
|
||||
<?php if (!in_array($invoice['status'], ['Entwurf', 'Storniert']) && !empty($invoice['beleg_id'])): ?>
|
||||
<button type="button" class="btn btn-success"
|
||||
onclick="document.getElementById('buchungs-modal').style.display='flex'">Zahlungen</button>
|
||||
<button type="button" class="btn btn-success" data-modal-open="buchungs-modal">Zahlungen</button>
|
||||
<?php endif; ?>
|
||||
<?php if (in_array($invoice['status'], ['Entwurf','Finalisiert','Gesendet']) && empty($invoice['storno_of_id'])): ?>
|
||||
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/cancel"
|
||||
|
|
@ -127,40 +126,34 @@ $badgeClass = match($invoice['status']) {
|
|||
<?php
|
||||
$logoMaxW = max(50, (int)($settings['logo_max_width'] ?? 600));
|
||||
$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));
|
||||
$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"
|
||||
style="max-width:<?= $logoMaxW ?>px;max-height:<?= $logoMaxH ?>px">
|
||||
style="--logo-maxw: <?= $logoMaxW ?>px; --logo-maxh: <?= $logoMaxH ?>px">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 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 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'] ?? '')) ?>
|
||||
</div>
|
||||
<div style="line-height:1.5;font-size:10pt">
|
||||
<div class="invoice-recipient-block">
|
||||
<?= $h($customerName) ?><br>
|
||||
<?= $h($invoice['strasse']) ?><br>
|
||||
<?= $h($invoice['plz']) ?> <?= $h($invoice['ort']) ?>
|
||||
<?php if ($invoice['land'] !== 'DE'): ?><br><?= $h($invoice['land']) ?><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;font-size:17pt">
|
||||
<div class="invoice-title-block">
|
||||
<?= !empty($invoice['storno_of_id']) ? 'STORNORECHNUNG' : 'Rechnung' ?>
|
||||
<?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>
|
||||
vom <?= $h(View::formatDate($originalrechnung['rechnungs_datum'] ?? '')) ?>
|
||||
</div>
|
||||
|
|
@ -169,55 +162,55 @@ $badgeClass = match($invoice['status']) {
|
|||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<tr>
|
||||
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Betreff</th>
|
||||
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Rechnungsnummer</th>
|
||||
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Kundennummer</th>
|
||||
<th style="width:20%;padding:.4rem .5rem;text-align:left;font-weight:bold">Leistungszeitraum</th>
|
||||
<th style="width:20%;padding:.4rem .5rem;text-align:right;font-weight:bold">Datum</th>
|
||||
<th>Betreff</th>
|
||||
<th>Rechnungsnummer</th>
|
||||
<th>Kundennummer</th>
|
||||
<th>Leistungszeitraum</th>
|
||||
<th>Datum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:.4rem .5rem;white-space:nowrap"><?= $h($invoice['betreff']) ?></td>
|
||||
<td style="padding:.4rem .5rem;white-space:nowrap;font-weight:700"><?= $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?></td>
|
||||
<td style="padding:.4rem .5rem;white-space:nowrap"><?= $h($invoice['kundennummer']) ?></td>
|
||||
<td style="padding:.4rem .5rem"><?= $lzHtml ?: '–' ?></td>
|
||||
<td style="padding:.4rem .5rem;text-align:right"><?= $h(View::formatDate($invoice['rechnungs_datum'])) ?></td>
|
||||
<td class="nowrap"><?= $h($invoice['betreff']) ?></td>
|
||||
<td class="nowrap font-bold"><?= $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?></td>
|
||||
<td class="nowrap"><?= $h($invoice['kundennummer']) ?></td>
|
||||
<td><?= $lzHtml ?: '–' ?></td>
|
||||
<td><?= $h(View::formatDate($invoice['rechnungs_datum'])) ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 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>
|
||||
<tr style="background:#f0f0f0">
|
||||
<th style="padding:.5rem;text-align:left;width:6%">Pos.</th>
|
||||
<th style="padding:.5rem;text-align:left">Bezeichnung</th>
|
||||
<th style="padding:.5rem;text-align:right;width:11%">Menge</th>
|
||||
<th style="padding:.5rem;text-align:left;width:10%">Einheit</th>
|
||||
<th style="padding:.5rem;text-align:right;width:8%">USt.</th>
|
||||
<th style="padding:.5rem;text-align:right;width:15%">Einzelpreis</th>
|
||||
<th style="padding:.5rem;text-align:right;width:16%">Gesamt</th>
|
||||
<tr>
|
||||
<th>Pos.</th>
|
||||
<th>Bezeichnung</th>
|
||||
<th>Menge</th>
|
||||
<th>Einheit</th>
|
||||
<th>USt.</th>
|
||||
<th>Einzelpreis</th>
|
||||
<th>Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($lines as $line): ?>
|
||||
<?php if (($line['type'] ?? 'position') === 'heading'): ?>
|
||||
<tr style="border-bottom:1px solid #eee">
|
||||
<td colspan="7" style="padding:.4rem .5rem;font-weight:700"><?= nl2br($h($line['bezeichnung'])) ?></td>
|
||||
<tr class="row-heading">
|
||||
<td colspan="7"><?= nl2br($h($line['bezeichnung'])) ?></td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<tr style="border-bottom:1px solid #eee">
|
||||
<td style="padding:.4rem .5rem"><?= $h($line['position']) ?></td>
|
||||
<td style="padding:.4rem .5rem"><?= nl2br($h($line['bezeichnung'])) ?></td>
|
||||
<td style="padding:.4rem .5rem;text-align:right"><?= number_format((float)$line['menge'], 1, ',', '.') ?></td>
|
||||
<td style="padding:.4rem .5rem"><?= $h($line['einheit']) ?></td>
|
||||
<td style="padding:.4rem .5rem;text-align:right"><?= number_format((float)$line['ust_satz'], 0) ?> %</td>
|
||||
<td style="padding:.4rem .5rem;text-align:right"><?= $fmt($line['einzelpreis']) ?></td>
|
||||
<td style="padding:.4rem .5rem;text-align:right;font-weight:600"><?= $fmt($line['betrag_netto']) ?></td>
|
||||
<tr>
|
||||
<td><?= $h($line['position']) ?></td>
|
||||
<td><?= nl2br($h($line['bezeichnung'])) ?></td>
|
||||
<td><?= number_format((float)$line['menge'], 1, ',', '.') ?></td>
|
||||
<td><?= $h($line['einheit']) ?></td>
|
||||
<td><?= number_format((float)$line['ust_satz'], 0) ?> %</td>
|
||||
<td><?= $fmt($line['einzelpreis']) ?></td>
|
||||
<td class="col-total"><?= $fmt($line['betrag_netto']) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -226,18 +219,18 @@ $badgeClass = match($invoice['status']) {
|
|||
|
||||
<!-- Summen + Zahlungshinweis -->
|
||||
<div id="invoice-summary">
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:1.5rem;padding-right:.5rem">
|
||||
<table style="width:auto">
|
||||
<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 style="padding:.3rem 0">zzgl. Umsatzsteuer:</td><td style="text-align:right;padding:.3rem 0"><?= $fmt($invoice['betrag_ust']) ?></td></tr>
|
||||
<tr style="font-weight:700;font-size:.82rem">
|
||||
<td style="padding:.5rem 0">Rechnungsbetrag:</td>
|
||||
<td style="text-align:right;padding:.5rem 0"><?= $fmt($invoice['betrag_brutto']) ?></td>
|
||||
<div class="invoice-summary-wrap">
|
||||
<table class="invoice-summary-table">
|
||||
<tr><td>Nettobetrag:</td><td><?= $fmt($invoice['betrag_netto']) ?></td></tr>
|
||||
<tr><td>zzgl. Umsatzsteuer:</td><td><?= $fmt($invoice['betrag_ust']) ?></td></tr>
|
||||
<tr class="row-total">
|
||||
<td>Rechnungsbetrag:</td>
|
||||
<td><?= $fmt($invoice['betrag_brutto']) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<?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.
|
||||
<strong>Fällig am: <?= View::formatDate($invoice['faellig_datum']) ?></strong>
|
||||
</div>
|
||||
|
|
@ -248,25 +241,25 @@ $badgeClass = match($invoice['status']) {
|
|||
|
||||
<!-- Fußzeile -->
|
||||
<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 style="min-width:0">
|
||||
<div class="invoice-footer-grid">
|
||||
<div class="min-w-0">
|
||||
<strong><?= $h($settings['firma'] ?? '') ?></strong><br>
|
||||
<?php if (trim((string)($settings['inhaber'] ?? '')) !== ''): ?>Inh. <?= $h($settings['inhaber']) ?><br><?php endif; ?>
|
||||
<?= $h($settings['strasse'] ?? '') ?><br>
|
||||
<?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?><br>
|
||||
USt-ID: <?= $h($settings['ust_id'] ?? '') ?>
|
||||
</div>
|
||||
<div style="white-space:nowrap">
|
||||
<div class="nowrap">
|
||||
Tel: <?= $h($settings['telefon'] ?? '') ?><br>
|
||||
E-Mail: <?= $h($settings['email'] ?? '') ?><br>
|
||||
Web: <?= $h($settings['website'] ?? '') ?>
|
||||
</div>
|
||||
<div style="white-space:nowrap">
|
||||
<div class="nowrap">
|
||||
<?= $h($settings['bank_name'] ?? '') ?><br>
|
||||
IBAN: <?= $h($settings['iban'] ?? '') ?><br>
|
||||
BIC: <?= $h($settings['bic'] ?? '') ?>
|
||||
</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>
|
||||
|
||||
|
|
@ -395,9 +388,9 @@ $badgeClass = match($invoice['status']) {
|
|||
</script>
|
||||
|
||||
<!-- 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 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)">
|
||||
<h3 style="margin:0 0 1rem">Rechnung per E-Mail senden</h3>
|
||||
<div id="send-modal" class="modal-overlay">
|
||||
<div class="modal-inline-form modal-card-invoice modal-card-invoice--sm">
|
||||
<h3 class="modal-form-title">Rechnung per E-Mail senden</h3>
|
||||
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/send">
|
||||
<div class="form-group">
|
||||
<label>Empfänger *</label>
|
||||
|
|
@ -415,9 +408,8 @@ $badgeClass = match($invoice['status']) {
|
|||
<input type="email" name="email_cc2" placeholder="cc2@example.com"
|
||||
value="<?= $h($customer['email_cc2'] ?? '') ?>">
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;justify-content:flex-end;margin-top:1.25rem">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
onclick="document.getElementById('send-modal').style.display='none'">Abbrechen</button>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Senden</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -429,48 +421,47 @@ $badgeClass = match($invoice['status']) {
|
|||
<?php
|
||||
$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 style="background:#fff;border-radius:8px;width:100%;max-width:720px;box-shadow:0 8px 32px rgba(0,0,0,.2)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid #e5e7eb">
|
||||
<h3 style="margin:0">Zahlungen – <?= $h($invoice['rechnungs_nr']) ?></h3>
|
||||
<button onclick="document.getElementById('buchungs-modal').style.display='none'"
|
||||
style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1">×</button>
|
||||
<div id="buchungs-modal" class="modal-overlay modal-overlay--scroll">
|
||||
<div class="modal-card-invoice modal-card-invoice--lg">
|
||||
<div class="modal-section modal-section-border row-between">
|
||||
<h3 class="m-0">Zahlungen – <?= $h($invoice['rechnungs_nr']) ?></h3>
|
||||
<button data-modal-close class="modal-card-close modal-card-close--lg">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Vorhandene Buchungen -->
|
||||
<div style="padding:1rem 1.25rem;border-bottom:1px solid #e5e7eb;overflow-x:auto">
|
||||
<table style="width:100%;font-size:.75rem;border-collapse:collapse">
|
||||
<div class="modal-section modal-section-border table-wrap">
|
||||
<table class="booking-table">
|
||||
<thead>
|
||||
<tr style="background:#f0f0f0">
|
||||
<th style="padding:.4rem .5rem;text-align:left">Datum</th>
|
||||
<th style="padding:.4rem .5rem;text-align:left">Kategorie</th>
|
||||
<th style="padding:.4rem .5rem;text-align:left">Zahlungskonto</th>
|
||||
<th style="padding:.4rem .5rem;text-align:right">Netto</th>
|
||||
<th style="padding:.4rem .5rem;text-align:right">USt</th>
|
||||
<th style="padding:.4rem .5rem;text-align:right">Brutto</th>
|
||||
<th style="padding:.4rem .5rem">Text</th>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Zahlungskonto</th>
|
||||
<th class="num-right">Netto</th>
|
||||
<th class="num-right">USt</th>
|
||||
<th class="num-right">Brutto</th>
|
||||
<th>Text</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($bookings as $bk):
|
||||
$bkStorno = (bool)$bk['is_reversal'] || !empty($bk['reversed_by']);
|
||||
?>
|
||||
<tr style="border-bottom:1px solid #eee;<?= $bkStorno ? 'opacity:.5;background:#fff5f5' : '' ?>">
|
||||
<td style="padding:.35rem .5rem;white-space: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 style="padding:.35rem .5rem;font-size:.7rem"><?= $h($bk['zahlungs_nr']) ?><br><span style="color:#6b7280"><?= $h($bk['zahlungs_name']) ?></span></td>
|
||||
<td style="padding:.35rem .5rem;text-align:right;font-family:monospace"><?= $fmt($bk['betrag_netto']) ?></td>
|
||||
<td style="padding:.35rem .5rem;text-align: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 style="padding:.35rem .5rem;color:#6b7280;font-size:.7rem">
|
||||
<tr<?= $bkStorno ? ' class="is-storno"' : '' ?>>
|
||||
<td class="nowrap"><?= $h(View::formatDate($bk['buchungs_datum'])) ?></td>
|
||||
<td class="cell-sub"><?= $h($bk['kategorie_nr']) ?><br><span class="text-muted"><?= $h($bk['kategorie_name']) ?></span></td>
|
||||
<td class="cell-sub"><?= $h($bk['zahlungs_nr']) ?><br><span class="text-muted"><?= $h($bk['zahlungs_name']) ?></span></td>
|
||||
<td class="num-right text-mono"><?= $fmt($bk['betrag_netto']) ?></td>
|
||||
<td class="num-right"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td>
|
||||
<td class="num-right text-mono font-bold"><?= $fmt($bk['betrag_brutto']) ?></td>
|
||||
<td class="text-muted cell-sub">
|
||||
<?= $h($bk['buchungstext']) ?>
|
||||
<?php if ((bool)$bk['is_reversal']): ?><span style="color:#dc2626;font-size:.65rem">[Storno]</span>
|
||||
<?php elseif (!empty($bk['reversed_by'])): ?><span style="color:#dc2626;font-size:.65rem">[Storniert]</span><?php endif; ?>
|
||||
<?php if ((bool)$bk['is_reversal']): ?><span class="text-danger cell-storno-tag">[Storno]</span>
|
||||
<?php elseif (!empty($bk['reversed_by'])): ?><span class="text-danger cell-storno-tag">[Storniert]</span><?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?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; ?>
|
||||
</tbody>
|
||||
<?php
|
||||
|
|
@ -478,19 +469,19 @@ $badgeClass = match($invoice['status']) {
|
|||
$offen = round((float)$invoice['betrag_brutto'] - $gezahlt, 2);
|
||||
?>
|
||||
<tfoot>
|
||||
<tr style="border-top:2px solid #e5e7eb">
|
||||
<td colspan="5" style="padding:.4rem .5rem;text-align:right">Rechnungsbetrag</td>
|
||||
<td style="padding:.4rem .5rem;text-align:right;font-family:monospace"><?= $fmt($invoice['betrag_brutto']) ?></td>
|
||||
<tr>
|
||||
<td colspan="5" class="num-right">Rechnungsbetrag</td>
|
||||
<td class="num-right text-mono"><?= $fmt($invoice['betrag_brutto']) ?></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" style="padding:.4rem .5rem;text-align:right">Gezahlt</td>
|
||||
<td style="padding:.4rem .5rem;text-align:right;font-family:monospace"><?= $fmt($gezahlt) ?></td>
|
||||
<td colspan="5" class="num-right">Gezahlt</td>
|
||||
<td class="num-right text-mono"><?= $fmt($gezahlt) ?></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" style="padding:.4rem .5rem;text-align:right;font-weight:600"><?= $offen < 0 ? 'Überzahlt' : 'Offen' ?></td>
|
||||
<td style="padding:.4rem .5rem;text-align:right;font-family:monospace;font-weight:600"><?= $fmt(abs($offen)) ?></td>
|
||||
<td colspan="5" class="num-right font-bold"><?= $offen < 0 ? 'Überzahlt' : 'Offen' ?></td>
|
||||
<td class="num-right text-mono font-bold"><?= $fmt(abs($offen)) ?></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
|
@ -509,35 +500,35 @@ $badgeClass = match($invoice['status']) {
|
|||
$ustVorgabe = (string)(int)round($ustQuote * 100);
|
||||
if (!in_array($ustVorgabe, ['0', '7', '19'], true)) $ustVorgabe = '19';
|
||||
?>
|
||||
<div style="padding:1rem 1.25rem">
|
||||
<div style="font-size:.8rem;font-weight:600;margin-bottom:.75rem">Neue Buchung erfassen</div>
|
||||
<div class="modal-section">
|
||||
<div class="booking-form-title">Neue Buchung erfassen</div>
|
||||
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/bookings">
|
||||
<input type="hidden" name="_from_modal" value="1">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem .75rem;margin-bottom:.75rem">
|
||||
<div style="display:flex;align-items:center;gap:.4rem">
|
||||
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Zahldatum *</label>
|
||||
<input type="date" name="buchungs_datum" value="<?= date('Y-m-d') ?>" required style="width:100%;min-width:0">
|
||||
<div class="booking-form-row">
|
||||
<div class="booking-form-field">
|
||||
<label>Zahldatum *</label>
|
||||
<input type="date" name="buchungs_datum" value="<?= date('Y-m-d') ?>" required class="w-full min-w-0">
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.4rem">
|
||||
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Nettobetrag (€) *</label>
|
||||
<div class="booking-form-field">
|
||||
<label>Nettobetrag (€) *</label>
|
||||
<input type="number" name="betrag_netto" step="0.01" min="0.01" required
|
||||
value="<?= $h(number_format(abs($vorschlagNet), 2, '.', '')) ?>"
|
||||
style="width:100%;min-width:0">
|
||||
class="w-full min-w-0">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem .75rem;margin-bottom:.75rem">
|
||||
<div style="display:flex;align-items:center;gap:.4rem">
|
||||
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Kategorie-Konto *</label>
|
||||
<select name="kategorie_konto_id" required style="width:100%;min-width:0">
|
||||
<div class="booking-form-row">
|
||||
<div class="booking-form-field">
|
||||
<label>Kategorie-Konto *</label>
|
||||
<select name="kategorie_konto_id" required class="w-full min-w-0">
|
||||
<option value="">– Konto wählen –</option>
|
||||
<?php foreach ($kontenGruppen['Betriebseinnahme'] ?? [] as $acc): ?>
|
||||
<option value="<?= $h($acc['id']) ?>"><?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.4rem">
|
||||
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Zahlungskonto *</label>
|
||||
<select name="zahlungskonto_id" required style="width:100%;min-width:0">
|
||||
<div class="booking-form-field">
|
||||
<label>Zahlungskonto *</label>
|
||||
<select name="zahlungskonto_id" required class="w-full min-w-0">
|
||||
<option value="">– Konto wählen –</option>
|
||||
<?php foreach ($kontenGruppen['Zahlungskonto'] ?? [] as $acc): ?>
|
||||
<option value="<?= $h($acc['id']) ?>"><?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?></option>
|
||||
|
|
@ -545,25 +536,24 @@ $badgeClass = match($invoice['status']) {
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem .75rem;margin-bottom:.75rem">
|
||||
<div style="display:flex;align-items:center;gap:.4rem">
|
||||
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">USt-Satz (%)</label>
|
||||
<select name="ust_satz" style="width:100%;min-width:0">
|
||||
<div class="booking-form-row">
|
||||
<div class="booking-form-field">
|
||||
<label>USt-Satz (%)</label>
|
||||
<select name="ust_satz" class="w-full min-w-0">
|
||||
<?php foreach (['0','7','19'] as $s): ?>
|
||||
<option value="<?= $s ?>"<?= $s === $ustVorgabe ? ' selected' : '' ?>><?= $s ?> %</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.4rem">
|
||||
<label style="font-size:.75rem;font-weight:600;white-space:nowrap;margin:0">Buchungstext *</label>
|
||||
<div class="booking-form-field">
|
||||
<label>Buchungstext *</label>
|
||||
<input type="text" name="buchungstext" required
|
||||
value="Ausgangsrechnung <?= $h($invoice['rechnungs_nr'] ?? '') ?>"
|
||||
style="width:100%;min-width:0">
|
||||
class="w-full min-w-0">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;justify-content:flex-end">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
onclick="document.getElementById('buchungs-modal').style.display='none'">Schließen</button>
|
||||
<div class="row-end">
|
||||
<button type="button" class="btn btn-secondary" data-modal-close>Schließen</button>
|
||||
<button type="submit" class="btn btn-primary">Buchung erfassen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -576,7 +566,7 @@ $badgeClass = match($invoice['status']) {
|
|||
if (flash) {
|
||||
var ref = document.referrer || '';
|
||||
if (ref.indexOf('/invoices/<?= (int)$invoice['id'] ?>/bookings') !== -1) {
|
||||
document.getElementById('buchungs-modal').style.display = 'flex';
|
||||
document.getElementById('buchungs-modal').classList.add('open');
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
|
|
|||
|
|
@ -144,8 +144,7 @@
|
|||
<div style="position:relative;width:48px;height:26px;flex-shrink:0">
|
||||
<input type="checkbox" name="test_mode" id="test_mode_toggle"
|
||||
<?= ($settings['test_mode'] ?? 1) ? 'checked' : '' ?>
|
||||
style="opacity:0;width:0;height:0;position:absolute"
|
||||
onchange="updateModeLabel(this)">
|
||||
style="opacity:0;width:0;height:0;position:absolute">
|
||||
<span id="toggle_track" style="
|
||||
position:absolute;inset:0;border-radius:13px;transition:background .2s;
|
||||
background:<?= ($settings['test_mode'] ?? 1) ? '#f59e0b' : '#22c55e' ?>;
|
||||
|
|
@ -454,8 +453,7 @@
|
|||
<?php foreach (['center' => 'Zentriert', 'left' => 'Links', 'right' => 'Rechts'] as $val => $label): ?>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer">
|
||||
<input type="radio" name="logo_position" value="<?= $val ?>" form="settings-form"
|
||||
<?= ($settings['logo_position'] ?? 'center') === $val ? 'checked' : '' ?>
|
||||
onchange="logoPositionChanged()">
|
||||
<?= ($settings['logo_position'] ?? 'center') === $val ? 'checked' : '' ?>>
|
||||
<?= $label ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -618,6 +616,11 @@ function logoPositionChanged() {
|
|||
(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 () {
|
||||
const btn = this;
|
||||
const result = document.getElementById('app-key-result');
|
||||
|
|
|
|||
|
|
@ -24,14 +24,17 @@
|
|||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary w-full" onclick="
|
||||
var y=document.getElementById('ustva-year').value;
|
||||
var m=document.getElementById('ustva-month').value;
|
||||
window.location='/tax/ustva/'+y+'/'+m;
|
||||
">UStVA anzeigen</button>
|
||||
<button type="button" class="btn btn-primary w-full" id="ustva-nav-btn">UStVA anzeigen</button>
|
||||
</form>
|
||||
</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 -->
|
||||
<div class="card">
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ $editId = $formData['edit_id'] ?? null;
|
|||
<?= $editId ? 'Speichern' : 'Anlegen' ?>
|
||||
</button>
|
||||
<button type="button" id="cancel-btn" class="btn btn-secondary"
|
||||
style="<?= $editId ? '' : 'display:none' ?>"
|
||||
onclick="resetForm()">Abbrechen</button>
|
||||
style="<?= $editId ? '' : 'display:none' ?>">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -70,9 +69,10 @@ $editId = $formData['edit_id'] ?? null;
|
|||
<td><?= $h(View::formatDate(substr($u['created_at'], 0, 10))) ?></td>
|
||||
<td style="text-align:right">
|
||||
<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"
|
||||
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"
|
||||
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"/>
|
||||
|
|
@ -130,4 +130,11 @@ function resetForm() {
|
|||
document.getElementById('submit-btn').textContent = 'Anlegen';
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -41,25 +41,28 @@ class UstBruttoAnzeigeTest extends TestCase
|
|||
'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(
|
||||
str_contains($content, 'oninput="updateNettoAnzeige()"'),
|
||||
'Nettobetrag-Input ruft updateNettoAnzeige() bei Eingabe auf'
|
||||
!str_contains($viewContent, 'oninput=') && !str_contains($viewContent, 'onchange='),
|
||||
'Nettobetrag-Input/USt-Satz-Select haben keine Inline-Handler mehr'
|
||||
);
|
||||
|
||||
$this->assert(
|
||||
str_contains($content, 'function updateNettoAnzeige()'),
|
||||
str_contains($jsContent, 'function updateNettoAnzeige()'),
|
||||
'JavaScript-Funktion updateNettoAnzeige() ist definiert'
|
||||
);
|
||||
|
||||
$this->assert(
|
||||
str_contains($content, 'updateNettoAnzeige();updateNettoAnzeige()') === false
|
||||
&& str_contains($content, 'updateNettoAnzeige()'),
|
||||
'updateNettoAnzeige() wird an mehreren Stellen aufgerufen'
|
||||
str_contains($jsContent, "_betragNettoInput.addEventListener('input', updateNettoAnzeige)"),
|
||||
'Nettobetrag-Input ruft updateNettoAnzeige() bei Eingabe auf'
|
||||
);
|
||||
|
||||
// Prüfe dass USt-Satz-Änderung auch updateNettoAnzeige aufruft
|
||||
$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'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,16 @@ class BetragModusToggleTest extends TestCase
|
|||
'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(
|
||||
str_contains($content, "onclick=\"setBetragModus('netto')\"")
|
||||
&& str_contains($content, "onclick=\"setBetragModus('brutto')\""),
|
||||
!str_contains($viewContent, 'onclick='),
|
||||
'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'
|
||||
);
|
||||
|
||||
|
|
|
|||
68
tests/97_InlineStyleBudgetTest.php
Normal file
68
tests/97_InlineStyleBudgetTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue