Steuerkram/src/Views/invoices/show.php

346 lines
16 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;
$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'] ?? '');
$badgeClass = match($invoice['status']) {
'Bezahlt' => 'badge-green',
'Storniert' => 'badge-red',
'Gesendet' => 'badge-yellow',
'Finalisiert' => 'badge-blue',
default => 'badge-gray',
};
?>
<!-- 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" class="btn btn-secondary">Bearbeiten</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 &amp; Nummerieren</button>
</form>
<?php endif; ?>
<?php if ($invoice['status'] !== 'Entwurf'): ?>
<a href="/invoices/<?= $h($invoice['id']) ?>/pdf" class="btn btn-secondary">PDF herunterladen</a>
<?php endif; ?>
<?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 ($invoice['status'] === 'Gesendet'): ?>
<a href="/invoices/<?= $h($invoice['id']) ?>/book" class="btn btn-success">Zahlungseingang erfassen</a>
<?php endif; ?>
<?php if (in_array($invoice['status'], ['Entwurf','Finalisiert','Gesendet'])): ?>
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/cancel"
data-confirm="Rechnung stornieren?">
<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="/<?= $h($settings['logo_path']) ?>" 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:14pt">Rechnung</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>
<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>
</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>
Inh. <?= $h($settings['inhaber'] ?? '') ?><br>
<?= $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">
</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">
</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>