Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style
This commit is contained in:
389
pirp/public/expenses.php
Normal file
389
pirp/public/expenses.php
Normal 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>
|
||||
Reference in New Issue
Block a user