Feature: Konto-Journal im Kontenrahmen

This commit is contained in:
fds2610-bot 2026-05-10 15:45:45 +02:00
parent 55c02b54de
commit 61bce80012
4 changed files with 180 additions and 0 deletions

View file

@ -44,6 +44,7 @@ $router->get('/dashboard', fn($p) => (new AccountController())->dashboard($p));
$router->get('/accounts', fn($p) => (new AccountController())->index($p));
$router->get('/accounts/new', fn($p) => (new AccountController())->create($p));
$router->post('/accounts', fn($p) => (new AccountController())->store($p));
$router->get('/accounts/{id}/journal', fn($p) => (new AccountController())->journal($p));
$router->get('/accounts/{id}/edit', fn($p) => (new AccountController())->edit($p));
$router->post('/accounts/{id}', fn($p) => (new AccountController())->update($p));
$router->post('/accounts/{id}/delete', fn($p) => (new AccountController())->destroy($p));

View file

@ -198,6 +198,60 @@ class AccountController
View::redirect($back, 'Konto gelöscht.');
}
public function journal(array $params): void
{
$id = (int)$params['id'];
$account = Account::find($id);
if (!$account) { http_response_code(404); return; }
$availableYears = \Steuerkram\Database::fetchAll(
'SELECT year FROM fiscal_years ORDER BY year DESC'
);
$years = array_column($availableYears, 'year');
if (empty($years)) {
$years = [(int)date('Y')];
}
$year = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
if (!in_array($year, $years, true)) {
$year = $years[0];
}
// Buchungen wo dieses Konto Kategorie-Konto ist (alle, auch Kindzeilen)
// ODER Zahlungs-Konto ist (nur Hauptbuchungen, Kindzeilen gehören nicht ans Zahlungskonto)
$bookings = \Steuerkram\Database::fetchAll(
'SELECT bk.*,
k.kontonummer AS kategorie_nr, k.bezeichnung AS kategorie_name,
z.kontonummer AS zahlungs_nr, z.bezeichnung AS zahlungs_name,
bl.beleg_nr
FROM bookings bk
JOIN accounts k ON bk.kategorie_konto_id = k.id
JOIN accounts z ON bk.zahlungskonto_id = z.id
LEFT JOIN belege bl ON bk.beleg_id = bl.id
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
WHERE fy.year = ?
AND (
bk.kategorie_konto_id = ?
OR (bk.zahlungskonto_id = ? AND bk.parent_booking_id IS NULL
AND bk.kategorie_konto_id != ?)
)
ORDER BY bk.buchungs_datum, bk.id',
[$year, $id, $id, $id]
);
$view = new View();
$view->render('layout/base.php', [
'pageTitle' => 'Konto ' . $account['kontonummer'] . ' – Journal',
'content' => $this->renderView('accounts/journal', [
'account' => $account,
'bookings' => $bookings,
'year' => $year,
'years' => $years,
'accountId' => $id,
]),
]);
}
public function search(array $params): void
{
$q = trim($_GET['q'] ?? '');

View file

@ -0,0 +1,124 @@
<?php
use Steuerkram\View;
$h = fn($v) => View::h($v);
$fmt = fn($v) => number_format(abs((float)$v), 2, ',', '.');
// Jahr-Navigation
$prevYear = $year - 1;
$nextYear = $year + 1;
$hasPrev = in_array($prevYear, $years, true);
$hasNext = in_array($nextYear, $years, true);
?>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">
<?= $h($account['kontonummer']) ?> – <?= $h($account['bezeichnung']) ?>
</div>
<div class="text-muted text-sm"><?= $h($account['kontenart']) ?></div>
</div>
<div style="display:flex;gap:.5rem;align-items:center">
<?php if ($hasPrev): ?>
<a href="/accounts/<?= $accountId ?>/journal?year=<?= $prevYear ?>" class="btn btn-secondary btn-sm">← <?= $prevYear ?></a>
<?php else: ?>
<button class="btn btn-secondary btn-sm" disabled>←</button>
<?php endif; ?>
<span class="font-bold" style="min-width:4rem;text-align:center"><?= $year ?></span>
<?php if ($hasNext): ?>
<a href="/accounts/<?= $accountId ?>/journal?year=<?= $nextYear ?>" class="btn btn-secondary btn-sm"><?= $nextYear ?> →</a>
<?php else: ?>
<button class="btn btn-secondary btn-sm" disabled>→</button>
<?php endif; ?>
<a href="/accounts" class="btn btn-secondary btn-sm">← Kontenrahmen</a>
</div>
</div>
<?php if (empty($bookings)): ?>
<div class="card-body">
<p class="text-muted">Keine Buchungen für dieses Konto in <?= $year ?>.</p>
</div>
<?php else: ?>
<?php
$saldo = 0.0;
foreach ($bookings as $b) {
$isKategorie = (int)$b['kategorie_konto_id'] === $accountId;
if ($isKategorie) {
$wert = (float)$b['betrag_netto'];
} else {
// Zahlungs-Seite: Brutto-Betrag, Ausgaben fließen ab
$wert = (float)$b['betrag_brutto'];
if ($b['buchungstyp'] === 'Ausgabe') {
$wert = -$wert;
}
}
$saldo += $wert;
}
?>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Datum</th>
<th>Nummer</th>
<th>Beleg</th>
<th>Typ</th>
<th>Gegenkonto</th>
<th>Text</th>
<th class="td-right">Wert</th>
</tr>
</thead>
<tbody>
<?php
$laufendSaldo = 0.0;
foreach ($bookings as $b):
$isKategorie = (int)$b['kategorie_konto_id'] === $accountId;
if ($isKategorie) {
$wert = (float)$b['betrag_netto'];
$gegenkonto = $b['zahlungs_nr'] . ' ' . $b['zahlungs_name'];
} else {
$wert = (float)$b['betrag_brutto'];
if ($b['buchungstyp'] === 'Ausgabe') {
$wert = -$wert;
}
$gegenkonto = $b['kategorie_nr'] . ' ' . $b['kategorie_name'];
}
$laufendSaldo += $wert;
$isReversal = (bool)$b['is_reversal'];
?>
<tr<?= $isReversal ? ' style="font-style:italic;color:#888"' : '' ?>>
<td class="td-mono text-sm"><?= $h($b['buchungs_datum']) ?></td>
<td class="td-mono text-sm"><?= $h($b['buchungs_nr']) ?></td>
<td>
<?php if ($b['beleg_id']): ?>
<a href="/belege/<?= (int)$b['beleg_id'] ?>"><?= $h($b['beleg_nr'] ?? '#' . $b['beleg_id']) ?></a>
<?php else: ?>
<span class="text-muted">–</span>
<?php endif; ?>
</td>
<td>
<span class="badge <?= $b['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>">
<?= $h($b['buchungstyp']) ?>
</span>
</td>
<td class="td-mono text-sm"><?= $h($gegenkonto) ?></td>
<td><?= $h($b['buchungstext']) ?></td>
<td class="td-right td-mono<?= $wert < 0 ? ' text-danger' : '' ?>">
<?= $wert < 0 ? '−' : '' ?><?= $fmt($wert) ?> €
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;border-top:2px solid var(--border-color,#e5e7eb)">
<td colspan="6">Kontostand <?= $year ?> (<?= count($bookings) ?> Buchungen)</td>
<td class="td-right td-mono<?= $saldo < 0 ? ' text-danger' : '' ?>">
<?= $saldo < 0 ? '−' : '' ?><?= $fmt($saldo) ?> €
</td>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
</div>

View file

@ -61,6 +61,7 @@
<td class="td-muted text-sm"><?= $h(mb_strimwidth($acc['beschreibung'] ?? '', 0, 60, '…')) ?></td>
<td>
<div style="display:flex;gap:.5rem">
<a href="/accounts/<?= $h($acc['id']) ?>/journal" class="btn btn-secondary btn-sm">Journal</a>
<a href="/accounts/<?= $h($acc['id']) ?>/edit" class="btn btn-secondary btn-sm">Bearbeiten</a>
<form method="post" action="/accounts/<?= $h($acc['id']) ?>/delete"
data-confirm="Konto '<?= $h($acc['kontonummer']) ?>' wirklich löschen?">