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

389
pirp/public/expenses.php Normal file
View File

@@ -0,0 +1,389 @@
<?php
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/auth.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/expense_functions.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();
$settings = get_settings();
$vat_mode = $settings['vat_mode'] ?? 'klein';
$msg = '';
$error = '';
$action = $_GET['action'] ?? '';
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// Aufwandskategorien für Dropdown laden
$expense_categories = get_journal_expense_categories(true);
// Ausgabe als bezahlt markieren (eigene Aktion)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_paid_id'])) {
$pay_id = (int)$_POST['mark_paid_id'];
$payment_date = $_POST['payment_date'] ?? date('Y-m-d');
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare('UPDATE expenses SET paid = TRUE, payment_date = :pd WHERE id = :id');
$stmt->execute([':id' => $pay_id, ':pd' => $payment_date]);
// Journal-Eintrag erstellen
$existing = get_journal_entry_for_expense($pay_id);
if (!$existing) {
$entry_id = create_journal_entry_from_expense($pay_id);
$msg = 'Ausgabe als bezahlt markiert. Journalbuchung #' . $entry_id . ' erstellt.';
} else {
$msg = 'Ausgabe als bezahlt markiert (Journalbuchung existierte bereits).';
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
$error = 'Fehler: ' . $e->getMessage();
}
}
// Normale Ausgabe speichern (neu oder Update)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['expense_date']) && !isset($_POST['mark_paid_id'])) {
$idPost = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
$data = [
'expense_date' => $_POST['expense_date'] ?? '',
'description' => $_POST['description'] ?? '',
'category' => $_POST['category'] ?? '',
'amount' => (float)($_POST['amount'] ?? 0),
'vat_rate' => (float)($_POST['vat_rate'] ?? 0),
'expense_category_id' => !empty($_POST['expense_category_id']) ? (int)$_POST['expense_category_id'] : null,
'paid' => !empty($_POST['paid']) ? 1 : 0,
'payment_date' => $_POST['payment_date'] ?? '',
];
if (!$data['expense_date'] || !$data['description'] || $data['amount'] <= 0) {
$error = 'Datum, Beschreibung und Betrag sind Pflichtfelder.';
} elseif (!empty($data['paid']) && empty($data['expense_category_id'])) {
$error = 'Bei bezahlten Ausgaben ist eine Aufwandskategorie für die Journalbuchung Pflicht.';
} else {
$pdo->beginTransaction();
try {
// War vorher schon bezahlt? (für Update-Check)
$was_paid = false;
$had_journal = false;
if ($idPost) {
$old_exp = get_expense($idPost);
$was_paid = $old_exp && $old_exp['paid'];
$had_journal = $old_exp ? (bool)get_journal_entry_for_expense($idPost) : false;
}
// Ausgabe speichern
$expense_id = save_expense($idPost, $data);
// Datei-Upload (PDF) verarbeiten
if (!empty($_FILES['attachment']['tmp_name'])) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['attachment']['tmp_name']);
finfo_close($finfo);
if ($mime === 'application/pdf') {
$uploadDir = __DIR__ . '/uploads/expenses';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$targetFile = $uploadDir . '/expense_' . $expense_id . '.pdf';
if (move_uploaded_file($_FILES['attachment']['tmp_name'], $targetFile)) {
$relPath = 'uploads/expenses/expense_' . $expense_id . '.pdf';
$stmt = $pdo->prepare("UPDATE expenses SET attachment_path = :p WHERE id = :id");
$stmt->execute([':p' => $relPath, ':id' => $expense_id]);
} else {
$error = 'Ausgabe gespeichert, aber Datei konnte nicht verschoben werden.';
}
} else {
$error = 'Bitte nur PDF-Dateien als Beleg hochladen.';
}
}
// Journal-Integration
if (!empty($data['paid']) && !$had_journal) {
// Neu bezahlt → Journal-Eintrag erstellen
$existing = get_journal_entry_for_expense($expense_id);
if (!$existing) {
$entry_id = create_journal_entry_from_expense($expense_id);
$msg = 'Ausgabe gespeichert. Journalbuchung #' . $entry_id . ' erstellt.';
} else {
$msg = 'Ausgabe gespeichert.';
}
} elseif (empty($data['paid']) && $had_journal) {
// War bezahlt, jetzt offen → Journal-Eintrag entfernen
delete_journal_entry_for_expense($expense_id);
$msg = 'Ausgabe gespeichert. Journalbuchung entfernt (Status: offen).';
} else {
$msg = $msg ?: 'Ausgabe gespeichert.';
}
$pdo->commit();
$action = '';
$id = null;
} catch (Exception $e) {
$pdo->rollBack();
$error = 'Fehler: ' . $e->getMessage();
}
}
}
if ($action === 'delete' && $id) {
$exp = get_expense($id);
if ($exp) {
// Verknüpften Journal-Eintrag löschen
delete_journal_entry_for_expense($id);
if (!empty($exp['attachment_path'])) {
$fsPath = __DIR__ . '/' . $exp['attachment_path'];
if (is_file($fsPath)) {
@unlink($fsPath);
}
}
delete_expense($id);
$msg = 'Ausgabe und zugehörige Journalbuchung gelöscht.';
}
$action = '';
$id = null;
}
$editExpense = null;
if ($action === 'edit' && $id) {
$editExpense = get_expense($id);
}
$filters = [
'from' => $_GET['from'] ?? '',
'to' => $_GET['to'] ?? '',
'paid' => $_GET['paid'] ?? '',
'search' => $_GET['search'] ?? '',
];
$expenses = get_expenses($filters);
// Journal-Verknüpfungen laden
$journal_linked_expenses = [];
try {
$stmt_jl = $pdo->query("SELECT expense_id, id FROM journal_entries WHERE expense_id IS NOT NULL");
foreach ($stmt_jl->fetchAll(PDO::FETCH_ASSOC) as $jl) {
$journal_linked_expenses[(int)$jl['expense_id']] = (int)$jl['id'];
}
} catch (PDOException $e) {
// Spalte existiert noch nicht
}
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Ausgaben</title>
<link rel="stylesheet" href="assets/style.css">
<script>
function updateVatCalc() {
const amount = parseFloat(document.getElementById('expense-amount').value) || 0;
const vatRate = parseFloat(document.getElementById('expense-vat-rate').value) || 0;
const infoEl = document.getElementById('vat-info');
if (vatRate > 0 && amount > 0) {
const net = amount / (1 + vatRate / 100);
const vat = amount - net;
infoEl.textContent = 'Netto: ' + net.toFixed(2).replace('.', ',') + ' € | VorSt: ' + vat.toFixed(2).replace('.', ',') + ' €';
infoEl.style.display = 'block';
} else {
infoEl.style.display = 'none';
}
}
</script>
</head>
<body>
<header>
<h1>PIRP</h1>
<nav>
<a href="<?= url_for('index.php') ?>"><?= 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') ?>" class="active"><?= 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 ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
<section>
<h2><?= $editExpense ? 'Ausgabe bearbeiten' : 'Neue Ausgabe' ?></h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="id" value="<?= htmlspecialchars($editExpense['id'] ?? '') ?>">
<div class="flex-row">
<label>Datum:
<input type="date" name="expense_date" value="<?= htmlspecialchars($editExpense['expense_date'] ?? date('Y-m-d')) ?>" required>
</label>
<label>Betrag (brutto):
<input type="number" step="0.01" name="amount" id="expense-amount"
value="<?= htmlspecialchars($editExpense['amount'] ?? '0.00') ?>"
required oninput="updateVatCalc()">
</label>
<?php if ($vat_mode === 'normal'): ?>
<label>MwSt-Satz:
<select name="vat_rate" id="expense-vat-rate" onchange="updateVatCalc()">
<option value="0" <?= (isset($editExpense['vat_rate']) && (float)$editExpense['vat_rate'] == 0) ? 'selected' : '' ?>>0% (keine MwSt)</option>
<option value="7" <?= (isset($editExpense['vat_rate']) && (float)$editExpense['vat_rate'] == 7) ? 'selected' : '' ?>>7%</option>
<option value="19" <?= (isset($editExpense['vat_rate']) && (float)$editExpense['vat_rate'] == 19) ? 'selected' : (!isset($editExpense) ? 'selected' : '') ?>>19%</option>
</select>
</label>
<?php else: ?>
<input type="hidden" name="vat_rate" value="0">
<?php endif; ?>
</div>
<div id="vat-info" style="display:none; font-size:11px; color:var(--text-dim); margin:-8px 0 8px;"></div>
<label>Beschreibung:
<input type="text" name="description" value="<?= htmlspecialchars($editExpense['description'] ?? '') ?>" required>
</label>
<div class="flex-row">
<label>Aufwandskategorie:
<select name="expense_category_id">
<option value="">-- keine --</option>
<?php foreach ($expense_categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= (isset($editExpense['expense_category_id']) && (int)$editExpense['expense_category_id'] === (int)$cat['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>Notiz / Kategorie:
<input type="text" name="category" value="<?= htmlspecialchars($editExpense['category'] ?? '') ?>" placeholder="Freitext-Notiz">
</label>
</div>
<div class="flex-row">
<label>
<input type="checkbox" name="paid" value="1" <?= (isset($editExpense['paid']) ? $editExpense['paid'] : 1) ? 'checked' : '' ?>>
bezahlt
</label>
<label>Zahlungsdatum:
<input type="date" name="payment_date" value="<?= htmlspecialchars($editExpense['payment_date'] ?? date('Y-m-d')) ?>">
</label>
</div>
<label>Beleg (PDF):
<input type="file" name="attachment" accept="application/pdf">
<?php if (!empty($editExpense['attachment_path'])): ?>
<br>Aktueller Beleg:
<a href="<?= url_for('expense_file.php?id=' . $editExpense['id']) ?>" target="_blank">PDF öffnen</a>
<?php endif; ?>
</label>
<button type="submit">Speichern</button>
<?php if ($editExpense): ?>
<a href="<?= url_for('expenses.php') ?>">Abbrechen</a>
<?php endif; ?>
</form>
</section>
<section>
<h2>Übersicht Ausgaben</h2>
<form method="get" class="filters">
<label>Von:
<input type="date" name="from" value="<?= htmlspecialchars($filters['from']) ?>">
</label>
<label>Bis:
<input type="date" name="to" value="<?= htmlspecialchars($filters['to']) ?>">
</label>
<label>Status:
<select name="paid">
<option value="">alle</option>
<option value="1" <?= $filters['paid']==='1' ? 'selected' : '' ?>>bezahlt</option>
<option value="0" <?= $filters['paid']==='0' ? 'selected' : '' ?>>offen</option>
</select>
</label>
<label>Suche:
<input type="text" name="search" value="<?= htmlspecialchars($filters['search']) ?>">
</label>
<button type="submit">Filtern</button>
<a href="<?= url_for('expenses.php') ?>">Zurücksetzen</a>
</form>
<table class="list">
<thead>
<tr>
<th>Datum</th>
<th>Beschreibung</th>
<th>Kategorie</th>
<th>Betrag</th>
<th>Status</th>
<th>Beleg</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php $total = 0.0; ?>
<?php foreach ($expenses as $e): ?>
<?php $total += $e['amount']; ?>
<tr>
<td><?= htmlspecialchars(date('d.m.Y', strtotime($e['expense_date']))) ?></td>
<td><?= htmlspecialchars($e['description']) ?></td>
<td><?= htmlspecialchars($e['category']) ?></td>
<td style="text-align:right;"><?= number_format($e['amount'], 2, ',', '.') ?> €</td>
<td><?= $e['paid'] ? 'bezahlt' : 'offen' ?></td>
<td>
<?php if (!empty($e['attachment_path'])): ?>
<a href="<?= url_for('expense_file.php?id=' . $e['id']) ?>" target="_blank">PDF</a>
<?php else: ?>
-
<?php endif; ?>
</td>
<td>
<a href="<?= url_for('expenses.php?action=edit&id=' . $e['id']) ?>">Bearbeiten</a>
| <a href="<?= url_for('expenses.php?action=delete&id=' . $e['id']) ?>" onclick="return confirm('Ausgabe wirklich löschen?');">Löschen</a>
<?php if (!$e['paid']): ?>
| <a href="#" onclick="document.getElementById('pay-exp-<?= $e['id'] ?>').style.display='table-row'; return false;">als bezahlt</a>
<?php endif; ?>
<?php if (isset($journal_linked_expenses[$e['id']])): ?>
| <a href="<?= url_for('journal_entry.php?id=' . $journal_linked_expenses[$e['id']]) ?>">Journal</a>
<?php endif; ?>
</td>
</tr>
<?php if (!$e['paid']): ?>
<tr id="pay-exp-<?= $e['id'] ?>" style="display:none;" class="payment-form-row">
<td colspan="7">
<form method="post" style="display:inline-flex; gap:8px; align-items:center; padding:4px 0;">
<input type="hidden" name="mark_paid_id" value="<?= $e['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('Ausgabe als bezahlt markieren und Journal buchen?');">Bezahlt + Journal buchen</button>
<a href="#" onclick="this.closest('tr').style.display='none'; return false;">Abbrechen</a>
</form>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
<?php if (empty($expenses)): ?>
<tr><td colspan="7">Keine Ausgaben gefunden.</td></tr>
<?php else: ?>
<tr>
<td colspan="3"><strong>Summe</strong></td>
<td style="text-align:right;"><strong><?= number_format($total, 2, ',', '.') ?> €</strong></td>
<td colspan="3"></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</section>
</main>
<script>
// MwSt-Info beim Laden aktualisieren (bei Edit)
document.addEventListener('DOMContentLoaded', updateVatCalc);
</script>
<script src="assets/command-palette.js"></script>
</body>
</html>