346 lines
16 KiB
PHP
346 lines
16 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'] ?? '');
|
||
|
||
$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 & 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>
|