312 lines
13 KiB
PHP
312 lines
13 KiB
PHP
<?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 & 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>
|