diff --git a/migrations/029_logo_position.sql b/migrations/029_logo_position.sql
new file mode 100644
index 0000000..2960f1f
--- /dev/null
+++ b/migrations/029_logo_position.sql
@@ -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;
diff --git a/public/static/css/app.css b/public/static/css/app.css
index a28181f..4e00b26 100644
--- a/public/static/css/app.css
+++ b/public/static/css/app.css
@@ -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;
diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php
index bc18b53..e262a5e 100644
--- a/src/Controllers/SettingsController.php
+++ b/src/Controllers/SettingsController.php
@@ -15,8 +15,9 @@ class SettingsController
$view = new View();
$view->render('layout/base.php', [
- 'pageTitle' => 'Einstellungen',
- 'content' => $this->renderView('settings/form', [
+ 'pageTitle' => 'Einstellungen',
+ 'topbarActions' => 'Einstellungen speichern ',
+ 'content' => $this->renderView('settings/form', [
'settings' => $settings,
'errors' => [],
]),
diff --git a/src/Models/Setting.php b/src/Models/Setting.php
index 903e4c2..c112139 100644
--- a/src/Models/Setting.php
+++ b/src/Models/Setting.php
@@ -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)),
]
);
}
diff --git a/src/Services/PdfInvoiceService.php b/src/Services/PdfInvoiceService.php
index 13d3e60..718c311 100644
--- a/src/Services/PdfInvoiceService.php
+++ b/src/Services/PdfInvoiceService.php
@@ -5,8 +5,6 @@ declare(strict_types=1);
namespace Steuerkram\Services;
use Mpdf\Mpdf;
-use Mpdf\Config\ConfigVariables;
-use Mpdf\Config\FontVariables;
class PdfInvoiceService
{
@@ -15,12 +13,12 @@ class PdfInvoiceService
$html = $this->buildHtml($invoice, $lines, $settings);
$mpdf = new Mpdf([
- 'mode' => 'utf-8',
- 'format' => 'A4',
- 'margin_top' => 15,
+ 'mode' => 'utf-8',
+ '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, ',', '.');
+ $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, 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 = ' ';
+ $maxW = max(50, (int)($settings['logo_max_width'] ?? 600));
+ $maxH = max(20, (int)($settings['logo_max_height'] ?? 100));
+ $logoHtml = ' ';
}
}
+ $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,69 +77,84 @@ class PdfInvoiceService
}
}
- // Build positions table
+ // Fälligkeitsdatum
+ $fälligText = !empty($inv['faellig_datum'])
+ ? ' Fällig am: ' . $fmtDate($inv['faellig_datum']) . ' '
+ : '';
+
+ // Positionszeilen
$posRows = '';
foreach ($lines as $line) {
$posRows .= '
- ' . $h($line['position']) . '
- ' . $h($line['bezeichnung']) . '
- ' . $fmtNum($line['menge']) . '
- ' . $h($line['einheit']) . '
- ' . $h(number_format((float)$line['ust_satz'], 0)) . ' %
- ' . $fmt($line['einzelpreis']) . '
- ' . $fmt($line['betrag_brutto']) . '
+ ' . $h($line['position']) . '
+ ' . $h($line['bezeichnung']) . '
+ ' . $fmtNum($line['menge']) . '
+ ' . $h($line['einheit']) . '
+ ' . $h(number_format((float)$line['ust_satz'], 0)) . ' %
+ ' . $fmt($line['einzelpreis']) . '
+ ' . $fmt($line['betrag_brutto']) . '
';
}
$payDays = $inv['payment_days'] ?? 14;
+ // Empfänger-Land nur wenn nicht DE
+ $land = ($inv['land'] ?? 'DE') !== 'DE' ? ' ' . $h($inv['land']) : '';
+
return <<
- {$logoHtml}
+ {$logoHtml}
-
-
-
-
{$h($settings['firma'] ?? '')} · {$h($settings['strasse'] ?? '')} · {$h($settings['plz'] ?? '')} {$h($settings['ort'] ?? '')}
-
- {$h($customerName)}
- {$h($inv['strasse'])}
- {$h($inv['plz'])} {$h($inv['ort'])}
- {$h($inv['land'] !== 'DE' ? $inv['land'] : '')}
-
-
-
-
- {$h($settings['firma'] ?? '')}
- {$h($settings['inhaber'] ?? '')}
- {$h($settings['strasse'] ?? '')}
- {$h($settings['plz'] ?? '')} {$h($settings['ort'] ?? '')}
-
- {$h($settings['telefon'] ?? '')}
- {$h($settings['email'] ?? '')}
- USt-ID: {$h($settings['ust_id'] ?? '')}
-
-
-
+
+
+
+
+
+ {$h($settings['firma'] ?? '')} · {$h($settings['strasse'] ?? '')} · {$h($settings['plz'] ?? '')} {$h($settings['ort'] ?? '')}
+
+ {$h($customerName)}
+ {$h($inv['strasse'])}
+ {$h($inv['plz'])} {$h($inv['ort'])}{$land}
+
+
+
+
+
+ {$h($settings['firma'] ?? '')}
+ {$h($settings['inhaber'] ?? '')}
+ {$h($settings['strasse'] ?? '')}
+ {$h($settings['plz'] ?? '')} {$h($settings['ort'] ?? '')}
+
+ {$h($settings['telefon'] ?? '')}
+ {$h($settings['email'] ?? '')}
+ USt-ID: {$h($settings['ust_id'] ?? '')}
+
+
+
+
@@ -145,16 +175,19 @@ class PdfInvoiceService
-
-
- Nettobetrag: {$fmt($inv['betrag_netto'])}
+
+
+ Nettobetrag: {$fmt($inv['betrag_netto'])}
zzgl. Umsatzsteuer: {$fmt($inv['betrag_ust'])}
- Rechnungsbetrag: {$fmt($inv['betrag_brutto'])}
+
+ Rechnungsbetrag:
+ {$fmt($inv['betrag_brutto'])}
+
- 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}
@@ -197,48 +230,60 @@ HTML;
return << 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 .= ' ' . $h(View::formatDate($invoice['leistungszeitraum_bis']));
+ }
+}
+
$badgeClass = match($invoice['status']) {
'Bezahlt' => 'badge-green',
'Storniert' => 'badge-red',
@@ -56,13 +65,28 @@ $badgeClass = match($invoice['status']) {
-
-
+
+
+
-
-
+
+
= $h($settings['firma'] ?? '') ?> · = $h($settings['strasse'] ?? '') ?> · = $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?>
@@ -74,14 +98,16 @@ $badgeClass = match($invoice['status']) {
= $h($invoice['land']) ?>
-
-
= $h($settings['firma'] ?? '') ?>
- = $h($settings['inhaber'] ?? '') ?>
- = $h($settings['strasse'] ?? '') ?>
- = $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?>
- = $h($settings['telefon'] ?? '') ?>
- = $h($settings['email'] ?? '') ?>
- USt-ID: = $h($settings['ust_id'] ?? '') ?>
+
+
+ = $h($settings['firma'] ?? '') ?>
+ = $h($settings['inhaber'] ?? '') ?>
+ = $h($settings['strasse'] ?? '') ?>
+ = $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?>
+ = $h($settings['telefon'] ?? '') ?>
+ = $h($settings['email'] ?? '') ?>
+ USt-ID: = $h($settings['ust_id'] ?? '') ?>
+
@@ -89,35 +115,35 @@ $badgeClass = match($invoice['status']) {
- Betreff
- Rechnungsnummer
- Kundennummer
- Leistungszeitraum
- Datum
+ Betreff
+ Rechnungsnummer
+ Kundennummer
+ Leistungszeitraum
+ Datum
- = $h($invoice['betreff']) ?>
- = $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?>
- = $h($invoice['kundennummer']) ?>
- = $h($lz ?: '–') ?>
- = View::formatDate($invoice['rechnungs_datum']) ?>
+ = $h($invoice['betreff']) ?>
+ = $h($invoice['rechnungs_nr'] ?? 'Entwurf') ?>
+ = $h($invoice['kundennummer']) ?>
+ = $lzHtml ?: '–' ?>
+ = $h(View::formatDate($invoice['rechnungs_datum'])) ?>
-
+
-
- Pos.
+
+ Pos.
Bezeichnung
- Menge
- Einheit
- USt.
- Einzelpreis
- Gesamt
+ Menge
+ Einheit
+ USt.
+ Einzelpreis
+ Gesamt
@@ -125,7 +151,7 @@ $badgeClass = match($invoice['status']) {
= $h($line['position']) ?>
= $h($line['bezeichnung']) ?>
- = number_format((float)$line['menge'], 3, ',', '.') ?>
+ = number_format((float)$line['menge'], 1, ',', '.') ?>
= $h($line['einheit']) ?>
= number_format((float)$line['ust_satz'], 0) ?> %
= $fmt($line['einzelpreis']) ?>
@@ -154,20 +180,20 @@ $badgeClass = match($invoice['status']) {
-
-
+
+
= $h($settings['firma'] ?? '') ?>
= $h($settings['inhaber'] ?? '') ?>
= $h($settings['strasse'] ?? '') ?>
= $h(($settings['plz'] ?? '') . ' ' . ($settings['ort'] ?? '')) ?>
USt-ID: = $h($settings['ust_id'] ?? '') ?>
-
+
Tel: = $h($settings['telefon'] ?? '') ?>
E-Mail: = $h($settings['email'] ?? '') ?>
Web: = $h($settings['website'] ?? '') ?>
-
+
= $h($settings['bank_name'] ?? '') ?>
IBAN: = $h($settings['iban'] ?? '') ?>
BIC: = $h($settings['bic'] ?? '') ?>
diff --git a/src/Views/settings/form.php b/src/Views/settings/form.php
index 679f2d4..d61b970 100644
--- a/src/Views/settings/form.php
+++ b/src/Views/settings/form.php
@@ -1,6 +1,6 @@
View::h($v); ?>
-