Logo-Upload und Rechnungserstellung überarbeitet

This commit is contained in:
fds2610-bot 2026-04-13 15:32:57 +02:00
parent 56b1cebfac
commit d8cba3e1f5
7 changed files with 342 additions and 156 deletions

View file

@ -0,0 +1,6 @@
ALTER TABLE company_settings
ADD COLUMN IF NOT EXISTS logo_max_width INT NOT NULL DEFAULT 600,
ADD COLUMN IF NOT EXISTS logo_max_height INT NOT NULL DEFAULT 100,
ADD COLUMN IF NOT EXISTS logo_position VARCHAR(10) NOT NULL DEFAULT 'center',
ADD COLUMN IF NOT EXISTS logo_margin_side INT NOT NULL DEFAULT 75,
ADD COLUMN IF NOT EXISTS logo_margin_top INT NOT NULL DEFAULT 6;

View file

@ -109,8 +109,9 @@ img { max-width: 100%; }
.main-content {
display: flex;
flex-direction: column;
min-height: 100vh;
overflow: hidden;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
}
.topbar {
@ -121,6 +122,9 @@ img { max-width: 100%; }
align-items: center;
justify-content: space-between;
gap: 1rem;
position: sticky;
top: 0;
z-index: 10;
}
.topbar-title {
@ -368,6 +372,18 @@ td { padding: .65rem 1rem; vertical-align: middle; }
}
/* ── Invoice preview (DIN 5008) ──────────────────────────────── */
.invoice-preview thead th {
text-transform: none;
letter-spacing: normal;
font-size: inherit;
font-weight: normal;
background: revert;
padding: revert;
white-space: revert;
color: inherit;
border-bottom: none;
}
.invoice-preview {
background: #fff;
padding: 2cm 2cm 2.5cm;
@ -379,6 +395,29 @@ td { padding: .65rem 1rem; vertical-align: middle; }
color: #000;
}
/*
* Adressbereich: DIN 5008-Proportionen (50% = 85mm bei vollem A4-Preview).
* Breakpoint: nav (220px) + page-body-padding (45px) + A4-max-width (794px) = 1059px.
* Darunter stapeln sich die Spalten vertikal.
*/
.invoice-address {
display: grid;
grid-template-columns: 50% 1fr;
gap: 1rem;
}
@media (max-width: 1059px) {
.invoice-address {
grid-template-columns: 1fr;
}
.invoice-address > div:last-child {
justify-content: flex-start !important;
border-top: 1px solid #ddd;
padding-top: 0.75rem;
font-size: .85rem;
}
}
/* ── Modals ──────────────────────────────────────────────────── */
.modal-overlay {
display: none;

View file

@ -16,6 +16,7 @@ class SettingsController
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Einstellungen',
'topbarActions' => '<button type="submit" form="settings-form" class="btn btn-primary">Einstellungen speichern</button>',
'content' => $this->renderView('settings/form', [
'settings' => $settings,
'errors' => [],

View file

@ -21,6 +21,10 @@ class Setting
public static function update(array $data): void
{
$logoPosition = in_array($data['logo_position'] ?? '', ['center', 'left', 'right'])
? $data['logo_position']
: 'center';
Database::execute(
'UPDATE company_settings SET
firma=?, inhaber=?, strasse=?, plz=?, ort=?,
@ -31,7 +35,9 @@ class Setting
invoice_nr_prefix=?, beleg_nr_prefix=?,
payment_days_default=?, test_mode=?, ki_vorschlag_aktiv=?, ocr_aktiv=?,
nextcloud_url=?, nextcloud_user=?, nextcloud_pass=?,
nextcloud_folder=?, nextcloud_done=?
nextcloud_folder=?, nextcloud_done=?,
logo_max_width=?, logo_max_height=?,
logo_position=?, logo_margin_side=?, logo_margin_top=?
WHERE id=1',
[
$data['firma'] ?? '',
@ -61,6 +67,11 @@ class Setting
$data['nextcloud_pass'] ?: null,
$data['nextcloud_folder'] ?: '/Belege/Import',
$data['nextcloud_done'] ?: null,
max(50, (int)($data['logo_max_width'] ?? 600)),
max(20, (int)($data['logo_max_height'] ?? 100)),
$logoPosition,
max(0, (int)($data['logo_margin_side'] ?? 75)),
max(0, (int)($data['logo_margin_top'] ?? 6)),
]
);
}

View file

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace Steuerkram\Services;
use Mpdf\Mpdf;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;
class PdfInvoiceService
{
@ -19,8 +17,8 @@ class PdfInvoiceService
'format' => 'A4',
'margin_top' => 15,
'margin_bottom' => 25,
'margin_left' => 20,
'margin_right' => 20,
'margin_left' => 25, // DIN 5008: 25mm links
'margin_right' => 15, // DIN 5008: 15mm rechts
]);
$mpdf->SetTitle('Rechnung ' . ($invoice['rechnungs_nr'] ?? 'Entwurf'));
@ -31,28 +29,45 @@ class PdfInvoiceService
$mpdf->WriteHTML($html, \Mpdf\HTMLParserMode::HTML_BODY);
$filename = 'Rechnung-' . ($invoice['rechnungs_nr'] ?? 'Entwurf') . '.pdf';
$mpdf->Output($filename, 'D'); // D = Download
$mpdf->Output($filename, 'D');
}
private function buildHtml(array $inv, array $lines, array $settings): string
{
$h = fn($v) => htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
$fmt = fn($v) => number_format((float)$v, 2, ',', '.') . ' €';
$fmtNum = fn($v) => number_format((float)$v, 3, ',', '.');
$fmtNum = fn($v) => number_format((float)$v, 1, ',', '.');
$fmtDate = fn($v) => $v ? date('d.m.Y', strtotime($v)) : '–';
// Customer address
$customerName = !empty($inv['firma']) ? $inv['firma'] : trim(($inv['anrede'] ?? '') . ' ' . ($inv['vorname'] ?? '') . ' ' . $inv['nachname']);
// Kundenname
$customerName = !empty($inv['firma'])
? $inv['firma']
: trim(($inv['anrede'] ?? '') . ' ' . ($inv['vorname'] ?? '') . ' ' . $inv['nachname']);
// Logo
$logoHtml = '';
if (!empty($settings['logo_path'])) {
$logoPath = dirname(__DIR__, 2) . '/public/' . $settings['logo_path'];
if (file_exists($logoPath)) {
$logoHtml = '<img src="' . $h($settings['logo_path']) . '" alt="Logo" class="logo">';
$maxW = max(50, (int)($settings['logo_max_width'] ?? 600));
$maxH = max(20, (int)($settings['logo_max_height'] ?? 100));
$logoHtml = '<img src="' . $h($settings['logo_path']) . '" alt="Logo"'
. ' style="max-width:' . $maxW . 'px;max-height:' . $maxH . 'px">';
}
}
$logoPosition = $settings['logo_position'] ?? 'center';
$logoMarginSide = max(0, (int)($settings['logo_margin_side'] ?? 75));
$logoMarginTop = max(0, (int)($settings['logo_margin_top'] ?? 6));
if ($logoPosition === 'left') {
$logoWrapStyle = "text-align:left;margin-left:{$logoMarginSide}px;margin-top:{$logoMarginTop}px";
} elseif ($logoPosition === 'right') {
$logoWrapStyle = "text-align:right;margin-right:{$logoMarginSide}px;margin-top:{$logoMarginTop}px";
} else {
$logoWrapStyle = 'text-align:center';
}
// Leistungszeitraum
$lz = '';
if (!empty($inv['leistungszeitraum_von'])) {
@ -62,7 +77,12 @@ class PdfInvoiceService
}
}
// Build positions table
// Fälligkeitsdatum
$fälligText = !empty($inv['faellig_datum'])
? ' Fällig am: <strong>' . $fmtDate($inv['faellig_datum']) . '</strong>'
: '';
// Positionszeilen
$posRows = '';
foreach ($lines as $line) {
$posRows .= '<tr>
@ -78,26 +98,31 @@ class PdfInvoiceService
$payDays = $inv['payment_days'] ?? 14;
// Empfänger-Land nur wenn nicht DE
$land = ($inv['land'] ?? 'DE') !== 'DE' ? '<br>' . $h($inv['land']) : '';
return <<<HTML
<div class="invoice-wrap">
<!-- Logo -->
<div class="logo-wrap">{$logoHtml}</div>
<div class="logo-wrap" style="{$logoWrapStyle}">{$logoHtml}</div>
<!-- Adressbereich (DIN 5008) -->
<div class="address-block">
<div class="address-left">
<!-- Adressbereich DIN 5008 Typ A (Anschriftfeld 85mm × 45mm) -->
<table class="address-block">
<tr>
<!-- Linke Spalte: Anschriftfeld 85mm (Kuvertfenster) -->
<td class="address-left">
<div class="return-address">{$h($settings['firma'] ?? '')} · {$h($settings['strasse'] ?? '')} · {$h($settings['plz'] ?? '')} {$h($settings['ort'] ?? '')}</div>
<div class="recipient">
{$h($customerName)}<br>
{$h($inv['strasse'])}<br>
{$h($inv['plz'])} {$h($inv['ort'])}<br>
{$h($inv['land'] !== 'DE' ? $inv['land'] : '')}
{$h($inv['plz'])} {$h($inv['ort'])}{$land}
</div>
</div>
<div class="address-right">
</td>
<!-- Rechte Spalte: Absenderinformationen -->
<td class="address-right">
<table class="sender-table">
<tr><td>{$h($settings['firma'] ?? '')}</td></tr>
<tr><td><strong>{$h($settings['firma'] ?? '')}</strong></td></tr>
<tr><td>{$h($settings['inhaber'] ?? '')}</td></tr>
<tr><td>{$h($settings['strasse'] ?? '')}</td></tr>
<tr><td>{$h($settings['plz'] ?? '')} {$h($settings['ort'] ?? '')}</td></tr>
@ -106,11 +131,13 @@ class PdfInvoiceService
<tr><td>{$h($settings['email'] ?? '')}</td></tr>
<tr><td>USt-ID: {$h($settings['ust_id'] ?? '')}</td></tr>
</table>
</div>
</div>
</td>
</tr>
</table>
<!-- Meta-Felder -->
<table class="meta-table">
<thead>
<tr>
<th>Betreff</th>
<th>Rechnungsnummer</th>
@ -118,13 +145,16 @@ class PdfInvoiceService
<th>Leistungszeitraum</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
<tr>
<td>{$h($inv['betreff'])}</td>
<td>{$h($inv['rechnungs_nr'] ?? 'Entwurf')}</td>
<td><strong>{$h($inv['rechnungs_nr'] ?? 'Entwurf')}</strong></td>
<td>{$h($inv['kundennummer'])}</td>
<td>{$h($lz)}</td>
<td>{$h($lz ?: '–')}</td>
<td>{$fmtDate($inv['rechnungs_datum'])}</td>
</tr>
</tbody>
</table>
<!-- Positionen -->
@ -145,16 +175,19 @@ class PdfInvoiceService
</tbody>
</table>
<!-- Summen -->
<table class="sum-table">
<tr><td>Nettobetrag:</td><td class="sum-val">{$fmt($inv['betrag_netto'])}</td></tr>
<!-- Summen (rechtsbündig) -->
<table class="sum-table" align="right">
<tr><td>Nettobetrag:</td> <td class="sum-val">{$fmt($inv['betrag_netto'])}</td></tr>
<tr><td>zzgl. Umsatzsteuer:</td><td class="sum-val">{$fmt($inv['betrag_ust'])}</td></tr>
<tr class="sum-total"><td><strong>Rechnungsbetrag:</strong></td><td class="sum-val"><strong>{$fmt($inv['betrag_brutto'])}</strong></td></tr>
<tr class="sum-total">
<td><strong>Rechnungsbetrag:</strong></td>
<td class="sum-val"><strong>{$fmt($inv['betrag_brutto'])}</strong></td>
</tr>
</table>
<!-- Zahlungshinweis -->
<p class="payment-note">
Bitte überweisen Sie den Rechnungsbetrag innerhalb {$h($payDays)} Tagen auf das unten genannte Konto.
Bitte überweisen Sie den Rechnungsbetrag innerhalb {$h($payDays)} Tagen auf das unten genannte Konto.{$fälligText}
</p>
</div>
@ -197,48 +230,60 @@ HTML;
return <<<CSS
body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; color: #000; }
.invoice-wrap { width: 100%; }
.logo-wrap { text-align: center; margin-bottom: 10mm; }
.logo { max-height: 25mm; max-width: 80mm; }
/* Logo */
.logo-wrap { margin-bottom: 8mm; }
/* DIN 5008 Adressbereich */
.address-block { display: flex; justify-content: space-between; margin-bottom: 10mm; }
.address-left { width: 85mm; }
.return-address { font-size: 7pt; color: #555; border-bottom: 0.3pt solid #aaa; padding-bottom: 1mm; margin-bottom: 2mm; }
.recipient { font-size: 10pt; line-height: 1.4; min-height: 27.3mm; }
.address-right { width: 75mm; text-align: left; }
.sender-table td { font-size: 9pt; padding: 0.5mm 0; }
.address-block { width: 100%; border-collapse: collapse; margin-bottom: 8mm; }
.address-left { width: 85mm; vertical-align: top; padding: 0; }
.address-right { vertical-align: top; padding: 0 0 0 5mm; }
.return-address {
font-size: 7pt; color: #555;
border-bottom: 0.3pt solid #aaa;
padding-bottom: 1mm; margin-bottom: 2mm;
}
.recipient { font-size: 10pt; line-height: 1.5; min-height: 27.3mm; }
/* Absendertabelle: auf Inhaltsbreite schrumpfen, rechtsbündig */
.sender-table { border-collapse: collapse; font-size: 9pt; margin-left: auto; }
.sender-table td { padding: 0.4mm 0; white-space: nowrap; }
/* Meta-Felder */
.meta-table { width: 100%; border-collapse: collapse; margin-bottom: 8mm; font-size: 9pt; }
.meta-table th { background: #f0f0f0; padding: 2mm 2mm; text-align: left; font-size: 8pt; color: #555; border: 0.5pt solid #ccc; }
.meta-table { width: 100%; table-layout: fixed; border-collapse: collapse; margin-bottom: 8mm; font-size: 9pt; }
.meta-table th { background: #f0f0f0; padding: 2mm; text-align: left; font-size: 8pt; color: #555; border: 0.5pt solid #ccc; }
.meta-table td { padding: 2mm; border: 0.5pt solid #ccc; }
/* Positionen */
.pos-table { width: 100%; border-collapse: collapse; margin-bottom: 5mm; font-size: 9pt; }
.pos-table thead th { background: #1e293b; color: #fff; padding: 2mm; text-align: left; }
.pos-table { width: 100%; table-layout: fixed; border-collapse: collapse; margin-bottom: 5mm; font-size: 9pt; }
.pos-table thead th { background: #f0f0f0; color: #000; padding: 2mm; }
.pos-table tbody tr:nth-child(even) { background: #f8f8f8; }
.pos-table tbody td { padding: 1.5mm 2mm; border-bottom: 0.3pt solid #e0e0e0; }
.pos-nr { width: 10mm; }
.pos-bez { width: 55mm; }
.pos-menge { width: 18mm; text-align: right; }
.pos-einheit { width: 15mm; }
.pos-ust { width: 12mm; text-align: right; }
.pos-preis { width: 20mm; text-align: right; }
.pos-gesamt { width: 22mm; text-align: right; }
.pos-nr { width: 8%; }
.pos-bez { /* nimmt den Rest */ }
.pos-menge { width: 11%; text-align: right; }
.pos-einheit { width: 10%; }
.pos-ust { width: 8%; text-align: right; }
.pos-preis { width: 15%; text-align: right; }
.pos-gesamt { width: 16%; text-align: right; }
/* Summen */
.sum-table { float: right; width: 70mm; border-collapse: collapse; margin-bottom: 8mm; font-size: 9.5pt; }
.sum-table { border-collapse: collapse; margin-bottom: 8mm; font-size: 9pt; }
.sum-table td { padding: 1.5mm 2mm; }
.sum-table .sum-val { text-align: right; border-top: 0.3pt solid #ccc; }
.sum-table .sum-total td { border-top: 1pt solid #000; font-size: 10.5pt; }
.sum-table .sum-total td { border-top: 1pt solid #000; }
/* Zahlungshinweis */
.payment-note { clear: both; margin-top: 5mm; font-size: 9pt; padding: 3mm; background: #f8f8f8; border-left: 2pt solid #2563eb; }
.payment-note { margin-top: 5mm; font-size: 9pt; padding: 3mm; background: #f8f8f8; border-left: 2pt solid #2563eb; }
/* Fußzeile */
.footer-table { width: 100%; border-collapse: collapse; font-size: 7.5pt; border-top: 0.5pt solid #ccc; padding-top: 2mm; }
.footer-table td { width: 25%; padding: 1mm 2mm; vertical-align: top; }
.page-nr { text-align: right; }
.footer-table { width: 100%; border-collapse: collapse; font-size: 7.5pt; border-top: 0.5pt solid #ccc; }
.footer-table td { padding: 1mm 2mm; vertical-align: top; }
.footer-table td:nth-child(1) { width: 35%; }
.footer-table td:nth-child(2) { width: 25%; }
.footer-table td:nth-child(3) { width: 28%; }
.page-nr { width: 12%; text-align: right; }
CSS;
}
}

View file

@ -15,6 +15,15 @@ if (!empty($invoice['leistungszeitraum_von'])) {
}
}
// Leistungszeitraum mit <br> für die Vorschau-Tabelle (Datumsangaben untereinander)
$lzHtml = '';
if (!empty($invoice['leistungszeitraum_von'])) {
$lzHtml = $h(View::formatDate($invoice['leistungszeitraum_von']));
if (!empty($invoice['leistungszeitraum_bis']) && $invoice['leistungszeitraum_bis'] !== $invoice['leistungszeitraum_von']) {
$lzHtml .= '<br>' . $h(View::formatDate($invoice['leistungszeitraum_bis']));
}
}
$badgeClass = match($invoice['status']) {
'Bezahlt' => 'badge-green',
'Storniert' => 'badge-red',
@ -56,13 +65,28 @@ $badgeClass = match($invoice['status']) {
<!-- Logo -->
<?php if (!empty($settings['logo_path'])): ?>
<div style="text-align:center;margin-bottom:1.5rem">
<img src="/<?= $h($settings['logo_path']) ?>" alt="Logo" style="max-height:60px;max-width:250px">
<?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 -->
<div style="display:grid;grid-template-columns:100mm 1fr;gap:1rem;margin-bottom:1.5rem">
<!-- Adressbereich (DIN 5008: 50% = 85mm bei vollem A4-Preview) -->
<div class="invoice-address" style="margin-bottom:1.5rem">
<div>
<div style="font-size:8pt;color:#666;border-bottom:1px solid #ccc;padding-bottom:2px;margin-bottom:4px">
<?= $h($settings['firma'] ?? '') ?> · <?= $h($settings['strasse'] ?? '') ?> · <?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?>
@ -74,7 +98,8 @@ $badgeClass = match($invoice['status']) {
<?php if ($invoice['land'] !== 'DE'): ?><br><?= $h($invoice['land']) ?><?php endif; ?>
</div>
</div>
<div style="text-align:left;font-size:.85rem;line-height:1.7">
<div style="display:flex;justify-content:flex-end;font-size:.85rem;line-height:1.7">
<div style="text-align:left">
<strong><?= $h($settings['firma'] ?? '') ?></strong><br>
<?= $h($settings['inhaber'] ?? '') ?><br>
<?= $h($settings['strasse'] ?? '') ?><br>
@ -84,40 +109,41 @@ $badgeClass = match($invoice['status']) {
USt-ID: <?= $h($settings['ust_id'] ?? '') ?>
</div>
</div>
</div>
<!-- Meta-Tabelle -->
<table style="width:100%;border-collapse:collapse;margin-bottom:1.5rem;font-size:.9rem">
<thead>
<tr style="background:#f0f0f0">
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-size:.8rem">Betreff</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-size:.8rem">Rechnungsnummer</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-size:.8rem">Kundennummer</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-size:.8rem">Leistungszeitraum</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-size:.8rem">Datum</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-weight:normal;white-space:nowrap">Betreff</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-weight:normal;white-space:nowrap">Rechnungsnummer</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-weight:normal;white-space:nowrap">Kundennummer</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-weight:normal;white-space:nowrap">Leistungszeitraum</th>
<th style="padding:.4rem .5rem;text-align:left;border:1px solid #ddd;font-weight:normal;white-space:nowrap;width:100%">Datum</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding:.4rem .5rem;border:1px solid #ddd"><?= $h($invoice['betreff']) ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd;font-weight:700"><?= $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd"><?= $h($invoice['kundennummer']) ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd"><?= $h($lz ?: '–') ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd"><?= View::formatDate($invoice['rechnungs_datum']) ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd;white-space:nowrap"><?= $h($invoice['betreff']) ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd;white-space:nowrap;font-weight:700"><?= $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd;white-space:nowrap"><?= $h($invoice['kundennummer']) ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd"><?= $lzHtml ?: '–' ?></td>
<td style="padding:.4rem .5rem;border:1px solid #ddd;white-space:nowrap"><?= $h(View::formatDate($invoice['rechnungs_datum'])) ?></td>
</tr>
</tbody>
</table>
<!-- Positionen -->
<table style="width:100%;border-collapse:collapse;margin-bottom:1.5rem;font-size:.9rem">
<table style="width:100%;table-layout:fixed;border-collapse:collapse;margin-bottom:1.5rem;font-size:.9rem">
<thead>
<tr style="background:#1e293b;color:#fff">
<th style="padding:.5rem;text-align:left;width:40px">Pos.</th>
<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:70px">Menge</th>
<th style="padding:.5rem;text-align:left;width:70px">Einheit</th>
<th style="padding:.5rem;text-align:right;width:60px">USt.</th>
<th style="padding:.5rem;text-align:right;width:100px">Einzelpreis</th>
<th style="padding:.5rem;text-align:right;width:110px">Gesamt</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>
@ -125,7 +151,7 @@ $badgeClass = match($invoice['status']) {
<tr style="border-bottom:1px solid #eee">
<td style="padding:.4rem .5rem"><?= $h($line['position']) ?></td>
<td style="padding:.4rem .5rem"><?= $h($line['bezeichnung']) ?></td>
<td style="padding:.4rem .5rem;text-align:right"><?= number_format((float)$line['menge'], 3, ',', '.') ?></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>
@ -154,20 +180,20 @@ $badgeClass = match($invoice['status']) {
</div>
<!-- Fußzeile -->
<div style="border-top:1px solid #ccc;padding-top:.75rem;display:grid;grid-template-columns:1fr 1fr 1fr 80px;gap:1rem;font-size:.75rem;color:#555">
<div>
<div style="border-top:1px solid #ccc;padding-top:.75rem;display:grid;grid-template-columns:auto auto auto 1fr;gap:1rem;font-size:.75rem;color:#555">
<div style="min-width:0">
<strong><?= $h($settings['firma'] ?? '') ?></strong><br>
<?= $h($settings['inhaber'] ?? '') ?><br>
<?= $h($settings['strasse'] ?? '') ?><br>
<?= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?><br>
USt-ID: <?= $h($settings['ust_id'] ?? '') ?>
</div>
<div>
<div style="min-width:0">
Tel: <?= $h($settings['telefon'] ?? '') ?><br>
E-Mail: <?= $h($settings['email'] ?? '') ?><br>
Web: <?= $h($settings['website'] ?? '') ?>
</div>
<div>
<div style="min-width:0">
<?= $h($settings['bank_name'] ?? '') ?><br>
IBAN: <?= $h($settings['iban'] ?? '') ?><br>
BIC: <?= $h($settings['bic'] ?? '') ?>

View file

@ -1,6 +1,6 @@
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
<form method="post" action="/settings">
<form id="settings-form" method="post" action="/settings">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start">
<!-- Firmendaten -->
@ -85,8 +85,6 @@
<!-- Bankdaten + Logo + Nummernkreise -->
<div>
<button type="submit" class="btn btn-primary btn-lg w-full" style="margin-bottom:1rem">Einstellungen speichern</button>
<!-- Betriebsmodus -->
<div class="card mb-2" style="border:2px solid <?= ($settings['test_mode'] ?? 1) ? 'var(--color-warning,#f59e0b)' : 'var(--color-success,#22c55e)' ?>">
<div class="card-header">
@ -239,28 +237,6 @@
</div>
</div>
<div class="card mb-2">
<div class="card-header"><div class="card-title">Logo</div></div>
<div class="card-body">
<?php if (!empty($settings['logo_path'])): ?>
<div style="text-align:center;margin-bottom:1rem">
<img src="/<?= $h($settings['logo_path']) ?>" alt="Logo" style="max-height:80px;max-width:200px">
</div>
<form method="post" action="/settings/logo/delete" style="margin-bottom:1rem">
<button type="submit" class="btn btn-danger btn-sm w-full">Logo löschen</button>
</form>
<?php endif; ?>
<form method="post" action="/settings/logo" enctype="multipart/form-data">
<div class="form-group">
<label>Logo hochladen (PNG, JPG)</label>
<input type="file" name="logo" accept=".png,.jpg,.jpeg">
<div class="form-hint">Empfohlen: transparentes PNG, mind. 300px breit</div>
</div>
<button type="submit" class="btn btn-secondary">Logo hochladen</button>
</form>
</div>
</div>
<div class="card mb-2">
<div class="card-header"><div class="card-title">Nummernkreise</div></div>
<div class="card-body">
@ -286,6 +262,82 @@
</div>
</form>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin-top:0">
<div></div>
<div>
<div class="card mb-2">
<div class="card-header"><div class="card-title">Logo</div></div>
<div class="card-body">
<?php if (!empty($settings['logo_path'])): ?>
<div style="text-align:center;margin-bottom:1rem">
<img src="/<?= $h($settings['logo_path']) ?>" alt="Logo" style="max-height:80px;max-width:400px">
</div>
<?php endif; ?>
<form method="post" action="/settings/logo" enctype="multipart/form-data">
<div class="form-group">
<label>Logo hochladen (PNG, JPG)</label>
<input type="file" name="logo" accept=".png,.jpg,.jpeg">
<div class="form-hint">Empfohlen: transparentes PNG, mind. 300px breit</div>
</div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button type="submit" class="btn btn-secondary">Hochladen</button>
<?php if (!empty($settings['logo_path'])): ?>
<button type="submit" form="delete-logo-form" class="btn btn-danger btn-sm">Löschen</button>
<?php endif; ?>
</div>
</form>
<?php if (!empty($settings['logo_path'])): ?>
<form id="delete-logo-form" method="post" action="/settings/logo/delete"></form>
<?php endif; ?>
<div style="margin-top:1rem;border-top:1px solid var(--color-border,#e5e7eb);padding-top:1rem">
<div class="form-hint" style="margin-bottom:.75rem">Darstellung in der Rechnung</div>
<div class="form-grid form-grid-2">
<div class="form-group">
<label>Max-Breite</label>
<input type="number" name="logo_max_width" form="settings-form"
value="<?= $h($settings['logo_max_width'] ?? 600) ?>" min="50" max="2000">
</div>
<div class="form-group">
<label>Max-Höhe</label>
<input type="number" name="logo_max_height" form="settings-form"
value="<?= $h($settings['logo_max_height'] ?? 100) ?>" min="20" max="500">
</div>
</div>
<div class="form-group">
<label>Position</label>
<div style="display:flex;gap:1.5rem;margin-top:.35rem">
<?php foreach (['center' => 'Zentriert', 'left' => 'Links', 'right' => 'Rechts'] as $val => $label): ?>
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer">
<input type="radio" name="logo_position" value="<?= $val ?>" form="settings-form"
<?= ($settings['logo_position'] ?? 'center') === $val ? 'checked' : '' ?>
onchange="logoPositionChanged()">
<?= $label ?>
</label>
<?php endforeach; ?>
</div>
</div>
<?php $showMargins = in_array($settings['logo_position'] ?? 'center', ['left', 'right']); ?>
<div id="logo-margin-fields" style="display:<?= $showMargins ? 'block' : 'none' ?>">
<div class="form-grid form-grid-2">
<div class="form-group">
<label>Rand Seite</label>
<input type="number" name="logo_margin_side" form="settings-form"
value="<?= $h($settings['logo_margin_side'] ?? 75) ?>" min="0">
</div>
<div class="form-group">
<label>Rand oben</label>
<input type="number" name="logo_margin_top" form="settings-form"
value="<?= $h($settings['logo_margin_top'] ?? 6) ?>" min="0">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function updateModeLabel(cb) {
const track = document.getElementById('toggle_track');
@ -312,4 +364,10 @@ function updateModeLabel(cb) {
cb.closest('.card').querySelector('.badge').textContent = 'Produktiv';
}
}
function logoPositionChanged() {
const pos = document.querySelector('input[name="logo_position"]:checked')?.value;
document.getElementById('logo-margin-fields').style.display =
(pos === 'left' || pos === 'right') ? 'block' : 'none';
}
</script>