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