Steuerkram/src/Views/belege/detail.php
fds2610-bot 262454e038 Feature: Belegbuchung verbessert, Metadaten bereinigt (Migrationen 024–026)
- Verbindlichkeiten in Kategorie-Auswahl bei Belegbuchung ergänzt
- Direkte Kontonummer-Eingabe über Suchfeld (ohne Dropdown-Auswahl)
- Auto-Select im Konto-Suchfeld nur noch bei exaktem Kontonummer-Treffer
- Flash-Messages bleiben 20 Sekunden sichtbar
- Bewirtungsfelder (Grund, Teilnehmerliste) am Beleg statt an Buchung gespeichert (Migration 024)
- Bewirtungssplit für Konto 4652→4657 analog zu 4650→4654; USt 0% für 4652/4657 automatisch
- Bewirtungshinweis als persistentes Flash (verschwindet nicht automatisch)
- Eingangsdatum aus Belegen entfernt (Migration 025)
- Bruttobetrag am Beleg wird aus Buchungszeilen berechnet, keine manuelle Eingabe mehr
- Kontenrahmen von Produktion übernommen (Migration 026)
2026-04-11 11:43:28 +02:00

965 lines
48 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use Steuerkram\View;
use Steuerkram\Models\Setting;
$h = fn($v) => View::h($v);
$fmt = fn($v) => View::formatMoney($v);
$testMode = Setting::isTestMode();
$suppliers = $suppliers ?? [];
?>
<!-- Beleg-Info-Bar -->
<div class="card mb-2"
id="beleg-info-bar"
data-beleg-id="<?= $h($beleg['id']) ?>"
data-belegtyp="<?= $h($beleg['belegtyp']) ?>"
data-beleg-datum="<?= $h($beleg['beleg_datum'] ?? '') ?>"
data-rechnungs-nr="<?= $h($beleg['rechnungs_nr'] ?? '') ?>"
data-betrag-brutto="<?= $h($beleg['betrag_brutto'] ?? '') ?>"
data-supplier-name="<?= $h($beleg['supplier_name'] ?? '') ?>">
<div class="card-body" style="display:flex;gap:2rem;align-items:center;flex-wrap:wrap;padding:.75rem 1.25rem">
<div>
<span class="text-muted text-sm">Belegnr.</span><br>
<strong class="font-mono"><?= $h($beleg['beleg_nr']) ?></strong>
</div>
<div>
<span class="text-muted text-sm">Typ</span><br>
<span class="badge badge-gray"><?= $h($beleg['belegtyp']) ?></span>
</div>
<div>
<span class="text-muted text-sm">Lieferant<?= (!$beleg['locked'] && $beleg['supplier_name']) ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br>
<?php if (!$beleg['locked'] && $beleg['supplier_name']): ?>
<span class="inline-edit-trigger"
id="supplier-inline-trigger"
data-field="supplier_name" data-type="text"
data-raw="<?= $h($beleg['supplier_name']) ?>"
title="Klicken zum Bearbeiten">
<?= $h($beleg['supplier_name']) ?>
</span>
<?php elseif (!$beleg['locked']): ?>
<input type="text" id="supplier-persistent-input"
value="" placeholder="Lieferant eingeben…"
list="supplier-datalist" autocomplete="off"
style="font-size:inherit;padding:2px 5px;border:1px solid var(--color-border);border-radius:var(--radius);background:#fff;min-width:180px">
<?php else: ?>
<?= $beleg['supplier_name'] ? $h($beleg['supplier_name']) : '<span class="text-muted">–</span>' ?>
<?php endif; ?>
</div>
<div>
<span class="text-muted text-sm">Belegdatum<?= !$beleg['locked'] ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br>
<?php if (!$beleg['locked']): ?>
<span class="inline-edit-trigger"
data-field="beleg_datum" data-type="date"
data-raw="<?= $h($beleg['beleg_datum'] ?? '') ?>"
title="Klicken zum Bearbeiten">
<?= $beleg['beleg_datum'] ? View::formatDate($beleg['beleg_datum']) : '<span class="text-muted">– setzen –</span>' ?>
</span>
<?php else: ?>
<?= View::formatDate($beleg['beleg_datum']) ?>
<?php endif; ?>
</div>
<div>
<span class="text-muted text-sm">Rechnungsnr.<?= !$beleg['locked'] ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br>
<?php if (!$beleg['locked']): ?>
<span class="inline-edit-trigger"
data-field="rechnungs_nr" data-type="text"
data-raw="<?= $h($beleg['rechnungs_nr'] ?? '') ?>"
title="Klicken zum Bearbeiten">
<?= $beleg['rechnungs_nr'] ? $h($beleg['rechnungs_nr']) : '<span class="text-muted">– setzen –</span>' ?>
</span>
<?php else: ?>
<?= $beleg['rechnungs_nr'] ? $h($beleg['rechnungs_nr']) : '<span class="text-muted">–</span>' ?>
<?php endif; ?>
</div>
<div>
<span class="text-muted text-sm">Brutto (Buchungen)</span><br>
<?= $beleg['betrag_brutto'] ? '<strong>' . $fmt($beleg['betrag_brutto']) . '</strong>' : '<span class="text-muted">–</span>' ?>
</div>
<div>
<span class="text-muted text-sm">GoBD-Integrität</span><br>
<?php if ($integrity): ?>
<span class="badge badge-green">✓ OK</span>
<?php else: ?>
<span class="badge badge-red">⚠ Prüfsumme abweichend</span>
<?php endif; ?>
</div>
<?php if (!empty($beleg['bewirtungsgrund']) || !empty($beleg['teilnehmerliste'])): ?>
<div>
<span class="text-muted text-sm">Bewirtung</span><br>
<?php if (!empty($beleg['bewirtungsgrund'])): ?>
<span><?= $h($beleg['bewirtungsgrund']) ?></span>
<?php endif; ?>
<?php if (!empty($beleg['teilnehmerliste'])): ?>
<span class="text-muted" style="font-size:.85em"> · <?= $h($beleg['teilnehmerliste']) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<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>
<?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'">
Metadaten bearbeiten
</button>
<?php endif; ?>
<?php if (!$beleg['locked'] && empty($bookings)): ?>
<form method="post" action="/belege/<?= $h($beleg['id']) ?>/delete"
data-confirm="Beleg wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.">
<button type="submit" class="btn btn-danger btn-sm">Löschen</button>
</form>
<?php endif; ?>
</div>
</div>
<?php if (!$beleg['locked']): ?>
<div id="beleg-meta-form" style="display:none;border-top:1px solid var(--color-border);padding:.75rem 1.25rem">
<form method="post" action="/belege/<?= $h($beleg['id']) ?>/meta">
<div style="display:flex;gap:1rem;flex-wrap:wrap;align-items:flex-end">
<div class="form-group" style="margin:0;min-width:140px">
<label style="font-size:.8rem">Rechnungsnr.</label>
<input type="text" name="rechnungs_nr" value="<?= $h($beleg['rechnungs_nr'] ?? '') ?>"
placeholder="z. B. RE-2024-001">
</div>
<div class="form-group" style="margin:0">
<label style="font-size:.8rem">Belegtyp</label>
<select name="belegtyp">
<?php foreach (['Eingangsrechnung','Ausgangsrechnung','Quittung','Kontoauszug','Sonstiges'] as $t): ?>
<option value="<?= $t ?>"<?= $beleg['belegtyp'] === $t ? ' selected' : '' ?>><?= $t ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0">
<label style="font-size:.8rem">Belegdatum</label>
<input type="date" name="beleg_datum" value="<?= $h($beleg['beleg_datum'] ?? '') ?>">
</div>
<div class="form-group" style="margin:0;min-width:180px">
<label style="font-size:.8rem">Lieferant</label>
<input type="text" name="supplier_name" id="supplier_name_input"
value="<?= $h($beleg['supplier_name'] ?? '') ?>"
placeholder="Name eingeben oder wählen…"
list="supplier-datalist"
autocomplete="off">
<datalist id="supplier-datalist">
<?php foreach ($suppliers as $sup): ?>
<option value="<?= $h($sup['name']) ?>">
<?php endforeach; ?>
</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>
</div>
</form>
</div>
<?php endif; ?>
</div>
<!-- Split-Pane: Vorschau + Buchungsformular -->
<div class="beleg-layout">
<!-- Linke Seite: Dokumentvorschau -->
<div class="beleg-preview">
<?php if (in_array($beleg['mime_type'], ['application/pdf'])): ?>
<iframe src="/belege/<?= $h($beleg['id']) ?>/file" title="Belegvorschau"></iframe>
<?php elseif (str_starts_with($beleg['mime_type'], 'image/')): ?>
<img src="/belege/<?= $h($beleg['id']) ?>/file" alt="Belegvorschau" style="object-fit:contain;max-height:700px;width:100%">
<?php elseif (str_contains($beleg['mime_type'], 'xml')): ?>
<div style="padding:1.5rem">
<div class="badge badge-blue mb-2">XML / eRechnung</div>
<p class="text-muted text-sm">XML-Datei kann nicht direkt angezeigt werden.</p>
<a href="/belege/<?= $h($beleg['id']) ?>/file" class="btn btn-secondary mt-2" download>XML herunterladen</a>
</div>
<?php endif; ?>
</div>
<!-- Rechte Seite: Buchungen -->
<div>
<!-- Vorhandene Buchungen -->
<div class="card mb-2">
<div class="card-header">
<div class="card-title">Buchungszeilen (<?= count($bookings) ?>)</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Buchungsnr.</th>
<th>Datum</th>
<th>Typ</th>
<th>Kategorie</th>
<th>Zahlungskonto</th>
<th class="td-right">Netto</th>
<th class="td-right">USt</th>
<th class="td-right">Brutto</th>
<th>Text</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($bookings as $bk):
$isReversed = !empty($bk['reversed_by']);
$isReversal = (bool)$bk['is_reversal'];
?>
<tr style="<?= ($isReversed || $isReversal) ? 'opacity:.55;background:#fff5f5' : '' ?>">
<td class="td-mono text-sm"><?= $h($bk['buchungs_nr']) ?></td>
<td class="td-muted"><?= View::formatDate($bk['buchungs_datum']) ?></td>
<td><span class="badge <?= $bk['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>"><?= $h($bk['buchungstyp']) ?></span></td>
<td class="td-mono text-sm"><?= $h($bk['kategorie_nr']) ?><br><span class="td-muted"><?= $h($bk['kategorie_name']) ?></span></td>
<td class="td-mono text-sm"><?= $h($bk['zahlungs_nr']) ?><br><span class="td-muted"><?= $h($bk['zahlungs_name']) ?></span></td>
<td class="td-right td-mono"><?= $fmt($bk['betrag_netto']) ?></td>
<td class="td-right td-mono"><?= number_format((float)$bk['ust_satz'], 0) ?> %</td>
<td class="td-right td-mono"><?= $fmt($bk['betrag_brutto']) ?></td>
<td class="text-sm">
<?= $h($bk['buchungstext']) ?>
<?php if ($isReversal): ?>
<span class="badge badge-red">Storno</span>
<?php elseif ($isReversed): ?>
<span class="badge badge-red">Storniert</span>
<?php endif; ?>
</td>
<td>
<?php if (!$bk['locked'] && !$isReversal && !$isReversed): ?>
<div style="display:flex;gap:.35rem">
<button type="button" class="btn btn-danger btn-sm"
data-modal-open="modal-storno-<?= $h($bk['id']) ?>">
Storno
</button>
<?php if ($testMode): ?>
<form method="post" action="/bookings/<?= $h($bk['id']) ?>/delete"
data-confirm="Buchung <?= $h($bk['buchungs_nr']) ?> wirklich löschen? (Nur im Testmodus – nicht GoBD-konform!)">
<input type="hidden" name="beleg_id" value="<?= $h($beleg['id']) ?>">
<button type="submit" class="btn btn-danger btn-sm" title="Löschen (Testmodus)">
Löschen
</button>
</form>
<?php endif; ?>
</div>
<!-- Storno-Modal -->
<div class="modal-overlay" id="modal-storno-<?= $h($bk['id']) ?>">
<div class="modal">
<div class="modal-title">Buchung stornieren</div>
<p class="text-sm text-muted mb-2">Die Buchung <?= $h($bk['buchungs_nr']) ?> wird durch eine Gegenbuchung storniert (GoBD-konform).</p>
<form method="post" action="/bookings/<?= $h($bk['id']) ?>/reverse">
<div class="form-group">
<label>Stornogrund</label>
<input type="text" name="reason" required placeholder="Grund für die Stornierung">
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button type="submit" class="btn btn-danger">Stornieren</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($bookings)): ?>
<tr><td colspan="10" class="text-center text-muted" style="padding:1rem">Noch keine Buchungen.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- KI-Kontierungsvorschlag -->
<?php if (!$beleg['locked'] && ($kiAktiv ?? false)): ?>
<div id="ki-badge" style="margin-bottom:1rem"></div>
<?php endif; ?>
<!-- Neue Buchungszeile erfassen -->
<?php if (!$beleg['locked']): ?>
<?php
$formErrors = $formErrors ?? [];
$formData = $formData ?? [];
// Default buchungstyp aus Belegtyp ableiten
$defaultTyp = $formData['buchungstyp'] ?? (
$beleg['belegtyp'] === 'Ausgangsrechnung' ? 'Einnahme' : 'Ausgabe'
);
// Konten nach Art gruppieren (Zahlungskonto nur 1200–1299)
$kontenGruppen = [];
foreach ($accounts as $acc) {
if ($acc['kontenart'] === 'Zahlungskonto') {
$nr = (int)$acc['kontonummer'];
if ($nr < 1200 || $nr > 1299) continue;
}
$kontenGruppen[$acc['kontenart']][] = $acc;
}
?>
<div class="card">
<div class="card-header">
<div class="card-title">Neue Buchungszeile</div>
</div>
<div class="card-body">
<?php if (!empty($formErrors)): ?>
<div class="flash flash-error" style="margin-bottom:1rem">
<?php if (isset($formErrors['_exception'])): ?>
Fehler: <?= $h($formErrors['_exception']) ?>
<?php else: ?>
<?= implode(' &nbsp;|&nbsp; ', array_map(fn($k,$v) => "<strong>$k:</strong> $v", array_keys($formErrors), $formErrors)) ?>
<?php endif; ?>
</div>
<?php endif; ?>
<form method="post" action="/belege/<?= $h($beleg['id']) ?>/bookings">
<!-- Buchungstyp -->
<div class="form-group">
<label>Buchungstyp *</label>
<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)">
<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)">
<span class="badge badge-green">Betriebseinnahme</span>
</label>
</div>
</div>
<div class="form-grid form-grid-2">
<div class="form-group">
<label>Zahldatum *</label>
<input type="date" name="buchungs_datum"
value="<?= $h($formData['buchungs_datum'] ?? date('Y-m-d')) ?>" required
class="<?= isset($formErrors['buchungs_datum']) ? 'input-error' : '' ?>">
</div>
<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">
<?php foreach (['0','7','19'] as $s): ?>
<option value="<?= $s ?>"<?= ($formData['ust_satz'] ?? '19') === $s ? ' selected' : '' ?>><?= $s ?> %</option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- Steuerart (für Auslandsumsätze) -->
<div class="form-group" id="steuerart_group">
<label>Steuerart (Ausland / EU)</label>
<select name="steuerart" id="steuerart_select">
<?php
$steuerartOptionen = [
'normal' => 'Normal (Inland)',
'ig_lieferung' => 'Steuerfreie ig. Lieferung § 4 Nr. 1b (Kz 41)',
'export' => 'Steuerfreie Ausfuhr § 4 Nr. 1a (Kz 44)',
'ig_erwerb_19' => 'Innergemeinschaftlicher Erwerb 19 % (Kz 89/91)',
'ig_erwerb_7' => 'Innergemeinschaftlicher Erwerb 7 % (Kz 93/94)',
];
$currentSteuerart = $formData['steuerart'] ?? 'normal';
foreach ($steuerartOptionen as $val => $label): ?>
<option value="<?= $val ?>"<?= $currentSteuerart === $val ? ' selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
<p class="text-muted text-sm" style="margin-top:.25rem">
Nur relevant bei USt-Satz 0 % für Auslandsumsätze und EU-Erwerbe.
</p>
</div>
<!-- Kategorie-Konto -->
<div class="form-group">
<label for="kategorie_konto_id">Kategorie *
<span id="kategorie_konto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
</label>
<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()">
<option value="">– Konto wählen –</option>
<?php foreach (['Betriebsausgabe', 'Betriebseinnahme', 'Privat', 'Verbindlichkeit'] as $art): ?>
<?php if (empty($kontenGruppen[$art])) continue; ?>
<optgroup label="── <?= $h($art) ?> ──" data-kontenart="<?= $h($art) ?>">
<?php foreach ($kontenGruppen[$art] as $acc): ?>
<option value="<?= $h($acc['id']) ?>"
data-art="<?= $h($acc['kontenart']) ?>"
data-kontonummer="<?= $h($acc['kontonummer']) ?>"
data-steuercode="<?= $h($acc['steuercode'] ?? '') ?>"
data-search="<?= $h($acc['kontonummer'] . ' ' . $acc['bezeichnung'] . ' ' . $acc['kontenart']) ?>">
<?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
</div>
<!-- Zahlungskonto -->
<div class="form-group">
<label for="zahlungskonto_id">Zahlungskonto *
<span id="zahlungskonto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
</label>
<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)">
<option value="">– Konto wählen –</option>
<?php foreach (['Zahlungskonto', 'Verbindlichkeit'] as $art): ?>
<?php if (empty($kontenGruppen[$art])) continue; ?>
<optgroup label="── <?= $h($art) ?> ──">
<?php foreach ($kontenGruppen[$art] as $acc): ?>
<option value="<?= $h($acc['id']) ?>"
data-art="<?= $h($acc['kontenart']) ?>"
data-search="<?= $h($acc['kontonummer'] . ' ' . $acc['bezeichnung'] . ' ' . $acc['kontenart']) ?>">
<?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
</div>
<div class="form-grid form-grid-2">
<div class="form-group">
<label>Nettobetrag (€) *</label>
<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()">
<p id="ust_brutto_anzeige" class="text-muted text-sm" style="margin-top:.25rem;line-height:1.6"></p>
</div>
<div class="form-group">
<label>Buchungstext *</label>
<?php
$defaultText = $formData['buchungstext']
?? ($beleg['rechnungs_nr']
? 'Rechnung ' . $beleg['rechnungs_nr']
: $beleg['belegtyp'] . ' ' . $beleg['beleg_nr']);
?>
<input type="text" name="buchungstext" required
value="<?= $h($defaultText) ?>"
class="<?= isset($formErrors['buchungstext']) ? 'input-error' : '' ?>">
</div>
</div>
<!-- Bewirtungsfelder (nur bei Konto 4650/4651) -->
<div id="bewirtungsfelder" style="display:none">
<div class="flash flash-info flash-persistent" style="margin-bottom:.75rem;padding:.5rem .75rem;font-size:.85rem">
<strong>Bewirtungsausgabe:</strong> 70 % werden auf das gewählte Konto gebucht, 30 % automatisch auf das Abzugskonto. Die Vorsteuer wird vollständig berücksichtigt.
</div>
<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'"
placeholder="Anlass/Grund der Bewirtung"><?= $h($formData['bewirtungsgrund'] ?? $beleg['bewirtungsgrund'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label for="teilnehmerliste_input">Teilnehmerliste *</label>
<textarea name="teilnehmerliste" id="teilnehmerliste_input"
style="height:5.4em;resize:vertical"
placeholder="Namen der Teilnehmer"><?= $h($formData['teilnehmerliste'] ?? $beleg['teilnehmerliste'] ?? '') ?></textarea>
</div>
</div>
<div class="form-group" title="Nur bei Telefon/Internet 19% wählbar" style="margin-bottom:.75rem">
<label id="eigenanteil_group"
style="display:inline-flex;align-items:center;gap:.5rem;opacity:.35;pointer-events:none;cursor:not-allowed">
<input type="checkbox" id="eigenanteil_30_cb" name="eigenanteil_30" value="1" disabled>
<span>30% Eigenanteil buchen</span>
</label>
</div>
<button type="submit" class="btn btn-primary">Buchung erfassen</button>
</form>
</div>
</div>
<?php else: ?>
<div class="flash flash-info">Dieser Beleg ist gesperrt (GoBD). Keine weiteren Buchungen möglich.</div>
<?php endif; ?>
</div>
</div>
<script>
function filterKategorieKonten(buchungstyp) {
const select = document.getElementById('kategorie_konto_id');
const groups = select.querySelectorAll('optgroup');
const zielArt = buchungstyp === 'Einnahme' ? 'Betriebseinnahme' : 'Betriebsausgabe';
groups.forEach(function(g) {
const art = g.getAttribute('data-kontenart');
if (!art) return;
// Privat und Verbindlichkeit immer anzeigen, Kategorie nach Typ filtern
g.style.display = (art === zielArt || art === 'Privat' || art === 'Verbindlichkeit') ? '' : 'none';
});
// Auswahl zurücksetzen wenn aktuell gewähltes Konto nicht mehr sichtbar
if (select.selectedIndex > 0) {
const selected = select.options[select.selectedIndex];
if (selected && selected.closest('optgroup') && selected.closest('optgroup').style.display === 'none') {
select.selectedIndex = 0;
}
}
updateEigenanteilState();
}
function updateAutoUst() {
const kategorieEl = document.getElementById('kategorie_konto_id');
const ustSatzEl = document.getElementById('ust_satz_select');
if (!kategorieEl || !ustSatzEl) return;
const selectedOpt = kategorieEl.options[kategorieEl.selectedIndex];
const steuercode = selectedOpt ? (selectedOpt.getAttribute('data-steuercode') || '') : '';
const kontonummer = selectedOpt ? (selectedOpt.getAttribute('data-kontonummer') || '') : '';
const ustMap = { 'VSt19': '19', 'USt19': '19', 'VSt7': '7', 'USt7': '7' };
// 4652/4657 immer 0 % (keine Vorsteuer bei pauschalen Bewirtungskosten ohne USt-Ausweis)
const autoUst = (['4652','4657'].includes(kontonummer)) ? '0' : (ustMap[steuercode] || null);
const hiddenEl = document.getElementById('ust_satz_hidden');
if (autoUst !== null) {
ustSatzEl.value = autoUst;
ustSatzEl.disabled = true;
ustSatzEl.title = 'Automatisch durch Steuercode des Kontos gesetzt';
if (hiddenEl) hiddenEl.value = autoUst;
} else {
ustSatzEl.disabled = false;
ustSatzEl.title = '';
if (hiddenEl) hiddenEl.value = ustSatzEl.value;
}
updateSteuerartVisibility();
updateNettoAnzeige();
}
function updateBewirtungsState() {
const kategorieEl = document.getElementById('kategorie_konto_id');
const felder = document.getElementById('bewirtungsfelder');
if (!kategorieEl || !felder) return;
const selectedOpt = kategorieEl.options[kategorieEl.selectedIndex];
const kontonummer = selectedOpt ? (selectedOpt.getAttribute('data-kontonummer') || '') : '';
const istBewirtung = ['4650','4651','4652'].includes(kontonummer);
felder.style.display = istBewirtung ? '' : 'none';
const grundEl = document.getElementById('bewirtungsgrund_input');
const teilnEl = document.getElementById('teilnehmerliste_input');
if (grundEl) grundEl.required = istBewirtung;
if (teilnEl) teilnEl.required = istBewirtung;
}
function updateEigenanteilState() {
const group = document.getElementById('eigenanteil_group');
const cb = document.getElementById('eigenanteil_30_cb');
if (!group || !cb) return;
const buchungstyp = document.querySelector('input[name="buchungstyp"]:checked');
const kategorieEl = document.getElementById('kategorie_konto_id');
const ustSatzEl = document.getElementById('ust_satz_select');
const istAusgabe = buchungstyp && buchungstyp.value === 'Ausgabe';
const selectedOpt = kategorieEl && kategorieEl.options[kategorieEl.selectedIndex];
const kontonummer = selectedOpt ? (selectedOpt.getAttribute('data-search') || '').split(' ')[0] : '';
const istTelInternet = kontonummer === '4920' || kontonummer === '4925';
const ist19 = ustSatzEl && ustSatzEl.value === '19';
const aktiv = istAusgabe && istTelInternet && ist19;
group.style.opacity = aktiv ? '1' : '.35';
group.style.pointerEvents = aktiv ? '' : 'none';
group.style.cursor = aktiv ? 'pointer' : 'not-allowed';
cb.disabled = !aktiv;
if (!aktiv) cb.checked = false;
}
// Initial filtern
document.addEventListener('DOMContentLoaded', function() {
const checked = document.querySelector('input[name="buchungstyp"]:checked');
if (checked) filterKategorieKonten(checked.value);
updateSteuerartVisibility();
updateEigenanteilState();
updateBewirtungsState();
updateAutoUst();
updateNettoAnzeige();
// Leeres Lieferantenfeld fokussieren wenn kein KI-Block aktiv
const sup = document.getElementById('supplier-persistent-input');
if (sup && !document.getElementById('ki-badge')) sup.focus();
});
function updateSteuerartVisibility() {
const ustSatz = document.getElementById('ust_satz_select');
const steuerartGroup = document.getElementById('steuerart_group');
const steuerartSelect = document.getElementById('steuerart_select');
if (!ustSatz || !steuerartGroup) return;
if (ustSatz.value === '0') {
steuerartGroup.style.display = '';
} else {
steuerartGroup.style.display = 'none';
if (steuerartSelect) steuerartSelect.value = 'normal';
}
updateEigenanteilState();
}
function updateNettoAnzeige() {
const nettoEl = document.getElementById('betrag_netto_input');
const ustEl = document.getElementById('ust_satz_select');
const anzeige = document.getElementById('ust_brutto_anzeige');
if (!nettoEl || !ustEl || !anzeige) return;
const netto = parseFloat(nettoEl.value);
if (isNaN(netto) || nettoEl.value === '') {
anzeige.textContent = '';
return;
}
const ustSatz = parseFloat(ustEl.value) || 0;
const ust = Math.round(netto * ustSatz / 100 * 100) / 100;
const brutto = Math.round((netto + ust) * 100) / 100;
const fmt = v => v.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €';
anzeige.innerHTML = 'USt. ' + fmt(ust) + ' &nbsp;·&nbsp; Brutto ' + fmt(brutto);
}
</script>
<?php if (!$beleg['locked'] && ($kiAktiv ?? false)): ?>
<script>
const _belegId = <?= (int)$beleg['id'] ?>;
const _kiVorschlag = <?= json_encode($kiVorschlag ?? null) ?>;
function _selectById(selectEl, id) {
if (!selectEl) return;
const str = String(id);
for (const opt of selectEl.options) {
opt.selected = (opt.value === str);
}
}
function _applyKiVorschlag(v) {
const btRadio = document.querySelector('input[name="buchungstyp"][value="' + v.buchungstyp + '"]');
if (btRadio) { btRadio.checked = true; filterKategorieKonten(v.buchungstyp); }
const ustEl = document.getElementById('ust_satz_select');
if (ustEl) { ustEl.value = String(v.ust_satz); updateSteuerartVisibility(); }
const katEl2 = document.getElementById('kategorie_konto_id');
_selectById(katEl2, v.kategorie_konto_id);
if (katEl2) transferKontoNr(katEl2);
updateEigenanteilState();
const zhlEl2 = document.getElementById('zahlungskonto_id');
_selectById(zhlEl2, v.zahlungskonto_id);
if (zhlEl2) transferKontoNr(zhlEl2);
const netEl = document.querySelector('input[name="betrag_netto"]');
if (netEl && v.betrag_netto) { netEl.value = v.betrag_netto; updateNettoAnzeige(); }
const txtEl = document.querySelector('input[name="buchungstext"]');
if (txtEl && v.buchungstext) txtEl.value = v.buchungstext;
// Lieferant aus KI-Vorschlag übernehmen
if (v.supplier_name) {
const bar = document.querySelector('[data-beleg-id]');
const persistentInput = document.getElementById('supplier-persistent-input');
const trigger = document.getElementById('supplier-inline-trigger');
if (persistentInput) {
persistentInput.value = v.supplier_name;
if (bar) bar.dataset.supplierName = v.supplier_name;
if (v.supplier_found === false) persistentInput.focus();
} else if (trigger) {
trigger.dataset.raw = v.supplier_name;
if (bar) bar.dataset.supplierName = v.supplier_name;
trigger.textContent = v.supplier_name;
if (v.supplier_found === false && !trigger.querySelector('input')) {
trigger.click();
}
}
}
}
function _showKiBadgeDone(v) {
const katEl = document.getElementById('kategorie_konto_id');
const zhlEl = document.getElementById('zahlungskonto_id');
const katOpt = katEl ? katEl.querySelector('option[value="' + v.kategorie_konto_id + '"]') : null;
const zhlOpt = zhlEl ? zhlEl.querySelector('option[value="' + v.zahlungskonto_id + '"]') : null;
const katTxt = katOpt ? katOpt.text : v.kategorie_konto_id;
const zhlTxt = zhlOpt ? zhlOpt.text : v.zahlungskonto_id;
const supTxt = v.supplier_name
? ' · Lieferant: ' + v.supplier_name + (v.supplier_found === false ? ' <span style="color:var(--color-warning);font-weight:600">(neu – bitte prüfen)</span>' : '')
: '';
const details =
(v.buchungstyp ? v.buchungstyp + ' · ' : '') +
'USt ' + v.ust_satz + ' % · ' +
'Kategorie: ' + katTxt + ' · ' +
'Zahlung: ' + zhlTxt + supTxt;
document.getElementById('ki-badge').innerHTML =
'<div class="flash flash-info" style="display:flex;justify-content:space-between;align-items:center;gap:1rem">' +
'<div><strong>KI-Vorschlag:</strong> <span style="font-size:.9em">' + details + '</span></div>' +
'<button class="btn btn-secondary btn-sm" style="flex-shrink:0" onclick="_startKiAnalyse()">Neu generieren</button>' +
'</div>';
}
function _showKiBadgeLoading() {
document.getElementById('ki-badge').innerHTML =
'<div class="flash flash-info" style="opacity:.65">Analysiere Beleg … bitte warten.</div>';
}
function _showKiBadgeError() {
document.getElementById('ki-badge').innerHTML =
'<div class="flash flash-warning">Kein Kontierungsvorschlag generiert. ' +
'<button class="btn btn-secondary btn-sm" onclick="_startKiAnalyse()">Erneut versuchen</button></div>';
}
async function _startKiAnalyse() {
await fetch('/belege/' + _belegId + '/vorschlag/reset', {method: 'POST'});
_showKiBadgeLoading();
try {
const r = await fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'});
const v = await r.json();
if (v && v.kategorie_konto_id) { _applyKiVorschlag(v); _showKiBadgeDone(v); }
else _showKiBadgeError();
} catch (e) {
_showKiBadgeError();
}
}
document.addEventListener('DOMContentLoaded', function () {
if (_kiVorschlag && _kiVorschlag.kategorie_konto_id) {
_applyKiVorschlag(_kiVorschlag);
_showKiBadgeDone(_kiVorschlag);
} else {
_showKiBadgeLoading();
fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'})
.then(r => r.json())
.then(v => {
if (v && v.kategorie_konto_id) { _applyKiVorschlag(v); _showKiBadgeDone(v); }
else _showKiBadgeError();
})
.catch(() => _showKiBadgeError());
}
});
</script>
<?php endif; ?>
<?php if (!$beleg['locked'] && in_array($beleg['mime_type'], ['image/jpeg', 'image/png'])): ?>
<!-- Crop-Modal -->
<div class="modal-overlay" id="crop-modal-overlay" style="display:none;align-items:center;justify-content:center">
<div class="modal" style="max-width:95vw;width:auto;padding:1.25rem">
<div class="modal-title" style="margin-bottom:.75rem">Beleg zuschneiden</div>
<p class="text-sm text-muted" style="margin-bottom:.75rem">
Ziehe einen Rahmen über den gewünschten Ausschnitt. Das Original wird unwiderruflich ersetzt.
</p>
<div id="crop-canvas-wrap" style="position:relative;display:inline-block;cursor:crosshair;line-height:0;border:1px solid var(--color-border);border-radius:var(--radius)">
<canvas id="crop-canvas"></canvas>
</div>
<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>
<span id="crop-size-hint" class="text-muted text-sm"></span>
</div>
</div>
</div>
<script>
(function() {
const BELEG_ID = <?= (int)$beleg['id'] ?>;
const FILE_URL = '/belege/<?= (int)$beleg['id'] ?>/file';
const overlay = document.getElementById('crop-modal-overlay');
const canvas = document.getElementById('crop-canvas');
const confirmBtn = document.getElementById('crop-confirm-btn');
const sizeHint = document.getElementById('crop-size-hint');
const ctx = canvas.getContext('2d');
let img = null;
let scaleX = 1, scaleY = 1;
let sel = null; // {x,y,w,h} in Canvas-Koordinaten
let dragging = false;
let startX = 0, startY = 0;
window.openCropModal = function() {
overlay.style.display = 'flex';
if (img) { drawCanvas(); return; }
img = new Image();
img.onload = function() {
// Canvas auf max. 90vw / 72vh skalieren
const maxW = Math.min(window.innerWidth * 0.90, 1200);
const maxH = Math.min(window.innerHeight * 0.72, 900);
const ratio = Math.min(maxW / img.naturalWidth, maxH / img.naturalHeight, 1);
canvas.width = Math.round(img.naturalWidth * ratio);
canvas.height = Math.round(img.naturalHeight * ratio);
scaleX = img.naturalWidth / canvas.width;
scaleY = img.naturalHeight / canvas.height;
sel = null;
drawCanvas();
};
img.src = FILE_URL + '?nocache=' + Date.now();
};
window.closeCropModal = function() {
overlay.style.display = 'none';
};
function drawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
if (!sel || sel.w === 0 || sel.h === 0) return;
// Abdunkeln außerhalb der Auswahl
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Ausschnitt freistellen
ctx.drawImage(img,
sel.x * scaleX, sel.y * scaleY, sel.w * scaleX, sel.h * scaleY,
sel.x, sel.y, sel.w, sel.h);
// Rahmen
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 3]);
ctx.strokeRect(sel.x + 0.5, sel.y + 0.5, sel.w, sel.h);
ctx.setLineDash([]);
const realW = Math.round(sel.w * scaleX);
const realH = Math.round(sel.h * scaleY);
sizeHint.textContent = realW + ' × ' + realH + ' px';
confirmBtn.disabled = (realW < 10 || realH < 10);
}
function canvasPos(e) {
const r = canvas.getBoundingClientRect();
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
return {
x: Math.max(0, Math.min(canvas.width, Math.round(cx - r.left))),
y: Math.max(0, Math.min(canvas.height, Math.round(cy - r.top))),
};
}
canvas.addEventListener('mousedown', startDrag);
canvas.addEventListener('touchstart', startDrag, {passive: true});
function startDrag(e) {
const p = canvasPos(e);
startX = p.x; startY = p.y;
sel = {x: p.x, y: p.y, w: 0, h: 0};
dragging = true;
confirmBtn.disabled = true;
sizeHint.textContent = '';
}
window.addEventListener('mousemove', moveDrag);
window.addEventListener('touchmove', moveDrag, {passive: true});
function moveDrag(e) {
if (!dragging) return;
const p = canvasPos(e);
sel = {
x: Math.min(startX, p.x),
y: Math.min(startY, p.y),
w: Math.abs(p.x - startX),
h: Math.abs(p.y - startY),
};
drawCanvas();
}
window.addEventListener('mouseup', endDrag);
window.addEventListener('touchend', endDrag);
function endDrag() { dragging = false; }
window.confirmCrop = async function() {
if (!sel || sel.w < 1 || sel.h < 1) return;
confirmBtn.disabled = true;
confirmBtn.textContent = 'Wird gespeichert…';
const payload = {
x: Math.round(sel.x * scaleX),
y: Math.round(sel.y * scaleY),
w: Math.round(sel.w * scaleX),
h: Math.round(sel.h * scaleY),
};
try {
const res = await fetch('/belege/' + BELEG_ID + '/crop', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Unbekannter Fehler');
location.reload();
} catch (err) {
alert('Fehler beim Zuschneiden: ' + err.message);
confirmBtn.disabled = false;
confirmBtn.textContent = 'Zuschnitt übernehmen';
}
};
// ESC schließt Modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && overlay.style.display !== 'none') closeCropModal();
});
// Klick auf Overlay-Hintergrund schließt
overlay.addEventListener('click', function(e) {
if (e.target === overlay) closeCropModal();
});
}());
</script>
<?php endif; ?>
<?php if (!$beleg['locked']): ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const _sup = document.getElementById('supplier-persistent-input');
if (!_sup) return;
const _bar = _sup.closest('[data-beleg-id]');
async function _saveSupplier() {
const newValue = _sup.value.trim();
if (!_bar) return;
const body = new URLSearchParams({
rechnungs_nr: _bar.dataset.rechnungsNr || '',
belegtyp: _bar.dataset.belegtyp || '',
beleg_datum: _bar.dataset.belegDatum || '',
betrag_brutto: _bar.dataset.betragBrutto || '',
supplier_name: newValue,
});
try {
const res = await fetch('/belege/<?= (int)$beleg['id'] ?>/meta', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded'},
body: body.toString(),
});
if (!res.ok) throw new Error();
if (newValue) {
_bar.dataset.supplierName = newValue;
// Hidden-Metaformular synchron halten
const hiddenInput = document.getElementById('supplier_name_input');
if (hiddenInput) hiddenInput.value = newValue;
// Persistentes Input durch Inline-Trigger ersetzen (= visuelle Bestätigung)
const span = document.createElement('span');
span.className = 'inline-edit-trigger';
span.id = 'supplier-inline-trigger';
span.dataset.field = 'supplier_name';
span.dataset.type = 'text';
span.dataset.raw = newValue;
span.title = 'Klicken zum Bearbeiten';
span.textContent = newValue;
_sup.replaceWith(span);
}
} catch (_) {
// Speichern fehlgeschlagen – Feld bleibt editierbar
}
}
_sup.addEventListener('blur', _saveSupplier);
_sup.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); _sup.blur(); }
});
});
</script>
<?php endif; ?>