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

View File

@@ -0,0 +1,311 @@
<?php
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/auth.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/journal_functions.php';
require_once __DIR__ . '/../src/icons.php';
require_login();
$msg = '';
$error = '';
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : null;
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('n');
$save_and_new = false;
// Duplizieren
if (isset($_GET['duplicate'])) {
$source_id = (int)$_GET['duplicate'];
try {
$new_id = duplicate_journal_entry($source_id);
header('Location: ' . url_for('journal_entry.php?id=' . $new_id));
exit;
} catch (\Exception $e) {
$error = 'Fehler beim Duplizieren: ' . $e->getMessage();
}
}
$entry = null;
if ($id) {
$entry = get_journal_entry($id);
if ($entry) {
$year_id = $entry['year_id'];
$month = $entry['month'];
}
}
// POST: Speichern
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$year_id = (int)$_POST['year_id'];
$save_and_new = isset($_POST['save_and_new']);
$id_post = !empty($_POST['id']) ? (int)$_POST['id'] : null;
$data = [
'year_id' => $year_id,
'entry_date' => $_POST['entry_date'] ?? '',
'description' => $_POST['description'] ?? '',
'attachment_note' => $_POST['attachment_note'] ?? '',
'amount' => $_POST['amount'] ?? 0,
'supplier_id' => $_POST['supplier_id'] ?? null,
'sort_order' => $_POST['sort_order'] ?? 0,
];
if (!$data['entry_date'] || !$data['description']) {
$error = 'Datum und Text sind Pflichtfelder.';
} else {
// Kontenverteilung aus POST parsen
$accounts = [];
$acct_types = $_POST['acct_type'] ?? [];
$acct_sides = $_POST['acct_side'] ?? [];
$acct_amounts = $_POST['acct_amount'] ?? [];
$acct_rev_ids = $_POST['acct_rev_id'] ?? [];
$acct_exp_ids = $_POST['acct_exp_id'] ?? [];
$acct_notes = $_POST['acct_note'] ?? [];
for ($i = 0; $i < count($acct_types); $i++) {
if (empty($acct_types[$i]) || (float)($acct_amounts[$i] ?? 0) == 0) continue;
$accounts[] = [
'account_type' => $acct_types[$i],
'side' => $acct_sides[$i] ?? 'soll',
'amount' => (float)($acct_amounts[$i] ?? 0),
'revenue_category_id' => !empty($acct_rev_ids[$i]) ? (int)$acct_rev_ids[$i] : null,
'expense_category_id' => !empty($acct_exp_ids[$i]) ? (int)$acct_exp_ids[$i] : null,
'note' => $acct_notes[$i] ?? '',
];
}
if (empty($accounts)) {
$error = 'Mindestens eine Kontenbuchung ist erforderlich.';
} else {
try {
$saved_id = save_journal_entry($id_post, $data, $accounts);
if ($save_and_new) {
$month = (int)date('n', strtotime($data['entry_date']));
header('Location: ' . url_for('journal_entry.php?year_id=' . $year_id . '&month=' . $month . '&msg=gespeichert'));
exit;
} else {
header('Location: ' . url_for('journal.php?year_id=' . $year_id . '&month=' . (int)date('n', strtotime($data['entry_date']))));
exit;
}
} catch (\Exception $e) {
$error = 'Fehler beim Speichern: ' . $e->getMessage();
}
}
}
}
if (isset($_GET['msg'])) {
$msg = 'Buchung gespeichert.';
}
// Daten laden
$years = get_journal_years();
$account_options = get_journal_account_options();
// Wenn kein year_id, das erste offene Jahr nehmen
if (!$year_id && $years) {
foreach ($years as $y) {
if (!$y['is_closed']) {
$year_id = $y['id'];
break;
}
}
if (!$year_id) $year_id = $years[0]['id'];
}
$current_year = null;
foreach ($years as $y) {
if ($y['id'] == $year_id) {
$current_year = $y;
break;
}
}
// Konto-Optionen als JSON für JavaScript
$acct_options_json = json_encode($account_options);
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Buchung <?= $entry ? 'bearbeiten' : 'erstellen' ?></title>
<link rel="stylesheet" href="assets/style.css">
<script src="assets/combobox.js"></script>
<script>
const accountOptions = <?= $acct_options_json ?>;
function addAccountRow(type, side, amount, revId, expId, note) {
type = type || '';
side = side || 'soll';
amount = amount || '';
revId = revId || '';
expId = expId || '';
note = note || '';
const tbody = document.getElementById('accounts-body');
const tr = document.createElement('tr');
tr.innerHTML =
'<td><div class="pirp-combobox-wrap"></div>' +
'<input type="hidden" name="acct_type[]" class="acct-type-hidden" value="' + type + '">' +
'<input type="hidden" name="acct_side[]" class="acct-side-hidden" value="' + side + '">' +
'<input type="hidden" name="acct_rev_id[]" class="acct-rev-hidden" value="' + revId + '">' +
'<input type="hidden" name="acct_exp_id[]" class="acct-exp-hidden" value="' + expId + '">' +
'</td>' +
'<td class="acct-side-display">' + (side === 'haben' ? 'H' : (type ? 'S' : '')) + '</td>' +
'<td><input type="number" step="0.01" name="acct_amount[]" value="' + amount + '" style="max-width:120px;"></td>' +
'<td><input type="text" name="acct_note[]" value="' + note + '" style="max-width:150px;"></td>' +
'<td><button type="button" class="danger" style="padding:2px 8px;font-size:10px;" onclick="this.closest(\'tr\').remove();">X</button></td>';
tbody.appendChild(tr);
const wrap = tr.querySelector('.pirp-combobox-wrap');
const selectedValue = type ? (type + '|' + side + '|' + revId + '|' + expId) : '';
new PirpCombobox(wrap, accountOptions, {
placeholder: '-- Konto wählen --',
selectedValue: selectedValue,
onSelect: function(opt) {
tr.querySelector('.acct-type-hidden').value = opt.value;
tr.querySelector('.acct-side-hidden').value = opt.side;
tr.querySelector('.acct-rev-hidden').value = opt.rev_id;
tr.querySelector('.acct-exp-hidden').value = opt.exp_id;
tr.querySelector('.acct-side-display').textContent = opt.side === 'soll' ? 'S' : 'H';
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
form.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA' && e.target.type !== 'submit') {
e.preventDefault();
const inputs = Array.from(form.querySelectorAll('input:not([type="hidden"]), select, textarea, button[type="submit"]'));
const currentIndex = inputs.indexOf(e.target);
if (currentIndex > -1 && currentIndex < inputs.length - 1) {
inputs[currentIndex + 1].focus();
}
}
});
});
</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') ?>"><?= icon_expenses() ?>Ausgaben</a>
<a href="<?= url_for('belegarchiv.php') ?>"><?= icon_archive() ?>Belege</a>
<a href="<?= url_for('journal.php') ?>" class="active"><?= 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="journal-subnav">
<a href="<?= url_for('journal.php') ?>">Monatsansicht</a>
<a href="<?= url_for('journal_summary.php') ?>">Jahressummen</a>
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
</div>
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
<?php if (empty($years)): ?>
<p class="warning">Bitte zuerst ein <a href="<?= url_for('settings.php?tab=journal') ?>">Jahr anlegen</a>.</p>
<?php else: ?>
<section>
<h2><?= $entry ? 'Buchung bearbeiten' : 'Neue Buchung' ?></h2>
<?php if ($entry && !empty($entry['source_type']) && $entry['source_type'] !== 'manual'): ?>
<p style="font-size:10px;color:var(--accent);margin-bottom:4px;">
<?php if ($entry['source_type'] === 'invoice_payment' && !empty($entry['invoice_id'])): ?>
Automatisch erstellt aus Rechnungszahlung:
<a href="<?= url_for('invoice_pdf.php?id=' . $entry['invoice_id']) ?>" target="_blank">Rechnung PDF</a>
<?php elseif ($entry['source_type'] === 'expense_payment' && !empty($entry['expense_id'])): ?>
Automatisch erstellt aus Ausgabe:
<a href="<?= url_for('expenses.php?action=edit&id=' . $entry['expense_id']) ?>">Ausgabe bearbeiten</a>
<?php endif; ?>
</p>
<?php endif; ?>
<form method="post">
<input type="hidden" name="id" value="<?= htmlspecialchars($entry['id'] ?? '') ?>">
<div class="flex-row">
<label>Jahr:
<select name="year_id" required>
<?php foreach ($years as $y): ?>
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?> <?= $y['is_closed'] ? 'disabled' : '' ?>>
<?= (int)$y['year'] ?><?= $y['is_closed'] ? ' (geschlossen)' : '' ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>Datum:
<input type="date" name="entry_date" value="<?= htmlspecialchars($entry['entry_date'] ?? date('Y-m-d')) ?>" required>
</label>
</div>
<label>Text:
<input type="text" name="description" value="<?= htmlspecialchars($entry['description'] ?? '') ?>" required style="max-width:600px;">
</label>
<label>Beleg:
<input type="text" name="attachment_note" value="<?= htmlspecialchars($entry['attachment_note'] ?? '') ?>" style="max-width:200px;">
</label>
<div class="flex-row">
<label>Betrag:
<input type="number" step="0.01" name="amount" value="<?= htmlspecialchars($entry['amount'] ?? '0.00') ?>" required style="max-width:150px;">
</label>
<label>Sortierung:
<input type="number" name="sort_order" value="<?= htmlspecialchars($entry['sort_order'] ?? '0') ?>" style="max-width:80px;">
</label>
</div>
<h2>Kontenverteilung</h2>
<table class="list">
<thead>
<tr>
<th>Konto</th>
<th>S/H</th>
<th>Betrag</th>
<th>Notiz</th>
<th></th>
</tr>
</thead>
<tbody id="accounts-body">
</tbody>
</table>
<button type="button" onclick="addAccountRow();" style="margin-top:8px;">+ Zeile hinzufügen</button>
<div style="margin-top:16px;">
<button type="submit">Speichern</button>
<button type="submit" name="save_and_new" value="1" class="secondary">Speichern &amp; Neu</button>
<a href="<?= url_for('journal.php?year_id=' . $year_id . '&month=' . $month) ?>" class="button-secondary">Abbrechen</a>
</div>
</form>
</section>
<script>
// Bestehende Konten laden oder leere Zeilen
<?php if ($entry && !empty($entry['accounts'])): ?>
<?php foreach ($entry['accounts'] as $acct): ?>
addAccountRow(
<?= json_encode($acct['account_type']) ?>,
<?= json_encode($acct['side']) ?>,
<?= json_encode($acct['amount']) ?>,
<?= json_encode($acct['revenue_category_id'] ?? '') ?>,
<?= json_encode($acct['expense_category_id'] ?? '') ?>,
<?= json_encode($acct['note'] ?? '') ?>
);
<?php endforeach; ?>
<?php else: ?>
addAccountRow();
addAccountRow();
<?php endif; ?>
</script>
<?php endif; ?>
</main>
<script src="assets/command-palette.js"></script>
</body>
</html>