- 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)
965 lines
48 KiB
PHP
965 lines
48 KiB
PHP
<?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(' | ', 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) + ' · 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; ?>
|