Files
PackControl/pirp/public/index.php

423 lines
19 KiB
PHP

<?php
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/auth.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/recurring_functions.php';
require_once __DIR__ . '/../src/pdf_functions.php';
require_once __DIR__ . '/../src/journal_functions.php';
require_once __DIR__ . '/../src/icons.php';
require_login();
$pdo = get_db();
$year = date('Y');
$month = (int)date('m');
$prev_month = $month === 1 ? 12 : $month - 1;
$prev_month_year = $month === 1 ? (int)$year - 1 : (int)$year;
// === RECHNUNGEN/AUSGABEN ===
// Offene Rechnungen
$stmt = $pdo->prepare("SELECT COUNT(*) FROM invoices WHERE paid = FALSE");
$stmt->execute();
$open_invoices_count = (int)$stmt->fetchColumn();
$stmt = $pdo->prepare("SELECT COALESCE(SUM(total_gross),0) FROM invoices WHERE paid = FALSE");
$stmt->execute();
$open_invoices_sum = (float)$stmt->fetchColumn();
// Überfällige Rechnungen (> 14 Tage offen)
$stmt = $pdo->prepare("SELECT i.*, c.name AS customer_name FROM invoices i JOIN customers c ON c.id = i.customer_id WHERE i.paid = FALSE AND i.invoice_date < NOW() - INTERVAL '14 days' ORDER BY i.invoice_date ASC LIMIT 5");
$stmt->execute();
$overdue_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fällige Abo-Rechnungen
$pending_recurring = count_pending_recurring_invoices();
// Konsistenz-Check: Fehlende Buchungen
$consistency = check_journal_consistency();
$has_unbooked = ($consistency['unbooked_invoices'] > 0 || $consistency['unbooked_expenses'] > 0);
// GoBD PDF-Status
$gobd_status = check_pdf_integrity_status();
$gobd_has_problems = ($gobd_status['unarchived'] > 0 || $gobd_status['invalid'] > 0 || $gobd_status['missing_files'] > 0);
// Letzte 5 Rechnungen
$stmt = $pdo->prepare("SELECT i.*, c.name AS customer_name FROM invoices i JOIN customers c ON c.id = i.customer_id ORDER BY i.created_at DESC LIMIT 5");
$stmt->execute();
$recent_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
// === JOURNAL MODUL ===
$journal_year = get_journal_year_by_year((int)$year);
$journal_year_id = $journal_year ? (int)$journal_year['id'] : null;
$journal_erloese_monat = 0;
$journal_wareneingang_monat = 0;
$journal_gewinn_monat = 0;
$journal_erloese_jahr = 0;
$journal_wareneingang_jahr = 0;
$journal_gewinn_jahr = 0;
$journal_entries_month = 0;
$journal_gewinn_prev = 0;
if ($journal_year_id) {
$month_profit = calculate_yearly_profitability($journal_year_id);
if (isset($month_profit[$month])) {
$journal_erloese_monat = $month_profit[$month]['erloese'];
$journal_wareneingang_monat = $month_profit[$month]['wareneingang'];
$journal_gewinn_monat = $month_profit[$month]['gewinn'];
}
if (isset($month_profit[$prev_month])) {
$journal_gewinn_prev = $month_profit[$prev_month]['gewinn'];
}
foreach ($month_profit as $m => $p) {
$journal_erloese_jahr += $p['erloese'];
$journal_wareneingang_jahr += $p['wareneingang'];
$journal_gewinn_jahr += $p['gewinn'];
}
$stmt = $pdo->prepare("SELECT COUNT(*) FROM journal_entries WHERE year_id = :y AND month = :m");
$stmt->execute([':y' => $journal_year_id, ':m' => $month]);
$journal_entries_month = (int)$stmt->fetchColumn();
$mt = calculate_monthly_totals($journal_year_id, $month);
$journal_kasse_balance = ($mt['kasse_s'] ?? 0) - ($mt['kasse_h'] ?? 0);
$journal_bank_balance = ($mt['bank_s'] ?? 0) - ($mt['bank_h'] ?? 0);
} else {
$journal_kasse_balance = 0;
$journal_bank_balance = 0;
}
// Sparkline-Daten
$sparkline_erloese = [];
$sparkline_gewinn = [];
$sparkline_wareneingang = [];
if ($journal_year_id && !empty($month_profit)) {
for ($m = 1; $m <= $month; $m++) {
$sparkline_erloese[] = $month_profit[$m]['erloese'] ?? 0;
$sparkline_gewinn[] = $month_profit[$m]['gewinn'] ?? 0;
$sparkline_wareneingang[] = $month_profit[$m]['wareneingang'] ?? 0;
}
}
// Letzte 5 Journal-Einträge
$recent_journal = get_recent_journal_entries(5);
$month_names_full = [
1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April',
5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August',
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember',
];
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>PIRP Dashboard</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<header>
<h1>PIRP</h1>
<nav>
<a href="<?= url_for('index.php') ?>" class="active"><?= icon_dashboard() ?>Dashboard</a>
<a href="<?= url_for('invoices.php') ?>"><?= icon_invoices() ?>Rechnungen</a>
<a href="<?= url_for('customers.php') ?>"><?= icon_customers() ?>Kunden</a>
<a href="<?= url_for('expenses.php') ?>"><?= icon_expenses() ?>Ausgaben</a>
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
<a href="<?= url_for('journal.php') ?>"><?= icon_journal() ?>Journal</a>
<a href="<?= url_for('euer.php') ?>"><?= icon_euer() ?>EÜR</a>
<a href="<?= url_for('settings.php') ?>"><?= icon_settings() ?>Einstellungen</a>
<a href="<?= url_for('logout.php') ?>"><?= icon_logout() ?>Logout (<?= htmlspecialchars($_SESSION['username'] ?? '') ?>)</a>
<span class="cmd-k-hint" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'k',ctrlKey:true}))"><kbd>Ctrl+K</kbd></span>
</nav>
</header>
<main>
<?php if ($has_unbooked): ?>
<div class="gobd-warning">
<strong>Fehlende Journalbuchungen:</strong>
<?php if ($consistency['unbooked_invoices'] > 0): ?>
<?= $consistency['unbooked_invoices'] ?> bezahlte Rechnung(en)
<?php endif; ?>
<?php if ($consistency['unbooked_invoices'] > 0 && $consistency['unbooked_expenses'] > 0): ?> und <?php endif; ?>
<?php if ($consistency['unbooked_expenses'] > 0): ?>
<?= $consistency['unbooked_expenses'] ?> bezahlte Ausgabe(n)
<?php endif; ?>
ohne Journaleintrag.
<a href="<?= url_for('euer.php?year=' . $year . '&fix_missing=1') ?>">Jetzt nachholen</a>
</div>
<?php endif; ?>
<!-- Finanz-Übersicht -->
<h2><?= htmlspecialchars($year) ?> &middot; <?= $month_names_full[$month] ?></h2>
<?php if ($journal_year_id): ?>
<!-- Rechnungen / Allgemein -->
<div class="dashboard-grid" style="margin-bottom:6px;">
<div class="card">
<h3>Offen</h3>
<p class="big"><?= number_format($open_invoices_sum, 2, ',', '.') ?> &euro;</p>
<span style="font-size:10px;color:var(--text-dim);"><?= $open_invoices_count ?> Rechnungen</span>
</div>
<div class="card">
<h3>Abo-Rechnungen</h3>
<p class="big"><?= $pending_recurring ?> fällig</p>
<?php if ($pending_recurring > 0): ?>
<a href="<?= url_for('recurring_generate.php') ?>">Generieren</a>
<?php endif; ?>
</div>
<div class="card">
<h3>Erlöse (Jahr)</h3>
<p class="big"><?= number_format($journal_erloese_jahr, 2, ',', '.') ?> &euro;</p>
<?php if (count($sparkline_erloese) >= 2): ?>
<?= generate_sparkline_svg($sparkline_erloese, '#22c55e') ?>
<?php endif; ?>
<a href="<?= url_for('journal_summary.php?year_id=' . $journal_year_id) ?>" style="font-size:10px;">Jahresübersicht →</a>
</div>
<div class="card">
<h3>Gewinn (Jahr)</h3>
<p class="big" <?= $journal_gewinn_jahr < 0 ? 'style="color:var(--error);"' : '' ?>><?= number_format($journal_gewinn_jahr, 2, ',', '.') ?> &euro;</p>
<?php if (count($sparkline_gewinn) >= 2): ?>
<?= generate_sparkline_svg($sparkline_gewinn, '#d4882a') ?>
<?php endif; ?>
<a href="<?= url_for('euer.php?year=' . $year) ?>" style="font-size:10px;">EÜR →</a>
</div>
</div>
<!-- Journal Monat -->
<h2 style="margin-bottom:4px;">
Journal &middot; <?= $month_names_full[$month] ?>
<a href="<?= url_for('journal.php?year_id=' . $journal_year_id . '&month=' . $month) ?>"
style="font-size:10px;font-weight:400;text-transform:none;letter-spacing:0;color:var(--accent);margin-left:10px;">
<?= $journal_entries_month ?> Buchungen →
</a>
</h2>
<div class="dashboard-grid" style="margin-bottom:4px;">
<div class="card">
<h3>Erlöse</h3>
<p class="big"><?= number_format($journal_erloese_monat, 2, ',', '.') ?> &euro;</p>
<?php if ($journal_gewinn_prev != 0): ?>
<?php $gwdiff = $journal_gewinn_monat - $journal_gewinn_prev; ?>
<span style="font-size:10px;color:<?= $gwdiff >= 0 ? 'var(--success)' : 'var(--error)' ?>;">
<?= $gwdiff >= 0 ? '+' : '' ?><?= number_format($gwdiff, 2, ',', '.') ?> Gewinn gg. Vormonat
</span>
<?php endif; ?>
</div>
<div class="card">
<h3>Gewinn</h3>
<p class="big" <?= $journal_gewinn_monat < 0 ? 'style="color:var(--error);"' : '' ?>><?= number_format($journal_gewinn_monat, 2, ',', '.') ?> &euro;</p>
<?php if (count($sparkline_wareneingang) >= 2): ?>
<?= generate_sparkline_svg($sparkline_wareneingang, '#737373') ?>
<?php endif; ?>
</div>
<div class="card">
<h3>Kasse</h3>
<p class="big" style="color:<?= $journal_kasse_balance < 0 ? 'var(--error)' : 'var(--accent)' ?>;">
<?= number_format($journal_kasse_balance, 2, ',', '.') ?> &euro;
</p>
<a href="<?= url_for('journal.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" style="font-size:10px;">Journal →</a>
</div>
<div class="card">
<h3>Bank</h3>
<p class="big" style="color:<?= $journal_bank_balance < 0 ? 'var(--error)' : 'var(--warning)' ?>;">
<?= number_format($journal_bank_balance, 2, ',', '.') ?> &euro;
</p>
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" style="font-size:10px;">+ Neue Buchung</a>
</div>
</div>
<!-- Monatsvergleich -->
<?php if (!empty($month_profit)): ?>
<section style="margin-bottom:4px;">
<h2>Monatsvergleich <?= $year ?></h2>
<div style="padding:8px 0;">
<?= generate_monthly_bar_chart_svg($month_profit, $month) ?>
</div>
</section>
<?php endif; ?>
<?php else: ?>
<div class="dashboard-grid">
<div class="card">
<h3>Offen</h3>
<p class="big"><?= number_format($open_invoices_sum, 2, ',', '.') ?> &euro;</p>
<span style="font-size:10px;color:var(--text-dim);"><?= $open_invoices_count ?> Rechnungen</span>
</div>
<div class="card">
<h3>Abo-Rechnungen</h3>
<p class="big"><?= $pending_recurring ?> fällig</p>
<?php if ($pending_recurring > 0): ?>
<a href="<?= url_for('recurring_generate.php') ?>">Generieren</a>
<?php endif; ?>
</div>
</div>
<section>
<h2>Journal</h2>
<div>
<p>Kein Journal für <?= $year ?> angelegt.
<a href="<?= url_for('settings.php?tab=journal') ?>">Jahr erstellen</a>
</p>
</div>
</section>
<?php endif; ?>
<!-- Überfällige Rechnungen -->
<?php if ($overdue_invoices): ?>
<section>
<h2>Überfällige Rechnungen</h2>
<div>
<table class="list">
<thead>
<tr>
<th>Datum</th>
<th>Nr.</th>
<th>Kunde</th>
<th style="text-align:right;">Betrag</th>
<th>Tage offen</th>
</tr>
</thead>
<tbody>
<?php foreach ($overdue_invoices as $oi): ?>
<?php $days = (int)((time() - strtotime($oi['invoice_date'])) / 86400); ?>
<tr>
<td><?= date('d.m.Y', strtotime($oi['invoice_date'])) ?></td>
<td><a href="<?= url_for('invoice_pdf.php?id=' . $oi['id']) ?>" target="_blank"><?= htmlspecialchars($oi['invoice_number']) ?></a></td>
<td><?= htmlspecialchars($oi['customer_name']) ?></td>
<td style="text-align:right;"><?= number_format((float)$oi['total_gross'], 2, ',', '.') ?> &euro;</td>
<td style="color:var(--error);"><strong><?= $days ?> Tage</strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<?php endif; ?>
<!-- Zwei-Spalten: Letzte Rechnungen + Letzte Buchungen -->
<div class="dashboard-two-col">
<section>
<h2>Letzte Rechnungen</h2>
<div>
<table class="list">
<thead><tr><th>Datum</th><th>Kunde</th><th style="text-align:right;">Betrag</th><th>Status</th></tr></thead>
<tbody>
<?php foreach ($recent_invoices as $ri): ?>
<tr>
<td><?= date('d.m', strtotime($ri['invoice_date'])) ?></td>
<td><?= htmlspecialchars($ri['customer_name']) ?></td>
<td style="text-align:right;"><?= number_format((float)$ri['total_gross'], 2, ',', '.') ?></td>
<td><?= $ri['paid'] ? 'bezahlt' : 'offen' ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recent_invoices)): ?>
<tr><td colspan="4" style="color:var(--text-dim);">Keine Rechnungen</td></tr>
<?php endif; ?>
</tbody>
</table>
<a href="<?= url_for('invoices.php') ?>" style="font-size:10px;">Alle Rechnungen</a>
</div>
</section>
<section>
<h2>Letzte Buchungen</h2>
<div>
<table class="list">
<thead><tr><th>Datum</th><th>Text</th><th style="text-align:right;">Betrag</th></tr></thead>
<tbody>
<?php foreach ($recent_journal as $rj): ?>
<tr>
<td><?= date('d.m', strtotime($rj['entry_date'])) ?></td>
<td>
<a href="<?= url_for('journal_entry.php?id=' . $rj['id']) ?>" style="color:inherit;">
<?= htmlspecialchars(mb_strimwidth($rj['description'], 0, 40, '...')) ?>
</a>
</td>
<td style="text-align:right;font-family:var(--font-mono);font-size:12px;"><?= number_format((float)$rj['amount'], 2, ',', '.') ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recent_journal)): ?>
<tr><td colspan="3" style="color:var(--text-dim);">Keine Buchungen</td></tr>
<?php endif; ?>
</tbody>
</table>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;">
<a href="<?= url_for('journal.php') ?>" style="font-size:10px;">Alle Buchungen →</a>
<?php if ($journal_year_id): ?>
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" class="button" style="font-size:11px;padding:3px 10px;">+ Neue Buchung</a>
<?php endif; ?>
</div>
</div>
</section>
</div>
<!-- GoBD PDF-Status -->
<?php if ($gobd_has_problems): ?>
<div class="gobd-warning">
<h3>GoBD-Warnung: PDF-Archivierung unvollständig</h3>
<ul>
<?php if ($gobd_status['unarchived'] > 0): ?>
<li><strong><?= $gobd_status['unarchived'] ?></strong> Rechnung(en) ohne archivierte PDF</li>
<?php endif; ?>
<?php if ($gobd_status['missing_files'] > 0): ?>
<li style="color:var(--error);"><strong><?= $gobd_status['missing_files'] ?></strong> PDF-Datei(en) fehlen auf dem Server</li>
<?php endif; ?>
<?php if ($gobd_status['invalid'] > 0): ?>
<li style="color:var(--error);"><strong><?= $gobd_status['invalid'] ?></strong> PDF(s) mit fehlgeschlagener Integritätsprüfung</li>
<?php endif; ?>
</ul>
<?php if (!empty($gobd_status['problems'])): ?>
<details>
<summary>Betroffene Rechnungen anzeigen</summary>
<table class="list">
<thead><tr><th>Rechnungsnr.</th><th>Problem</th><th>Aktion</th></tr></thead>
<tbody>
<?php foreach (array_slice($gobd_status['problems'], 0, 10) as $prob): ?>
<tr>
<td><?= htmlspecialchars($prob['invoice_number']) ?></td>
<td><?= htmlspecialchars($prob['message']) ?></td>
<td><a href="<?= url_for('invoice_pdf.php?id=' . $prob['id']) ?>">PDF neu generieren</a></td>
</tr>
<?php endforeach; ?>
<?php if (count($gobd_status['problems']) > 10): ?>
<tr><td colspan="3"><em>... und <?= count($gobd_status['problems']) - 10 ?> weitere</em></td></tr>
<?php endif; ?>
</tbody>
</table>
</details>
<?php endif; ?>
<?php if (!empty($gobd_status['migration_needed'])): ?>
<p style="margin-top:4px;margin-bottom:0;">
<strong>Datenbank-Migration erforderlich:</strong><br>
1. <code>sudo -u postgres psql -d pirp -f /var/www/pirp/tools/migrate_pdf.sql</code><br>
2. <code>php /var/www/pirp/tools/run_migration.php</code>
</p>
<?php elseif ($gobd_status['unarchived'] > 0): ?>
<p style="margin-top:4px;margin-bottom:0;">
<strong>Migration:</strong> <code>php tools/run_migration.php</code>
</p>
<?php endif; ?>
</div>
<?php else: ?>
<div class="gobd-ok">
<strong>GoBD OK:</strong>
<?= $gobd_status['archived'] ?>/<?= $gobd_status['total_invoices'] ?> archiviert.
</div>
<?php endif; ?>
<section>
<h2>Schnellzugriff</h2>
<div class="flex-row" style="gap:8px;flex-wrap:wrap;">
<a href="<?= url_for('invoice_new.php') ?>" class="button">Neue Rechnung</a>
<a href="<?= url_for('expenses.php') ?>" class="button secondary">Neue Ausgabe</a>
<?php if ($journal_year_id): ?>
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" class="button secondary">Neue Buchung</a>
<?php endif; ?>
<a href="<?= url_for('euer.php') ?>" class="button secondary">EÜR</a>
</div>
</section>
</main>
<script src="assets/command-palette.js"></script>
</body>
</html>