587 lines
31 KiB
PHP
587 lines
31 KiB
PHP
<?php
|
||
use Steuerkram\View;
|
||
$h = fn($v) => View::h($v);
|
||
$fmt = fn($v) => View::formatMoney($v);
|
||
|
||
$customerName = !empty($invoice['firma'])
|
||
? $invoice['firma']
|
||
: trim(($invoice['anrede'] ?? '') . ' ' . ($invoice['vorname'] ?? '') . ' ' . $invoice['nachname']);
|
||
|
||
$lzHtml = $h($invoice['leistungszeitraum'] ?? '');
|
||
|
||
$xmlHinweise = $xmlHinweise ?? [];
|
||
$xmlTooltip = empty($xmlHinweise)
|
||
? 'XML-Rechnung (EN 16931 / XRechnung 3.0) herunterladen'
|
||
: 'XML-Rechnung (EN 16931 / XRechnung 3.0) herunterladen – es fehlen Angaben, siehe Hinweis oben';
|
||
|
||
$badgeClass = match($invoice['status']) {
|
||
'Bezahlt' => 'badge-green',
|
||
'Storniert' => 'badge-red',
|
||
'Gesendet' => 'badge-yellow',
|
||
'Finalisiert' => 'badge-blue',
|
||
'Teilzahlung' => 'badge-orange',
|
||
'Überzahlt' => 'badge-purple',
|
||
default => 'badge-gray',
|
||
};
|
||
?>
|
||
|
||
<!-- 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">
|
||
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">
|
||
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">
|
||
<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">
|
||
<em><?= $h($bereich) ?>:</em>
|
||
<ul style="margin:.2rem 0 0 1.2rem">
|
||
<?php foreach ($felder as $feld): ?>
|
||
<li><?= $h($feld) ?></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php if (isset($xmlHinweise['Stammdaten'])): ?>
|
||
<div style="margin-top:.4rem">Stammdaten pflegen unter <a href="/settings">Einstellungen</a>.</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- 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>
|
||
</div>
|
||
<div class="toolbar-right" style="gap:.5rem">
|
||
<?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"
|
||
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">
|
||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||
</svg>
|
||
</a>
|
||
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/finalize"
|
||
data-confirm="Rechnung finalisieren? Danach kann sie nicht mehr bearbeitet werden.">
|
||
<button type="submit" class="btn btn-primary">Finalisieren & Nummerieren</button>
|
||
</form>
|
||
<?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"
|
||
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">
|
||
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||
<path d="M5 12s2.5-4 7-4 7 4 7 4-2.5 4-7 4-7-4-7-4z"/>
|
||
<circle cx="12" cy="12" r="1.5"/>
|
||
</svg>
|
||
</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"
|
||
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">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||
<polyline points="14 2 14 8 20 8"/>
|
||
<text x="12" y="18.5" text-anchor="middle" font-size="6.5" font-family="sans-serif"
|
||
font-weight="700" fill="currentColor" stroke="none">XML</text>
|
||
</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>
|
||
<?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>
|
||
<?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"
|
||
data-confirm="Rechnung stornieren und Stornorechnung erstellen?">
|
||
<button type="submit" class="btn btn-danger">Stornieren</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rechnungsvorschau (DIN 5008 ähnlich) -->
|
||
<div id="invoice-pages">
|
||
<div class="invoice-preview">
|
||
<div class="invoice-page-body">
|
||
|
||
<!-- Logo -->
|
||
<?php if (!empty($settings['logo_path'])): ?>
|
||
<?php
|
||
$logoMaxW = max(50, (int)($settings['logo_max_width'] ?? 600));
|
||
$logoMaxH = max(20, (int)($settings['logo_max_height'] ?? 100));
|
||
$logoPos = $settings['logo_position'] ?? '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 ?>">
|
||
<img src="/settings/logo/file" alt="Logo"
|
||
style="max-width:<?= $logoMaxW ?>px;max-height:<?= $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>
|
||
<div style="font-size:6pt;color:#666;border-bottom:1px solid #ccc;padding-bottom:2px;margin-bottom:12px">
|
||
<?= $h($settings['firma'] ?? '') ?> · <?= $h($settings['strasse'] ?? '') ?> · <?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?>
|
||
</div>
|
||
<div style="line-height:1.5;font-size:10pt">
|
||
<?= $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">
|
||
<?= !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">
|
||
zu Rechnung Nr. <strong><?= $h($originalrechnung['rechnungs_nr']) ?></strong>
|
||
vom <?= $h(View::formatDate($originalrechnung['rechnungs_datum'] ?? '')) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Meta-Tabelle -->
|
||
<table style="width:100%;table-layout:fixed;border-collapse:collapse;margin-top:2cm;margin-bottom:1.5rem;font-size:.7rem">
|
||
<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>
|
||
</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>
|
||
</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">
|
||
<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>
|
||
</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>
|
||
<?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>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 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>
|
||
</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">
|
||
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>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
</div><!-- /.invoice-page-body -->
|
||
|
||
<!-- 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">
|
||
<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">
|
||
Tel: <?= $h($settings['telefon'] ?? '') ?><br>
|
||
E-Mail: <?= $h($settings['email'] ?? '') ?><br>
|
||
Web: <?= $h($settings['website'] ?? '') ?>
|
||
</div>
|
||
<div style="white-space: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>
|
||
</div>
|
||
|
||
</div><!-- /.invoice-preview -->
|
||
</div><!-- /#invoice-pages -->
|
||
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
|
||
// 1cm in px: Referenzelement messen, damit Browserzoom berücksichtigt wird
|
||
function getCmPx() {
|
||
var d = document.createElement('div');
|
||
d.style.cssText = 'position:absolute;visibility:hidden;width:1cm';
|
||
document.body.appendChild(d);
|
||
var px = d.offsetWidth;
|
||
document.body.removeChild(d);
|
||
return px;
|
||
}
|
||
|
||
function paginate() {
|
||
var wrap = document.getElementById('invoice-pages');
|
||
var page1 = wrap.querySelector('.invoice-preview');
|
||
var body1 = page1.querySelector('.invoice-page-body');
|
||
var footer1 = page1.querySelector('.invoice-footer-bar');
|
||
var posTable = document.getElementById('invoice-pos-table');
|
||
var summary = document.getElementById('invoice-summary');
|
||
|
||
if (!posTable || !summary) return;
|
||
|
||
var cmPx = getCmPx();
|
||
var a4H = 29.7 * cmPx;
|
||
var padTop = parseFloat(getComputedStyle(page1).paddingTop);
|
||
var padBot = parseFloat(getComputedStyle(page1).paddingBottom);
|
||
var footerH = footer1.offsetHeight;
|
||
var availH = a4H - padTop - padBot - footerH;
|
||
|
||
// Passt alles auf Seite 1?
|
||
if (body1.offsetHeight <= availH) {
|
||
return;
|
||
}
|
||
|
||
// Alle Zeilen sichern, tbody leeren
|
||
var tbody = posTable.querySelector('tbody');
|
||
var rows = Array.from(tbody.querySelectorAll('tr'));
|
||
rows.forEach(function (r) { tbody.removeChild(r); });
|
||
|
||
// summary aus body1 herauslösen
|
||
body1.removeChild(summary);
|
||
|
||
// Hilfsfunktionen
|
||
function makeFooter() {
|
||
var f = footer1.cloneNode(true);
|
||
return f;
|
||
}
|
||
|
||
function makePage() {
|
||
var p = document.createElement('div');
|
||
p.className = 'invoice-preview';
|
||
var b = document.createElement('div');
|
||
b.className = 'invoice-page-body';
|
||
p.appendChild(b);
|
||
p.appendChild(makeFooter());
|
||
wrap.appendChild(p);
|
||
return b;
|
||
}
|
||
|
||
function makeContTable(targetBody) {
|
||
var tbl = posTable.cloneNode(false);
|
||
tbl.removeAttribute('id');
|
||
var thead = posTable.querySelector('thead').cloneNode(true);
|
||
var tb = document.createElement('tbody');
|
||
tbl.appendChild(thead);
|
||
tbl.appendChild(tb);
|
||
targetBody.appendChild(tbl);
|
||
return tb;
|
||
}
|
||
|
||
// Zeilen seitenweise verteilen
|
||
var pages = [page1];
|
||
var currentBody = body1;
|
||
var currentTbody = tbody; // zeigt auf tbody in posTable auf Seite 1
|
||
|
||
for (var i = 0; i < rows.length; i++) {
|
||
currentTbody.appendChild(rows[i]);
|
||
|
||
var currentPage = pages[pages.length - 1];
|
||
var usedH = currentBody.offsetHeight;
|
||
|
||
if (usedH > availH) {
|
||
// Zeile wieder entfernen, neue Seite beginnen
|
||
currentTbody.removeChild(rows[i]);
|
||
var newBody = makePage();
|
||
pages.push(newBody.parentNode); // .invoice-preview
|
||
currentBody = newBody;
|
||
currentTbody = makeContTable(newBody);
|
||
currentTbody.appendChild(rows[i]);
|
||
}
|
||
}
|
||
|
||
// summary auf letzte Seite; falls dort kein Platz, nochmal neue Seite
|
||
currentBody.appendChild(summary);
|
||
var lastPage = pages[pages.length - 1];
|
||
if (currentBody.offsetHeight > availH) {
|
||
var extraBody = makePage();
|
||
pages.push(extraBody.parentNode);
|
||
extraBody.appendChild(summary);
|
||
}
|
||
|
||
// Seitenzahlen aktualisieren
|
||
var total = wrap.querySelectorAll('.invoice-preview').length;
|
||
wrap.querySelectorAll('.page-num').forEach(function (el, i) {
|
||
el.textContent = String(i + 1);
|
||
});
|
||
wrap.querySelectorAll('.page-total').forEach(function (el) {
|
||
el.textContent = String(total);
|
||
});
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', paginate);
|
||
} else {
|
||
paginate();
|
||
}
|
||
}());
|
||
</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 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>
|
||
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/send">
|
||
<div class="form-group">
|
||
<label>Empfänger *</label>
|
||
<input type="email" name="email_to" required
|
||
value="<?= $h($invoice['email'] ?? '') ?>"
|
||
placeholder="empfaenger@example.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>CC 1 <span style="font-weight:normal;color:#888">(optional)</span></label>
|
||
<input type="email" name="email_cc1" placeholder="cc1@example.com"
|
||
value="<?= $h($customer['email_cc1'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>CC 2 <span style="font-weight:normal;color:#888">(optional)</span></label>
|
||
<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>
|
||
<button type="submit" class="btn btn-primary">Senden</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Zahlungs-Modal -->
|
||
<?php if (!in_array($invoice['status'], ['Entwurf', 'Storniert']) && !empty($invoice['beleg_id'])): ?>
|
||
<?php
|
||
$kontenGruppen = [];
|
||
foreach ($accounts as $acc) { $kontenGruppen[$acc['kontenart']][] = $acc; }
|
||
?>
|
||
<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>
|
||
|
||
<!-- 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">
|
||
<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>
|
||
</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">
|
||
<?= $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; ?>
|
||
</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>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
<?php
|
||
$gezahlt = \Steuerkram\Models\Booking::sumZahlungen($bookings);
|
||
$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>
|
||
<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></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></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Neue Buchung erfassen -->
|
||
<?php
|
||
// Vorbelegung: noch offener Restbetrag, umgerechnet auf netto mit dem USt-Satz der Rechnung
|
||
$ustQuote = (float)$invoice['betrag_netto'] != 0.0
|
||
? (float)$invoice['betrag_ust'] / (float)$invoice['betrag_netto']
|
||
: 0.0;
|
||
$vorschlagNet = $offen > 0
|
||
? round($offen / (1 + $ustQuote), 2)
|
||
: abs((float)$invoice['betrag_netto']);
|
||
$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>
|
||
<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:.75rem;margin-bottom:.75rem">
|
||
<div>
|
||
<label style="font-size:.75rem;font-weight:600;display:block;margin-bottom:.25rem">Zahldatum *</label>
|
||
<input type="date" name="buchungs_datum" value="<?= date('Y-m-d') ?>" required style="width:100%">
|
||
</div>
|
||
<div>
|
||
<label style="font-size:.75rem;font-weight:600;display:block;margin-bottom:.25rem">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%">
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:.75rem">
|
||
<div>
|
||
<label style="font-size:.75rem;font-weight:600;display:block;margin-bottom:.25rem">Kategorie-Konto *</label>
|
||
<select name="kategorie_konto_id" required style="width:100%">
|
||
<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>
|
||
<label style="font-size:.75rem;font-weight:600;display:block;margin-bottom:.25rem">Zahlungskonto *</label>
|
||
<select name="zahlungskonto_id" required style="width:100%">
|
||
<option value="">– Konto wählen –</option>
|
||
<?php foreach ($kontenGruppen['Zahlungskonto'] ?? [] as $acc): ?>
|
||
<option value="<?= $h($acc['id']) ?>"><?= $h($acc['kontonummer']) ?> – <?= $h($acc['bezeichnung']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:.75rem">
|
||
<div>
|
||
<label style="font-size:.75rem;font-weight:600;display:block;margin-bottom:.25rem">USt-Satz (%)</label>
|
||
<select name="ust_satz" style="width:100%">
|
||
<?php foreach (['0','7','19'] as $s): ?>
|
||
<option value="<?= $s ?>"<?= $s === $ustVorgabe ? ' selected' : '' ?>><?= $s ?> %</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="font-size:.75rem;font-weight:600;display:block;margin-bottom:.25rem">Buchungstext *</label>
|
||
<input type="text" name="buchungstext" required
|
||
value="Ausgangsrechnung <?= $h($invoice['rechnungs_nr'] ?? '') ?>"
|
||
style="width:100%">
|
||
</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>
|
||
<button type="submit" class="btn btn-primary">Buchung erfassen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
var flash = document.querySelector('.flash-error, .flash-success');
|
||
if (flash) {
|
||
var ref = document.referrer || '';
|
||
if (ref.indexOf('/invoices/<?= (int)$invoice['id'] ?>/bookings') !== -1) {
|
||
document.getElementById('buchungs-modal').style.display = 'flex';
|
||
}
|
||
}
|
||
}());
|
||
</script>
|
||
<?php endif; ?>
|