kleine Verbesserungen, Favicon und Sicherheit
This commit is contained in:
parent
a0ddc6a5d8
commit
4f06a19c2c
20 changed files with 276 additions and 42 deletions
28
Changes.md
Normal file
28
Changes.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Changelog
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-25 – Sicherheit, UX-Verbesserungen & Bugfixes
|
||||
|
||||
### Sicherheit
|
||||
- **Timing-sicherer Login**: Passwort- und Benutzernamevergleich auf `hash_equals()` umgestellt (verhindert Timing-Angriffe)
|
||||
- **SVG-Upload gesperrt**: Logo-Upload akzeptiert nur noch PNG und JPEG – SVG wurde entfernt (XSS-Risiko)
|
||||
- **CSV-Injection-Schutz**: Felder im Buchungsjournal-CSV-Export werden gegen Formula-Injection (`=`, `+`, `-`, `@`) abgesichert
|
||||
|
||||
### Features
|
||||
- **Inline-Editing**: Belegdatum und Rechnungsnr. können direkt in der Beleg-Detailansicht per Klick bearbeitet werden (kein Seitenreload)
|
||||
- **Belegart-Filter**: Neue Filteroption in der Belegliste zum Filtern nach Belegart (Eingangsrechnung, Ausgangsrechnung, Quittung etc.)
|
||||
- **Favicon & App-Icon**: Favicon für Browser-Tabs, Apple Touch Icon und Web-App-Manifest eingebunden
|
||||
|
||||
### Robustheit
|
||||
- **Datenbankfehler-Handling**: PDO-Exceptions in Invoice-, Booking- und BelegController werden separat abgefangen und geloggt statt als generische Fehler zu erscheinen
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-24 – Initiales Release
|
||||
|
||||
### Features
|
||||
- **Basis-System**: MVC-Framework, Routing, Authentifizierung, Datenbank-Migrations-System
|
||||
- **Ausgangsrechnungen**: Erstellen, Verwalten und PDF-Export von Ausgangsrechnungen
|
||||
- **EÜR-Vorbereitung**: Einnahmen-Überschuss-Rechnung als PDF-Export
|
||||
- **UStVA-Vorbereitung**: Umsatzsteuer-Voranmeldung inkl. XML-Export für elster.de
|
||||
8
migrations/004_rechnungs_nr_from_filename.sql
Normal file
8
migrations/004_rechnungs_nr_from_filename.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Migration 004: Rechnungsnummer aus Dateinamen ableiten (bestehende Belege)
|
||||
-- Setzt rechnungs_nr auf den Dateinamen ohne Extension, wo rechnungs_nr leer/NULL ist
|
||||
-- und storage_path auf eine echte Datei zeigt (nicht automatisch erstellte Ausgangsrechnungen).
|
||||
UPDATE belege
|
||||
SET rechnungs_nr = REGEXP_REPLACE(filename, '\\.[^.]+$', ''),
|
||||
updated_at = NOW()
|
||||
WHERE (rechnungs_nr IS NULL OR rechnungs_nr = '')
|
||||
AND storage_path != '';
|
||||
BIN
public/static/favicon/apple-touch-icon.png
Normal file
BIN
public/static/favicon/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
public/static/favicon/favicon-96x96.png
Normal file
BIN
public/static/favicon/favicon-96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/static/favicon/favicon.ico
Normal file
BIN
public/static/favicon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
public/static/favicon/favicon.svg
Normal file
1
public/static/favicon/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 33 KiB |
21
public/static/favicon/site.webmanifest
Normal file
21
public/static/favicon/site.webmanifest
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
public/static/favicon/web-app-manifest-192x192.png
Normal file
BIN
public/static/favicon/web-app-manifest-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/static/favicon/web-app-manifest-512x512.png
Normal file
BIN
public/static/favicon/web-app-manifest-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
|
|
@ -255,6 +255,101 @@ function setupAccountSearch(inputId, hiddenId) {
|
|||
input.addEventListener('blur', () => setTimeout(closeDropdown, 150));
|
||||
}
|
||||
|
||||
// ── Inline-Edit (Belegdatum / Rechnungsnr. in der Info-Bar) ──────
|
||||
document.addEventListener('click', (e) => {
|
||||
const trigger = e.target.closest('.inline-edit-trigger');
|
||||
if (!trigger || trigger.querySelector('input')) return;
|
||||
|
||||
const bar = trigger.closest('[data-beleg-id]');
|
||||
if (!bar) return;
|
||||
|
||||
const field = trigger.dataset.field;
|
||||
const type = trigger.dataset.type || 'text';
|
||||
const rawValue = trigger.dataset.raw || '';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = type;
|
||||
input.value = rawValue;
|
||||
input.style.cssText = 'font-size:inherit;padding:2px 5px;border:1px solid var(--color-border);border-radius:var(--radius);background:#fff;min-width:' + (type === 'date' ? '140px' : '180px');
|
||||
|
||||
const originalHTML = trigger.innerHTML;
|
||||
trigger.innerHTML = '';
|
||||
trigger.appendChild(input);
|
||||
input.focus();
|
||||
if (type === 'text') input.select();
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function save() {
|
||||
if (cancelled) return;
|
||||
const newValue = input.value.trim();
|
||||
|
||||
const body = new URLSearchParams({
|
||||
rechnungs_nr: field === 'rechnungs_nr' ? newValue : bar.dataset.rechnungsNr || '',
|
||||
belegtyp: bar.dataset.belegtyp || '',
|
||||
beleg_datum: field === 'beleg_datum' ? newValue : bar.dataset.belegDatum || '',
|
||||
betrag_brutto: bar.dataset.betragBrutto || '',
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch(`/belege/${bar.dataset.belegId}/meta`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
// Update stored data-attribute
|
||||
if (field === 'beleg_datum') {
|
||||
bar.dataset.belegDatum = newValue;
|
||||
trigger.dataset.raw = newValue;
|
||||
if (newValue) {
|
||||
const [y, m, d] = newValue.split('-');
|
||||
trigger.textContent = `${d}.${m}.${y}`;
|
||||
} else {
|
||||
trigger.innerHTML = '<span class="text-muted">– setzen –</span>';
|
||||
}
|
||||
} else {
|
||||
bar.dataset.rechnungsNr = newValue;
|
||||
trigger.dataset.raw = newValue;
|
||||
trigger.textContent = newValue || '';
|
||||
if (!newValue) trigger.innerHTML = '<span class="text-muted">– setzen –</span>';
|
||||
}
|
||||
} catch (_) {
|
||||
trigger.innerHTML = originalHTML;
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener('blur', save);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
|
||||
if (e.key === 'Escape') { cancelled = true; trigger.innerHTML = originalHTML; }
|
||||
});
|
||||
});
|
||||
|
||||
// ── Konto-Select: Doppelklick / Enter → Kontonr. ins Filterfeld ──
|
||||
function transferKontoNr(select) {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
if (!opt || !opt.value) return;
|
||||
const kontoNr = opt.textContent.split(' – ')[0].trim();
|
||||
const filterInput = document.querySelector(`.konto-filter[data-target="${select.id}"]`);
|
||||
if (filterInput) filterInput.value = kontoNr;
|
||||
}
|
||||
|
||||
document.addEventListener('dblclick', (e) => {
|
||||
const select = e.target.closest('select#soll_konto_id, select#haben_konto_id');
|
||||
if (select) transferKontoNr(select);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
const select = e.target.closest('select#soll_konto_id, select#haben_konto_id');
|
||||
if (select) {
|
||||
e.preventDefault();
|
||||
transferKontoNr(select);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Flash Message Auto-dismiss ────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.flash').forEach(el => {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ class Auth
|
|||
|
||||
if (
|
||||
!isset($_SERVER['PHP_AUTH_USER']) ||
|
||||
$_SERVER['PHP_AUTH_USER'] !== $user ||
|
||||
$_SERVER['PHP_AUTH_PW'] !== $pass
|
||||
!hash_equals($user, $_SERVER['PHP_AUTH_USER']) ||
|
||||
!hash_equals($pass, $_SERVER['PHP_AUTH_PW'])
|
||||
) {
|
||||
header('WWW-Authenticate: Basic realm="Steuerkram"');
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ class BelegController
|
|||
public function index(array $params): void
|
||||
{
|
||||
$filter = $_GET;
|
||||
if (!isset($filter['year'])) $filter['year'] = (string)date('Y');
|
||||
if (!isset($filter['month'])) $filter['month'] = (string)date('n');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$result = Beleg::all($filter, $page);
|
||||
$totalAll = Beleg::all([], 1, 1)['total'];
|
||||
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
|
|
@ -33,6 +36,7 @@ class BelegController
|
|||
'content' => $this->renderView('belege/list', [
|
||||
'rows' => $result['rows'],
|
||||
'total' => $result['total'],
|
||||
'totalAll' => $totalAll,
|
||||
'pages' => $result['pages'],
|
||||
'page' => $page,
|
||||
'filter' => $filter,
|
||||
|
|
@ -99,6 +103,7 @@ class BelegController
|
|||
if (!is_dir($absDir)) mkdir($absDir, 0755, true);
|
||||
|
||||
$filename = pathinfo($file['name'], PATHINFO_BASENAME);
|
||||
$filenameWithoutExt = pathinfo($file['name'], PATHINFO_FILENAME);
|
||||
$storagePath = "{$relDir}/original.{$ext}";
|
||||
$absPath = dirname(__DIR__, 2) . "/public/{$storagePath}";
|
||||
|
||||
|
|
@ -112,8 +117,8 @@ class BelegController
|
|||
'file_hash' => $hash,
|
||||
'beleg_datum' => $_POST['beleg_datum'] ?? null,
|
||||
'eingangs_datum'=> $_POST['eingangs_datum'] ?? date('Y-m-d'),
|
||||
'rechnungs_nr' => $_POST['rechnungs_nr'] ?? null,
|
||||
'betrag_brutto' => $_POST['betrag_brutto'] ?? null,
|
||||
'rechnungs_nr' => $filenameWithoutExt,
|
||||
'betrag_brutto' => $_POST['betrag_brutto'] !== '' ? $_POST['betrag_brutto'] : null,
|
||||
'belegtyp' => $_POST['belegtyp'] ?? 'Eingangsrechnung',
|
||||
'notes' => $_POST['notes'] ?? null,
|
||||
]);
|
||||
|
|
@ -164,10 +169,16 @@ class BelegController
|
|||
|
||||
public function updateMeta(array $params): void
|
||||
{
|
||||
$isAjax = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest';
|
||||
$id = (int)$params['id'];
|
||||
$beleg = Beleg::find($id);
|
||||
if (!$beleg) { http_response_code(404); return; }
|
||||
|
||||
if (!$beleg) {
|
||||
if ($isAjax) { http_response_code(404); header('Content-Type: application/json'); echo json_encode(['error' => 'Nicht gefunden.']); return; }
|
||||
http_response_code(404); return;
|
||||
}
|
||||
if ($beleg['locked']) {
|
||||
if ($isAjax) { http_response_code(403); header('Content-Type: application/json'); echo json_encode(['error' => 'GoBD: Gesperrter Beleg.']); return; }
|
||||
View::redirect("/belege/{$id}", 'GoBD: Gesperrter Beleg kann nicht geändert werden.', 'error');
|
||||
return;
|
||||
}
|
||||
|
|
@ -176,9 +187,15 @@ class BelegController
|
|||
'rechnungs_nr' => trim($_POST['rechnungs_nr'] ?? ''),
|
||||
'belegtyp' => $_POST['belegtyp'] ?? $beleg['belegtyp'],
|
||||
'beleg_datum' => $_POST['beleg_datum'] ?: null,
|
||||
'betrag_brutto' => $_POST['betrag_brutto'] !== '' ? $_POST['betrag_brutto'] : null,
|
||||
'betrag_brutto' => ($_POST['betrag_brutto'] ?? '') !== '' ? $_POST['betrag_brutto'] : null,
|
||||
]);
|
||||
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => true]);
|
||||
return;
|
||||
}
|
||||
|
||||
View::redirect("/belege/{$id}", 'Metadaten gespeichert.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@ class BookingController
|
|||
public function journal(array $params): void
|
||||
{
|
||||
$filter = $_GET;
|
||||
if (!isset($filter['year'])) $filter['year'] = (string)date('Y');
|
||||
if (!isset($filter['month'])) $filter['month'] = (string)date('n');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$result = Booking::journal($filter, $page);
|
||||
$totalAll = Booking::journal([], 1, 1)['total'];
|
||||
$years = FiscalYear::availableYears();
|
||||
$accounts = Account::all(['aktiv' => 1]);
|
||||
|
||||
|
|
@ -27,6 +30,7 @@ class BookingController
|
|||
'content' => $this->renderView('bookings/journal', [
|
||||
'rows' => $result['rows'],
|
||||
'total' => $result['total'],
|
||||
'totalAll' => $totalAll,
|
||||
'pages' => $result['pages'],
|
||||
'page' => $page,
|
||||
'filter' => $filter,
|
||||
|
|
@ -58,18 +62,18 @@ class BookingController
|
|||
|
||||
foreach ($result['rows'] as $row) {
|
||||
fputcsv($out, [
|
||||
$row['buchungs_nr'],
|
||||
$this->csvSanitize((string)$row['buchungs_nr']),
|
||||
$row['buchungs_datum'],
|
||||
$row['beleg_nr'],
|
||||
$this->csvSanitize((string)$row['beleg_nr']),
|
||||
$row['soll_nr'],
|
||||
$row['soll_name'],
|
||||
$this->csvSanitize((string)$row['soll_name']),
|
||||
$row['haben_nr'],
|
||||
$row['haben_name'],
|
||||
$this->csvSanitize((string)$row['haben_name']),
|
||||
number_format((float)$row['betrag_netto'], 2, ',', ''),
|
||||
number_format((float)$row['ust_satz'], 2, ',', ''),
|
||||
number_format((float)$row['betrag_ust'], 2, ',', ''),
|
||||
number_format((float)$row['betrag_brutto'], 2, ',', ''),
|
||||
$row['buchungstext'],
|
||||
$this->csvSanitize((string)$row['buchungstext']),
|
||||
$row['is_reversal'] ? 'Ja' : 'Nein',
|
||||
], ';');
|
||||
}
|
||||
|
|
@ -103,6 +107,9 @@ class BookingController
|
|||
$data['beleg_id'] = $belegId;
|
||||
Booking::create($data);
|
||||
View::redirect("/belege/{$belegId}", 'Buchung erfasst.');
|
||||
} catch (\PDOException $e) {
|
||||
error_log('[Steuerkram] PDO-Fehler in BookingController::store(): ' . $e->getMessage());
|
||||
$this->renderBelegDetail($belegId, ['_exception' => 'Datenbankfehler beim Speichern der Buchung.'], $data);
|
||||
} catch (\Exception $e) {
|
||||
$this->renderBelegDetail($belegId, ['_exception' => $e->getMessage()], $data);
|
||||
}
|
||||
|
|
@ -159,11 +166,22 @@ class BookingController
|
|||
Booking::reverse($id, $reason);
|
||||
$booking = Booking::find($id);
|
||||
View::redirect("/belege/{$booking['beleg_id']}", 'Buchung wurde storniert.');
|
||||
} catch (\PDOException $e) {
|
||||
error_log('[Steuerkram] PDO-Fehler in BookingController::reverse(): ' . $e->getMessage());
|
||||
View::redirect('/journal', 'Datenbankfehler beim Stornieren der Buchung.', 'error');
|
||||
} catch (\Exception $e) {
|
||||
View::redirect('/journal', $e->getMessage(), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
private function csvSanitize(string $value): string
|
||||
{
|
||||
if ($value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
|
||||
return "'" . $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function renderView(string $template, array $data): string
|
||||
{
|
||||
extract($data, EXTR_SKIP);
|
||||
|
|
|
|||
|
|
@ -157,6 +157,9 @@ class InvoiceController
|
|||
try {
|
||||
Invoice::update($id, $data);
|
||||
View::redirect("/invoices/{$id}", 'Rechnung gespeichert.');
|
||||
} catch (\PDOException $e) {
|
||||
error_log('[Steuerkram] PDO-Fehler in InvoiceController::update(): ' . $e->getMessage());
|
||||
View::redirect("/invoices/{$id}", 'Datenbankfehler beim Speichern der Rechnung.', 'error');
|
||||
} catch (\Exception $e) {
|
||||
View::redirect("/invoices/{$id}", $e->getMessage(), 'error');
|
||||
}
|
||||
|
|
@ -168,6 +171,9 @@ class InvoiceController
|
|||
try {
|
||||
$nr = Invoice::finalize($id);
|
||||
View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert.");
|
||||
} catch (\PDOException $e) {
|
||||
error_log('[Steuerkram] PDO-Fehler in InvoiceController::finalize(): ' . $e->getMessage());
|
||||
View::redirect("/invoices/{$id}", 'Datenbankfehler beim Finalisieren der Rechnung.', 'error');
|
||||
} catch (\Exception $e) {
|
||||
View::redirect("/invoices/{$id}", $e->getMessage(), 'error');
|
||||
}
|
||||
|
|
@ -270,6 +276,10 @@ class InvoiceController
|
|||
|
||||
\Steuerkram\Database::commit();
|
||||
View::redirect("/invoices/{$id}", 'Rechnung erfolgreich gebucht (Beleg ' . $belegNr . ').');
|
||||
} catch (\PDOException $e) {
|
||||
\Steuerkram\Database::rollBack();
|
||||
error_log('[Steuerkram] PDO-Fehler in InvoiceController::book(): ' . $e->getMessage());
|
||||
View::redirect("/invoices/{$id}", 'Datenbankfehler beim Buchen der Rechnung.', 'error');
|
||||
} catch (\Exception $e) {
|
||||
\Steuerkram\Database::rollBack();
|
||||
View::redirect("/invoices/{$id}", $e->getMessage(), 'error');
|
||||
|
|
|
|||
|
|
@ -64,12 +64,12 @@ class SettingsController
|
|||
$file = $_FILES['logo'];
|
||||
$mime = mime_content_type($file['tmp_name']);
|
||||
|
||||
if (!in_array($mime, ['image/png', 'image/jpeg', 'image/svg+xml'])) {
|
||||
View::redirect('/settings', 'Logo muss PNG, JPEG oder SVG sein.', 'error');
|
||||
if (!in_array($mime, ['image/png', 'image/jpeg'])) {
|
||||
View::redirect('/settings', 'Logo muss PNG oder JPEG sein.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
$ext = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/svg+xml' => 'svg'][$mime];
|
||||
$ext = ['image/png' => 'png', 'image/jpeg' => 'jpg'][$mime];
|
||||
$relPath = 'static/uploads/logo/logo.' . $ext;
|
||||
$absDir = dirname(__DIR__, 2) . '/public/static/uploads/logo';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ $testMode = Setting::isTestMode();
|
|||
?>
|
||||
|
||||
<!-- Beleg-Info-Bar -->
|
||||
<div class="card mb-2">
|
||||
<div class="card mb-2"
|
||||
id="beleg-info-bar"
|
||||
data-beleg-id="<?= $h($beleg['id']) ?>"
|
||||
data-belegtyp="<?= $h($beleg['belegtyp']) ?>"
|
||||
data-beleg-datum="<?= $h($beleg['beleg_datum'] ?? '') ?>"
|
||||
data-rechnungs-nr="<?= $h($beleg['rechnungs_nr'] ?? '') ?>"
|
||||
data-betrag-brutto="<?= $h($beleg['betrag_brutto'] ?? '') ?>">
|
||||
<div class="card-body" style="display:flex;gap:2rem;align-items:center;flex-wrap:wrap;padding:.75rem 1.25rem">
|
||||
<div>
|
||||
<span class="text-muted text-sm">Belegnr.</span><br>
|
||||
|
|
@ -18,16 +24,34 @@ $testMode = Setting::isTestMode();
|
|||
<span class="badge badge-gray"><?= $h($beleg['belegtyp']) ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted text-sm">Belegdatum</span><br>
|
||||
<span class="text-muted text-sm">Belegdatum<?= !$beleg['locked'] ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br>
|
||||
<?php if (!$beleg['locked']): ?>
|
||||
<span class="inline-edit-trigger"
|
||||
data-field="beleg_datum" data-type="date"
|
||||
data-raw="<?= $h($beleg['beleg_datum'] ?? '') ?>"
|
||||
title="Klicken zum Bearbeiten">
|
||||
<?= $beleg['beleg_datum'] ? View::formatDate($beleg['beleg_datum']) : '<span class="text-muted">– setzen –</span>' ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<?= View::formatDate($beleg['beleg_datum']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted text-sm">Eingangsdatum</span><br>
|
||||
<?= View::formatDate($beleg['eingangs_datum']) ?>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted text-sm">Rechnungsnr.</span><br>
|
||||
<span class="text-muted text-sm">Rechnungsnr.<?= !$beleg['locked'] ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br>
|
||||
<?php if (!$beleg['locked']): ?>
|
||||
<span class="inline-edit-trigger"
|
||||
data-field="rechnungs_nr" data-type="text"
|
||||
data-raw="<?= $h($beleg['rechnungs_nr'] ?? '') ?>"
|
||||
title="Klicken zum Bearbeiten">
|
||||
<?= $beleg['rechnungs_nr'] ? $h($beleg['rechnungs_nr']) : '<span class="text-muted">– setzen –</span>' ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<?= $beleg['rechnungs_nr'] ? $h($beleg['rechnungs_nr']) : '<span class="text-muted">–</span>' ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted text-sm">Brutto lt. Beleg</span><br>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,25 @@
|
|||
<form method="get" style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center">
|
||||
<input type="search" name="search" placeholder="Belegnr., Dateiname, Rechnungsnr.…"
|
||||
value="<?= $h($filter['search'] ?? '') ?>" style="width:240px">
|
||||
<select name="year">
|
||||
<select name="year" style="width:80px">
|
||||
<option value="">Alle Jahre</option>
|
||||
<?php for ($y = (int)date('Y'); $y >= 2020; $y--): ?>
|
||||
<option value="<?= $y ?>"<?= ($filter['year'] ?? '') == $y ? ' selected' : '' ?>><?= $y ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<select name="month">
|
||||
<option value="">Alle Monate</option>
|
||||
<select name="month" style="width:70px">
|
||||
<option value="">Alle</option>
|
||||
<?php foreach (['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'] as $i => $m): ?>
|
||||
<option value="<?= $i+1 ?>"<?= ($filter['month'] ?? '') == $i+1 ? ' selected' : '' ?>><?= $m ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="booked">
|
||||
<select name="belegtyp" style="width:130px">
|
||||
<option value="">Alle Belegarten</option>
|
||||
<?php foreach (['Eingangsrechnung','Ausgangsrechnung','Quittung','Kontoauszug','Sonstiges'] as $t): ?>
|
||||
<option value="<?= $t ?>"<?= ($filter['belegtyp'] ?? '') === $t ? ' selected' : '' ?>><?= $t ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="booked" style="width:90px">
|
||||
<option value="">Alle</option>
|
||||
<option value="0"<?= ($filter['booked'] ?? '') === '0' ? ' selected' : '' ?>>Ungebucht</option>
|
||||
<option value="1"<?= ($filter['booked'] ?? '') === '1' ? ' selected' : '' ?>>Gebucht</option>
|
||||
|
|
@ -27,7 +33,7 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<span class="text-muted text-sm"><?= $total ?> Belege</span>
|
||||
<span class="text-muted text-sm"><?= $total ?> von <?= $totalAll ?> Belegen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
<form method="get" style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center">
|
||||
<input type="search" name="search" placeholder="Buchungstext suchen…"
|
||||
value="<?= $h($filter['search'] ?? '') ?>" style="width:220px">
|
||||
<select name="year">
|
||||
<select name="year" style="width:80px">
|
||||
<option value="">Alle Jahre</option>
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= $y ?>"<?= ($filter['year'] ?? '') == $y ? ' selected' : '' ?>><?= $y ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="month">
|
||||
<option value="">Alle Monate</option>
|
||||
<select name="month" style="width:70px">
|
||||
<option value="">Alle</option>
|
||||
<?php foreach (['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'] as $i => $m): ?>
|
||||
<option value="<?= $i+1 ?>"<?= ($filter['month'] ?? '') == $i+1 ? ' selected' : '' ?>><?= $m ?></option>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<span class="text-muted text-sm"><?= $total ?> Buchungen</span>
|
||||
<span class="text-muted text-sm"><?= $total ?> von <?= $totalAll ?> Buchungen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= \Steuerkram\View::h($pageTitle ?? 'Steuerkram') ?> – Steuerkram</title>
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
<link rel="icon" type="image/png" href="/static/favicon/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/favicon/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="Steuerkram" />
|
||||
<link rel="manifest" href="/static/favicon/site.webmanifest" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ function navActive(string $prefix, string $current): string {
|
|||
}
|
||||
?>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">Steuer<span>kram</span></div>
|
||||
<div class="sidebar-brand"><img src="/static/favicon/web-app-manifest-512x512.png" alt="" style="height:1.1em;width:auto;vertical-align:middle;margin-right:.4em;" />Steuer<span>kram</span></div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-label">Übersicht</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue