Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style

This commit is contained in:
2026-02-27 22:47:08 +01:00
committed by erik
parent 5a70f775f1
commit 93faae5cc8
1463 changed files with 306917 additions and 0 deletions

248
pirp/public/invoices.php Normal file
View File

@@ -0,0 +1,248 @@
<?php
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/auth.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/invoice_functions.php';
require_once __DIR__ . '/../src/journal_functions.php';
require_once __DIR__ . '/../src/icons.php';
require_login();
$pdo = get_db();
$msg = '';
// Rechnung als bezahlt markieren + automatische Journalbuchung
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_paid_id'])) {
$id = (int)$_POST['mark_paid_id'];
$payment_date = $_POST['payment_date'] ?? date('Y-m-d');
$pdo->beginTransaction();
try {
// Bezahlt-Status und Zahlungsdatum setzen
$stmt = $pdo->prepare('UPDATE invoices SET paid = TRUE, payment_date = :pd WHERE id = :id');
$stmt->execute([':id' => $id, ':pd' => $payment_date]);
// Automatisch Journalbuchung erstellen
$existing = get_journal_entry_for_invoice($id);
if (!$existing) {
$entry_id = create_journal_entry_from_invoice($id);
$msg = 'Rechnung als bezahlt markiert. Journalbuchung #' . $entry_id . ' erstellt.';
} else {
$msg = 'Rechnung als bezahlt markiert (Journalbuchung existierte bereits).';
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
$msg = 'Fehler: ' . $e->getMessage();
}
header('Location: ' . url_for('invoices.php?msg=' . urlencode($msg)));
exit;
}
// Nachricht aus Redirect anzeigen
if (isset($_GET['msg'])) {
$msg = $_GET['msg'];
}
$filter_number = trim($_GET['number'] ?? '');
$filter_customer = trim($_GET['customer'] ?? '');
$filter_from = trim($_GET['from'] ?? '');
$filter_to = trim($_GET['to'] ?? '');
$where = "WHERE 1=1";
$params = [];
if ($filter_number !== '') {
$where .= " AND i.invoice_number ILIKE :num";
$params[':num'] = '%' . $filter_number . '%';
}
if ($filter_customer !== '') {
$where .= " AND c.name ILIKE :cust";
$params[':cust'] = '%' . $filter_customer . '%';
}
if ($filter_from !== '') {
$where .= " AND i.invoice_date >= :from";
$params[':from'] = $filter_from;
}
if ($filter_to !== '') {
$where .= " AND i.invoice_date <= :to";
$params[':to'] = $filter_to;
}
try {
$sql = "SELECT i.*, c.name AS customer_name, c.customer_number,
COALESCE(i.is_storno, FALSE) AS is_storno,
i.storno_of,
storno_child.invoice_number AS storno_child_number,
storno_child.id AS storno_child_id
FROM invoices i
JOIN customers c ON c.id = i.customer_id
LEFT JOIN invoices storno_child ON storno_child.storno_of = i.id
$where ORDER BY i.invoice_date DESC, i.id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
} catch (\PDOException $e) {
// is_storno/storno_of noch nicht migriert — Fallback
$sql = "SELECT i.*, c.name AS customer_name, c.customer_number,
FALSE AS is_storno, NULL AS storno_of,
NULL AS storno_child_number, NULL AS storno_child_id
FROM invoices i
JOIN customers c ON c.id = i.customer_id
$where ORDER BY i.invoice_date DESC, i.id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
}
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Verknüpfte Journal-Einträge laden (nur wenn Spalte existiert)
$journal_linked = [];
try {
$stmt_jl = $pdo->query("SELECT invoice_id, id FROM journal_entries WHERE invoice_id IS NOT NULL");
foreach ($stmt_jl->fetchAll(PDO::FETCH_ASSOC) as $jl) {
$journal_linked[(int)$jl['invoice_id']] = (int)$jl['id'];
}
} catch (PDOException $e) {
// Spalte invoice_id existiert noch nicht - ignorieren
}
// Mahnungen pro Rechnung laden
$mahnungen_count = [];
try {
$stmt_m = $pdo->query("SELECT invoice_id, COUNT(*) AS cnt, MAX(level) AS max_level FROM mahnungen GROUP BY invoice_id");
foreach ($stmt_m->fetchAll(PDO::FETCH_ASSOC) as $m) {
$mahnungen_count[(int)$m['invoice_id']] = ['cnt' => $m['cnt'], 'level' => $m['max_level']];
}
} catch (PDOException $e) {
// Tabelle noch nicht migriert
}
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Rechnungen</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<header>
<h1>PIRP</h1>
<nav>
<a href="<?= url_for('index.php') ?>"><?= icon_dashboard() ?>Dashboard</a>
<a href="<?= url_for('invoices.php') ?>" class="active"><?= 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>
<div class="module-subnav">
<a href="<?= url_for('invoices.php') ?>" class="active">Übersicht</a>
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
<a href="<?= url_for('recurring.php') ?>">Abo-Rechnungen</a>
</div>
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
<form method="get" class="filters">
<label>Rechnungsnummer:
<input type="text" name="number" value="<?= htmlspecialchars($filter_number) ?>">
</label>
<label>Kunde:
<input type="text" name="customer" value="<?= htmlspecialchars($filter_customer) ?>">
</label>
<label>Von:
<input type="date" name="from" value="<?= htmlspecialchars($filter_from) ?>">
</label>
<label>Bis:
<input type="date" name="to" value="<?= htmlspecialchars($filter_to) ?>">
</label>
<button type="submit">Filtern</button>
<a href="<?= url_for('invoices.php') ?>">Zurücksetzen</a>
<a href="<?= url_for('invoices_csv.php') . '?number=' . urlencode($filter_number) . '&customer=' . urlencode($filter_customer) . '&from=' . urlencode($filter_from) . '&to=' . urlencode($filter_to) ?>" class="button-secondary">Export CSV</a>
</form>
<table class="list">
<thead>
<tr>
<th>Datum</th>
<th>Nr.</th>
<th>Kunde</th>
<th>Betrag (brutto)</th>
<th>Status</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($invoices as $inv): ?>
<?php
$is_storno = !empty($inv['is_storno']);
$has_storno_child = !empty($inv['storno_child_id']);
$mahnung_info = $mahnungen_count[(int)$inv['id']] ?? null;
?>
<tr <?= $is_storno ? 'style="opacity:0.6;"' : '' ?>>
<td><?= htmlspecialchars(date('d.m.Y', strtotime($inv['invoice_date']))) ?></td>
<td>
<?= htmlspecialchars($inv['invoice_number']) ?>
<?php if ($is_storno): ?>
<span style="font-size:9px;color:var(--error);font-weight:600;margin-left:4px;">STORNO</span>
<?php endif; ?>
<?php if ($has_storno_child): ?>
<span style="font-size:9px;color:var(--text-muted);margin-left:4px;" title="Storniert durch <?= htmlspecialchars($inv['storno_child_number']) ?>">storniert</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($inv['customer_name']) ?></td>
<td style="text-align:right;"><?= number_format($inv['total_gross'], 2, ',', '.') ?> €</td>
<td>
<?= $inv['paid'] ? 'bezahlt' : 'offen' ?>
<?php if ($mahnung_info): ?>
<a href="<?= url_for('belegarchiv.php?invoice_id=' . $inv['id'] . '&type=mahnung') ?>"
style="font-size:9px;color:var(--warning);margin-left:4px;font-weight:600;">
M<?= $mahnung_info['level'] ?>
</a>
<?php endif; ?>
</td>
<td>
<a href="<?= url_for('invoice_pdf.php?id=' . $inv['id']) ?>" target="_blank">PDF</a>
<?php if (!$inv['paid']): ?>
| <a href="#" onclick="document.getElementById('pay-form-<?= $inv['id'] ?>').style.display='block'; return false;">bezahlt</a>
| <a href="<?= url_for('mahnung_new.php?invoice_id=' . $inv['id']) ?>">Mahnung</a>
<?php endif; ?>
<?php if ($inv['paid'] && isset($journal_linked[$inv['id']])): ?>
| <a href="<?= url_for('journal_entry.php?id=' . $journal_linked[$inv['id']]) ?>">Journal</a>
<?php endif; ?>
<?php if (!$is_storno && !$has_storno_child): ?>
| <a href="<?= url_for('invoice_storno.php?id=' . $inv['id']) ?>" style="color:var(--error);">Storno</a>
<?php endif; ?>
</td>
</tr>
<?php if (!$inv['paid']): ?>
<tr id="pay-form-<?= $inv['id'] ?>" style="display:none;" class="payment-form-row">
<td colspan="6">
<form method="post" style="display:inline-flex; gap:8px; align-items:center; padding:4px 0;">
<input type="hidden" name="mark_paid_id" value="<?= $inv['id'] ?>">
<label style="margin:0;">Zahlungsdatum:
<input type="date" name="payment_date" value="<?= date('Y-m-d') ?>" required>
</label>
<button type="submit" onclick="return confirm('Rechnung als bezahlt markieren und Journalbuchung erstellen?');">Bezahlt + Journal buchen</button>
<a href="#" onclick="this.closest('tr').style.display='none'; return false;">Abbrechen</a>
</form>
</td>
</tr>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php if (empty($invoices)): ?>
<tr><td colspan="6">Keine Rechnungen gefunden.</td></tr>
<?php endif; ?>
</tbody>
</table>
</main>
<script src="assets/command-palette.js"></script>
</body>
</html>