Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style
This commit is contained in:
184
pirp/public/assets/combobox.js
Normal file
184
pirp/public/assets/combobox.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* PirpCombobox - Tippbare Dropdown-Auswahl als Ersatz für <select>
|
||||
*
|
||||
* Verwendung:
|
||||
* new PirpCombobox(containerEl, accountOptions, {
|
||||
* placeholder: '--Konto--',
|
||||
* selectedValue: 'kasse_s|soll||',
|
||||
* onSelect: function(opt) { ... }
|
||||
* });
|
||||
*
|
||||
* accountOptions: { "Gruppenname": [ {value, label, side, rev_id, exp_id}, ... ], ... }
|
||||
*/
|
||||
class PirpCombobox {
|
||||
constructor(container, options, config) {
|
||||
this.container = container;
|
||||
this.options = options; // grouped: { group: [{value, label, side, rev_id, exp_id}] }
|
||||
this.config = config || {};
|
||||
this.flatOptions = [];
|
||||
this.highlighted = -1;
|
||||
|
||||
// Flatten options for filtering
|
||||
for (const [group, opts] of Object.entries(this.options)) {
|
||||
for (const opt of opts) {
|
||||
this.flatOptions.push({ group, ...opt });
|
||||
}
|
||||
}
|
||||
|
||||
this.build();
|
||||
this.bindEvents();
|
||||
|
||||
// Set initial selection
|
||||
if (this.config.selectedValue) {
|
||||
const parts = this.config.selectedValue.split('|');
|
||||
const match = this.flatOptions.find(o =>
|
||||
o.value === parts[0] && o.side === parts[1] &&
|
||||
String(o.rev_id) === String(parts[2]) &&
|
||||
String(o.exp_id) === String(parts[3])
|
||||
);
|
||||
if (match) {
|
||||
this.input.value = match.label;
|
||||
this.selectedOpt = match;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
this.container.classList.add('pirp-combobox');
|
||||
|
||||
this.input = document.createElement('input');
|
||||
this.input.type = 'text';
|
||||
this.input.placeholder = this.config.placeholder || '--Konto--';
|
||||
this.input.autocomplete = 'off';
|
||||
|
||||
this.dropdown = document.createElement('div');
|
||||
this.dropdown.className = 'pirp-combobox-dropdown';
|
||||
this.dropdown.style.display = 'none';
|
||||
|
||||
this.container.appendChild(this.input);
|
||||
this.container.appendChild(this.dropdown);
|
||||
|
||||
this.selectedOpt = null;
|
||||
}
|
||||
|
||||
renderDropdown(filter) {
|
||||
this.dropdown.innerHTML = '';
|
||||
this.highlighted = -1;
|
||||
const f = (filter || '').toLowerCase();
|
||||
let visibleItems = [];
|
||||
let lastGroup = null;
|
||||
|
||||
for (const opt of this.flatOptions) {
|
||||
if (f && opt.label.toLowerCase().indexOf(f) === -1) continue;
|
||||
if (opt.group !== lastGroup) {
|
||||
lastGroup = opt.group;
|
||||
const groupEl = document.createElement('div');
|
||||
groupEl.className = 'pirp-combobox-group';
|
||||
groupEl.textContent = opt.group;
|
||||
this.dropdown.appendChild(groupEl);
|
||||
}
|
||||
const optEl = document.createElement('div');
|
||||
optEl.className = 'pirp-combobox-option';
|
||||
optEl.textContent = opt.label;
|
||||
optEl.dataset.index = visibleItems.length;
|
||||
visibleItems.push({ el: optEl, opt });
|
||||
this.dropdown.appendChild(optEl);
|
||||
}
|
||||
|
||||
this.visibleItems = visibleItems;
|
||||
}
|
||||
|
||||
open() {
|
||||
this.renderDropdown(this.input.value === (this.selectedOpt ? this.selectedOpt.label : '') ? '' : this.input.value);
|
||||
this.dropdown.style.display = 'block';
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dropdown.style.display = 'none';
|
||||
this.highlighted = -1;
|
||||
}
|
||||
|
||||
select(opt) {
|
||||
this.selectedOpt = opt;
|
||||
this.input.value = opt.label;
|
||||
this.close();
|
||||
if (this.config.onSelect) {
|
||||
this.config.onSelect(opt);
|
||||
}
|
||||
}
|
||||
|
||||
highlightIndex(idx) {
|
||||
if (!this.visibleItems || !this.visibleItems.length) return;
|
||||
if (this.highlighted >= 0 && this.highlighted < this.visibleItems.length) {
|
||||
this.visibleItems[this.highlighted].el.classList.remove('highlighted');
|
||||
}
|
||||
this.highlighted = Math.max(0, Math.min(idx, this.visibleItems.length - 1));
|
||||
this.visibleItems[this.highlighted].el.classList.add('highlighted');
|
||||
this.visibleItems[this.highlighted].el.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.input.addEventListener('focus', () => {
|
||||
this.open();
|
||||
});
|
||||
|
||||
this.input.addEventListener('input', () => {
|
||||
this.renderDropdown(this.input.value);
|
||||
this.dropdown.style.display = 'block';
|
||||
if (this.visibleItems.length > 0) {
|
||||
this.highlightIndex(0);
|
||||
}
|
||||
});
|
||||
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
if (this.dropdown.style.display === 'none') {
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
this.open();
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
this.highlightIndex(this.highlighted + 1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
this.highlightIndex(this.highlighted - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (this.highlighted >= 0 && this.visibleItems[this.highlighted]) {
|
||||
this.select(this.visibleItems[this.highlighted].opt);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (this.selectedOpt) {
|
||||
this.input.value = this.selectedOpt.label;
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
this.input.addEventListener('blur', () => {
|
||||
// Delay to allow click on option to register
|
||||
setTimeout(() => {
|
||||
if (this.selectedOpt) {
|
||||
this.input.value = this.selectedOpt.label;
|
||||
}
|
||||
this.close();
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Use mousedown to prevent blur race condition
|
||||
this.dropdown.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault(); // Prevent blur
|
||||
const optEl = e.target.closest('.pirp-combobox-option');
|
||||
if (optEl) {
|
||||
const idx = parseInt(optEl.dataset.index);
|
||||
if (this.visibleItems[idx]) {
|
||||
this.select(this.visibleItems[idx].opt);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
202
pirp/public/assets/command-palette.js
Normal file
202
pirp/public/assets/command-palette.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// PIRP Command Palette (Ctrl+K / Cmd+K)
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var overlay = null;
|
||||
var input = null;
|
||||
var resultsList = null;
|
||||
var debounceTimer = null;
|
||||
var activeIndex = -1;
|
||||
var currentResults = [];
|
||||
|
||||
var typeLabels = {
|
||||
invoice: 'Rechnungen',
|
||||
customer: 'Kunden',
|
||||
expense: 'Ausgaben',
|
||||
journal: 'Journal'
|
||||
};
|
||||
|
||||
var typeIcons = {
|
||||
invoice: '\u25B8',
|
||||
customer: '\u25CF',
|
||||
expense: '\u25A0',
|
||||
journal: '\u25C6'
|
||||
};
|
||||
|
||||
function create() {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'cmd-palette-overlay hidden';
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'cmd-palette';
|
||||
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.placeholder = 'Suche nach Rechnungen, Kunden, Ausgaben, Journal...';
|
||||
input.addEventListener('input', function() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(doSearch, 200);
|
||||
});
|
||||
input.addEventListener('keydown', handleKeydown);
|
||||
|
||||
resultsList = document.createElement('div');
|
||||
resultsList.className = 'cmd-palette-results';
|
||||
|
||||
modal.appendChild(input);
|
||||
modal.appendChild(resultsList);
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!overlay) create();
|
||||
overlay.classList.remove('hidden');
|
||||
input.value = '';
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
currentResults = [];
|
||||
setTimeout(function() { input.focus(); }, 10);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (overlay) overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
function isOpen() {
|
||||
return overlay && !overlay.classList.contains('hidden');
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
var q = input.value.trim();
|
||||
if (q.length < 2) {
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
currentResults = [];
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('search_api.php?q=' + encodeURIComponent(q))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.ok) return;
|
||||
currentResults = data.results;
|
||||
activeIndex = -1;
|
||||
renderResults(data.results);
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
resultsList.innerHTML = '';
|
||||
if (!results.length) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'cmd-palette-empty';
|
||||
empty.textContent = 'Keine Ergebnisse';
|
||||
resultsList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by type
|
||||
var groups = {};
|
||||
var order = [];
|
||||
results.forEach(function(r) {
|
||||
if (!groups[r.type]) {
|
||||
groups[r.type] = [];
|
||||
order.push(r.type);
|
||||
}
|
||||
groups[r.type].push(r);
|
||||
});
|
||||
|
||||
var idx = 0;
|
||||
order.forEach(function(type) {
|
||||
var header = document.createElement('div');
|
||||
header.className = 'cmd-palette-group';
|
||||
header.textContent = typeLabels[type] || type;
|
||||
resultsList.appendChild(header);
|
||||
|
||||
groups[type].forEach(function(r) {
|
||||
var item = document.createElement('a');
|
||||
item.className = 'cmd-palette-item';
|
||||
item.href = r.url;
|
||||
item.setAttribute('data-idx', idx);
|
||||
|
||||
var icon = document.createElement('span');
|
||||
icon.className = 'cmd-palette-icon';
|
||||
icon.textContent = typeIcons[r.type] || '\u25B8';
|
||||
item.appendChild(icon);
|
||||
|
||||
var text = document.createElement('span');
|
||||
text.className = 'cmd-palette-text';
|
||||
|
||||
var title = document.createElement('span');
|
||||
title.className = 'cmd-palette-title';
|
||||
title.textContent = r.title;
|
||||
text.appendChild(title);
|
||||
|
||||
if (r.subtitle) {
|
||||
var sub = document.createElement('span');
|
||||
sub.className = 'cmd-palette-subtitle';
|
||||
sub.textContent = r.subtitle;
|
||||
text.appendChild(sub);
|
||||
}
|
||||
|
||||
item.appendChild(text);
|
||||
item.addEventListener('mouseenter', function() {
|
||||
setActive(parseInt(item.getAttribute('data-idx')));
|
||||
});
|
||||
resultsList.appendChild(item);
|
||||
idx++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setActive(idx) {
|
||||
var items = resultsList.querySelectorAll('.cmd-palette-item');
|
||||
items.forEach(function(el) { el.classList.remove('active'); });
|
||||
activeIndex = idx;
|
||||
if (idx >= 0 && idx < items.length) {
|
||||
items[idx].classList.add('active');
|
||||
items[idx].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
var items = resultsList.querySelectorAll('.cmd-palette-item');
|
||||
var count = items.length;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex < count - 1 ? activeIndex + 1 : 0);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex > 0 ? activeIndex - 1 : count - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (activeIndex >= 0 && activeIndex < count) {
|
||||
window.location = items[activeIndex].href;
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// Global keyboard listener
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (isOpen()) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape' && isOpen()) {
|
||||
e.preventDefault();
|
||||
close();
|
||||
}
|
||||
});
|
||||
})();
|
||||
1391
pirp/public/assets/style.css
Normal file
1391
pirp/public/assets/style.css
Normal file
File diff suppressed because it is too large
Load Diff
192
pirp/public/belegarchiv.php
Normal file
192
pirp/public/belegarchiv.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$pdo = get_db();
|
||||
|
||||
$filter_type = $_GET['type'] ?? 'all'; // all | rechnung | ausgabe | mahnung
|
||||
$filter_from = trim($_GET['from'] ?? '');
|
||||
$filter_to = trim($_GET['to'] ?? '');
|
||||
$filter_q = trim($_GET['q'] ?? '');
|
||||
$invoice_id = isset($_GET['invoice_id']) ? (int)$_GET['invoice_id'] : 0;
|
||||
|
||||
$belege = [];
|
||||
|
||||
// ---- Rechnungs-PDFs ----
|
||||
if ($filter_type === 'all' || $filter_type === 'rechnung') {
|
||||
$base = "FROM invoices i JOIN customers c ON c.id = i.customer_id WHERE i.pdf_path IS NOT NULL";
|
||||
$params = [];
|
||||
if ($filter_from) { $base .= " AND i.invoice_date >= :from"; $params[':from'] = $filter_from; }
|
||||
if ($filter_to) { $base .= " AND i.invoice_date <= :to"; $params[':to'] = $filter_to; }
|
||||
if ($filter_q) { $base .= " AND (i.invoice_number ILIKE :q OR c.name ILIKE :q2)";
|
||||
$params[':q'] = '%' . $filter_q . '%'; $params[':q2'] = '%' . $filter_q . '%'; }
|
||||
try {
|
||||
$sql = "SELECT i.id, i.invoice_date AS beleg_date, i.invoice_number AS beleg_ref,
|
||||
c.name AS kunde, i.total_gross AS betrag, i.pdf_path,
|
||||
'rechnung' AS beleg_type, COALESCE(i.is_storno, FALSE) AS is_storno $base";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
} catch (\PDOException $e) {
|
||||
// is_storno Spalte noch nicht migriert — Fallback ohne die Spalte
|
||||
$sql = "SELECT i.id, i.invoice_date AS beleg_date, i.invoice_number AS beleg_ref,
|
||||
c.name AS kunde, i.total_gross AS betrag, i.pdf_path,
|
||||
'rechnung' AS beleg_type, FALSE AS is_storno $base";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
}
|
||||
$belege = array_merge($belege, $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
// ---- Ausgaben-Belege ----
|
||||
if ($filter_type === 'all' || $filter_type === 'ausgabe') {
|
||||
$sql = "SELECT e.id, e.expense_date AS beleg_date, e.description AS beleg_ref,
|
||||
'' AS kunde, e.amount AS betrag, e.attachment_path AS pdf_path,
|
||||
'ausgabe' AS beleg_type, FALSE AS is_storno
|
||||
FROM expenses e
|
||||
WHERE e.attachment_path IS NOT NULL";
|
||||
$params = [];
|
||||
if ($filter_from) { $sql .= " AND e.expense_date >= :from"; $params[':from'] = $filter_from; }
|
||||
if ($filter_to) { $sql .= " AND e.expense_date <= :to"; $params[':to'] = $filter_to; }
|
||||
if ($filter_q) { $sql .= " AND e.description ILIKE :q"; $params[':q'] = '%' . $filter_q . '%'; }
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$belege = array_merge($belege, $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
// ---- Mahnungen ----
|
||||
$show_mahnungen = ($filter_type === 'all' || $filter_type === 'mahnung');
|
||||
if ($show_mahnungen) {
|
||||
$sql = "SELECT m.id, m.mahnung_date AS beleg_date,
|
||||
'MAHNUNG L' || m.level || ' – ' || i.invoice_number AS beleg_ref,
|
||||
c.name AS kunde, i.total_gross + m.fee_amount AS betrag,
|
||||
m.pdf_path, 'mahnung' AS beleg_type, FALSE AS is_storno,
|
||||
m.invoice_id
|
||||
FROM mahnungen m
|
||||
JOIN invoices i ON i.id = m.invoice_id
|
||||
JOIN customers c ON c.id = i.customer_id
|
||||
WHERE m.pdf_path IS NOT NULL";
|
||||
$params = [];
|
||||
if ($invoice_id) { $sql .= " AND m.invoice_id = :iid"; $params[':iid'] = $invoice_id; }
|
||||
if ($filter_from) { $sql .= " AND m.mahnung_date >= :from"; $params[':from'] = $filter_from; }
|
||||
if ($filter_to) { $sql .= " AND m.mahnung_date <= :to"; $params[':to'] = $filter_to; }
|
||||
if ($filter_q) { $sql .= " AND (i.invoice_number ILIKE :q OR c.name ILIKE :q2)";
|
||||
$params[':q'] = '%' . $filter_q . '%'; $params[':q2'] = '%' . $filter_q . '%'; }
|
||||
try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$belege = array_merge($belege, $stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
} catch (\PDOException $e) {
|
||||
// Tabelle noch nicht migriert
|
||||
}
|
||||
}
|
||||
|
||||
// Sortieren: neueste zuerst
|
||||
usort($belege, fn($a, $b) => strcmp($b['beleg_date'], $a['beleg_date']));
|
||||
|
||||
$type_labels = ['rechnung' => 'Rechnung', 'ausgabe' => 'Ausgabe', 'mahnung' => 'Mahnung'];
|
||||
$type_colors = ['rechnung' => 'var(--accent)', 'ausgabe' => 'var(--info)', 'mahnung' => 'var(--warning)'];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Belegarchiv</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') ?>"><?= 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') ?>" class="active"><?= 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>
|
||||
<form method="get" class="filters">
|
||||
<label>Typ:
|
||||
<select name="type">
|
||||
<option value="all" <?= $filter_type === 'all' ? 'selected' : '' ?>>Alle</option>
|
||||
<option value="rechnung" <?= $filter_type === 'rechnung' ? 'selected' : '' ?>>Rechnungen</option>
|
||||
<option value="ausgabe" <?= $filter_type === 'ausgabe' ? 'selected' : '' ?>>Ausgaben</option>
|
||||
<option value="mahnung" <?= $filter_type === 'mahnung' ? 'selected' : '' ?>>Mahnungen</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Suche:
|
||||
<input type="text" name="q" value="<?= htmlspecialchars($filter_q) ?>" placeholder="Nr., Beschreibung, Kunde...">
|
||||
</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('belegarchiv.php') ?>">Zurücksetzen</a>
|
||||
</form>
|
||||
|
||||
<section>
|
||||
<h2>Belegarchiv
|
||||
<span style="font-weight:normal;font-size:12px;color:var(--text-muted);"><?= count($belege) ?> Dokument(e)</span>
|
||||
</h2>
|
||||
|
||||
<?php if (empty($belege)): ?>
|
||||
<p style="color:var(--text-muted);">Keine Belege gefunden.</p>
|
||||
<?php else: ?>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Typ</th>
|
||||
<th>Referenz / Beschreibung</th>
|
||||
<th>Kunde</th>
|
||||
<th style="text-align:right;">Betrag</th>
|
||||
<th>PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($belege as $b): ?>
|
||||
<tr>
|
||||
<td><?= date('d.m.Y', strtotime($b['beleg_date'])) ?></td>
|
||||
<td>
|
||||
<span style="font-size:10px;color:<?= $type_colors[$b['beleg_type']] ?? 'var(--text)' ?>;">
|
||||
<?= $type_labels[$b['beleg_type']] ?? $b['beleg_type'] ?>
|
||||
<?php if (!empty($b['is_storno'])): ?>
|
||||
<span style="color:var(--error);"> · STORNO</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($b['beleg_ref']) ?></td>
|
||||
<td style="color:var(--text-muted);"><?= htmlspecialchars($b['kunde']) ?></td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);font-size:12px;">
|
||||
<?= number_format((float)$b['betrag'], 2, ',', '.') ?> €
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($b['beleg_type'] === 'rechnung'): ?>
|
||||
<a href="<?= url_for('invoice_pdf.php?id=' . $b['id']) ?>" target="_blank">PDF</a>
|
||||
<?php elseif ($b['beleg_type'] === 'ausgabe'): ?>
|
||||
<a href="<?= url_for('expense_file.php?id=' . $b['id']) ?>" target="_blank">PDF</a>
|
||||
<?php elseif ($b['beleg_type'] === 'mahnung'): ?>
|
||||
<a href="<?= url_for('mahnung_pdf.php?id=' . $b['id']) ?>" target="_blank">PDF</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
139
pirp/public/customers.php
Normal file
139
pirp/public/customers.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/customer_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
$msg = '';
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$idPost = isset($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$data = [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'address' => $_POST['address'] ?? '',
|
||||
'zip' => $_POST['zip'] ?? '',
|
||||
'city' => $_POST['city'] ?? '',
|
||||
'country' => $_POST['country'] ?? '',
|
||||
];
|
||||
if (trim($data['name']) === '') {
|
||||
$error = 'Name darf nicht leer sein.';
|
||||
} else {
|
||||
save_customer($idPost, $data);
|
||||
$msg = 'Kunde gespeichert.';
|
||||
$action = '';
|
||||
$id = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $id) {
|
||||
delete_customer($id);
|
||||
$msg = 'Kunde gelöscht.';
|
||||
$action = '';
|
||||
$id = null;
|
||||
}
|
||||
|
||||
$customers = get_customers();
|
||||
$editCustomer = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
$editCustomer = get_customer($id);
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Kunden</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') ?>"><?= icon_invoices() ?>Rechnungen</a>
|
||||
<a href="<?= url_for('customers.php') ?>" class="active"><?= 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>
|
||||
<?php if ($msg): ?><p class="success"><?= htmlspecialchars($msg) ?></p><?php endif; ?>
|
||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
||||
|
||||
<section>
|
||||
<h2><?= $editCustomer ? 'Kunde bearbeiten' : 'Neuer Kunde' ?></h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="id" value="<?= htmlspecialchars($editCustomer['id'] ?? '') ?>">
|
||||
<label>Name:
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($editCustomer['name'] ?? '') ?>" required>
|
||||
</label>
|
||||
<label>Adresse:
|
||||
<textarea name="address" rows="3"><?= htmlspecialchars($editCustomer['address'] ?? '') ?></textarea>
|
||||
</label>
|
||||
<div class="flex-row">
|
||||
<label>PLZ:
|
||||
<input type="text" name="zip" value="<?= htmlspecialchars($editCustomer['zip'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Ort:
|
||||
<input type="text" name="city" value="<?= htmlspecialchars($editCustomer['city'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Land:
|
||||
<input type="text" name="country" value="<?= htmlspecialchars($editCustomer['country'] ?? '') ?>">
|
||||
</label>
|
||||
</div>
|
||||
<?php if (!empty($editCustomer['customer_number'])): ?>
|
||||
<p>Kundennummer: <?= htmlspecialchars($editCustomer['customer_number']) ?></p>
|
||||
<?php endif; ?>
|
||||
<button type="submit">Speichern</button>
|
||||
<?php if ($editCustomer): ?>
|
||||
<a href="<?= url_for('customers.php') ?>">Abbrechen</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Alle Kunden</h2>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kundennr.</th>
|
||||
<th>Name</th>
|
||||
<th>Adresse</th>
|
||||
<th>Ort</th>
|
||||
<th>Land</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($c['customer_number'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($c['name']) ?></td>
|
||||
<td><?= nl2br(htmlspecialchars($c['address'])) ?></td>
|
||||
<td><?= htmlspecialchars($c['zip'] . ' ' . $c['city']) ?></td>
|
||||
<td><?= htmlspecialchars($c['country']) ?></td>
|
||||
<td>
|
||||
<a href="<?= url_for('customers.php?action=edit&id=' . $c['id']) ?>">Bearbeiten</a>
|
||||
<a href="<?= url_for('customers.php?action=delete&id=' . $c['id']) ?>" onclick="return confirm('Kunde wirklich löschen?');">Löschen</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($customers)): ?>
|
||||
<tr><td colspan="6">Keine Kunden vorhanden.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
336
pirp/public/euer.php
Normal file
336
pirp/public/euer.php
Normal file
@@ -0,0 +1,336 @@
|
||||
<?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();
|
||||
|
||||
$pdo = get_db();
|
||||
|
||||
// Jahr bestimmen
|
||||
$years = get_journal_years();
|
||||
$current_cal_year = (int)date('Y');
|
||||
|
||||
$year = isset($_GET['year']) ? (int)$_GET['year'] : $current_cal_year;
|
||||
|
||||
// Journal-Jahr finden (oder automatisch erstellen bei Bedarf)
|
||||
$journal_year = get_journal_year_by_year($year);
|
||||
|
||||
// Fehlende Buchungen nachholen (Quick-Fix)
|
||||
$fix_msg = '';
|
||||
$fix_errors = [];
|
||||
if (isset($_GET['fix_missing']) && $journal_year) {
|
||||
$fixed_inv = 0;
|
||||
$fixed_exp = 0;
|
||||
$consistency = check_journal_consistency();
|
||||
|
||||
foreach ($consistency['unbooked_invoice_list'] as $inv) {
|
||||
try {
|
||||
create_journal_entry_from_invoice((int)$inv['id']);
|
||||
$fixed_inv++;
|
||||
} catch (Exception $e) {
|
||||
$fix_errors[] = 'Rechnung ' . ($inv['invoice_number'] ?? '#' . $inv['id']) . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($consistency['unbooked_expense_list'] as $exp) {
|
||||
try {
|
||||
create_journal_entry_from_expense((int)$exp['id']);
|
||||
$fixed_exp++;
|
||||
} catch (Exception $e) {
|
||||
$fix_errors[] = 'Ausgabe "' . ($exp['description'] ?? '#' . $exp['id']) . '": ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$fix_msg = $fixed_inv . ' Rechnungen und ' . $fixed_exp . ' Ausgaben nachgebucht.';
|
||||
if ($fix_errors) {
|
||||
$fix_msg .= ' ' . count($fix_errors) . ' Fehler aufgetreten.';
|
||||
}
|
||||
}
|
||||
|
||||
// CSV-Export Journal
|
||||
if (isset($_GET['csv_journal']) && $journal_year) {
|
||||
$euer = generate_journal_euer((int)$journal_year['id']);
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="eur_' . $year . '.csv"');
|
||||
|
||||
$out = fopen('php://output', 'w');
|
||||
fwrite($out, "\xEF\xBB\xBF");
|
||||
|
||||
fputcsv($out, ['EÜR - ' . $year], ';');
|
||||
fputcsv($out, [], ';');
|
||||
|
||||
fputcsv($out, ['Position', 'Betrag'], ';');
|
||||
fputcsv($out, [], ';');
|
||||
|
||||
fputcsv($out, ['EINNAHMEN'], ';');
|
||||
foreach ($euer['erloese_detail'] as $row) {
|
||||
fputcsv($out, [$row['name'], number_format((float)$row['total'], 2, ',', '.')], ';');
|
||||
}
|
||||
if ($euer['sonstiges_einnahmen'] > 0) {
|
||||
fputcsv($out, ['Sonstige Einnahmen', number_format($euer['sonstiges_einnahmen'], 2, ',', '.')], ';');
|
||||
}
|
||||
fputcsv($out, ['Einnahmen gesamt', number_format($euer['einnahmen_total'], 2, ',', '.')], ';');
|
||||
fputcsv($out, [], ';');
|
||||
|
||||
fputcsv($out, ['AUSGABEN'], ';');
|
||||
if ($euer['wareneingang'] > 0) {
|
||||
fputcsv($out, ['Wareneingang', number_format($euer['wareneingang'], 2, ',', '.')], ';');
|
||||
}
|
||||
foreach ($euer['aufwand_detail'] as $row) {
|
||||
fputcsv($out, [$row['name'], number_format((float)$row['total'], 2, ',', '.')], ';');
|
||||
}
|
||||
if ($euer['sonstiges_ausgaben'] > 0) {
|
||||
fputcsv($out, ['Sonstige Ausgaben', number_format($euer['sonstiges_ausgaben'], 2, ',', '.')], ';');
|
||||
}
|
||||
fputcsv($out, ['Ausgaben gesamt', number_format($euer['ausgaben_total'], 2, ',', '.')], ';');
|
||||
fputcsv($out, [], ';');
|
||||
|
||||
fputcsv($out, ['STEUER'], ';');
|
||||
fputcsv($out, ['MwSt (eingenommen)', number_format($euer['mwst'], 2, ',', '.')], ';');
|
||||
fputcsv($out, ['VorSt (gezahlt)', number_format($euer['vorst'], 2, ',', '.')], ';');
|
||||
fputcsv($out, ['Steuer-Saldo', number_format($euer['steuer_saldo'], 2, ',', '.')], ';');
|
||||
fputcsv($out, [], ';');
|
||||
|
||||
if ($euer['privat_entnahmen'] > 0 || $euer['privat_einlagen'] > 0) {
|
||||
fputcsv($out, ['PRIVATKONTEN'], ';');
|
||||
if ($euer['privat_einlagen'] > 0) {
|
||||
fputcsv($out, ['Privateinlagen', number_format($euer['privat_einlagen'], 2, ',', '.')], ';');
|
||||
}
|
||||
if ($euer['privat_entnahmen'] > 0) {
|
||||
fputcsv($out, ['Privatentnahmen', number_format($euer['privat_entnahmen'], 2, ',', '.')], ';');
|
||||
}
|
||||
fputcsv($out, [], ';');
|
||||
}
|
||||
|
||||
fputcsv($out, ['GEWINN', number_format($euer['gewinn'], 2, ',', '.')], ';');
|
||||
|
||||
fclose($out);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Journal-Daten laden
|
||||
$journal_euer = null;
|
||||
if ($journal_year) {
|
||||
$journal_euer = generate_journal_euer((int)$journal_year['id']);
|
||||
}
|
||||
|
||||
// Konsistenz-Check: Fehlende Buchungen
|
||||
$consistency = check_journal_consistency();
|
||||
$has_missing = ($consistency['unbooked_invoices'] > 0 || $consistency['unbooked_expenses'] > 0);
|
||||
|
||||
// Verfügbare Jahre sammeln (aus Journal + Rechnungen + Ausgaben)
|
||||
$available_years = [];
|
||||
foreach ($years as $y) {
|
||||
$available_years[(int)$y['year']] = true;
|
||||
}
|
||||
$stmt = $pdo->query("SELECT DISTINCT EXTRACT(YEAR FROM invoice_date)::int AS y FROM invoices WHERE paid = TRUE ORDER BY y DESC");
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$available_years[$row['y']] = true;
|
||||
}
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT DISTINCT EXTRACT(YEAR FROM expense_date)::int AS y FROM expenses WHERE paid = TRUE ORDER BY y DESC");
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$available_years[$row['y']] = true;
|
||||
}
|
||||
} catch (PDOException $e) {}
|
||||
krsort($available_years);
|
||||
$available_years = array_keys($available_years);
|
||||
if (empty($available_years)) {
|
||||
$available_years = [$current_cal_year];
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>EÜR <?= $year ?></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') ?>"><?= 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') ?>" class="active"><?= 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>
|
||||
<h2>Einnahmen-Überschuss-Rechnung <?= $year ?></h2>
|
||||
|
||||
<!-- Jahr-Auswahl -->
|
||||
<form method="get" style="margin-bottom:16px;">
|
||||
<div class="flex-row">
|
||||
<label>Jahr:
|
||||
<select name="year" onchange="this.form.submit();">
|
||||
<?php foreach ($available_years as $y): ?>
|
||||
<option value="<?= $y ?>" <?= $y == $year ? 'selected' : '' ?>><?= $y ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($fix_msg): ?>
|
||||
<p class="success"><?= htmlspecialchars($fix_msg) ?></p>
|
||||
<?php if (!empty($fix_errors)): ?>
|
||||
<div class="gobd-warning">
|
||||
<h3>Fehler beim Nachbuchen</h3>
|
||||
<ul>
|
||||
<?php foreach ($fix_errors as $fe): ?>
|
||||
<li><?= htmlspecialchars($fe) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p style="font-size:11px;">Bitte die betroffenen Belege manuell prüfen und ggf. eine Aufwandskategorie zuweisen.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($has_missing): ?>
|
||||
<div class="gobd-warning">
|
||||
<h3>Fehlende Journalbuchungen</h3>
|
||||
<p>Es gibt bezahlte Belege ohne Journaleintrag. Die EÜR ist dadurch unvollständig.</p>
|
||||
<ul>
|
||||
<?php if ($consistency['unbooked_invoices'] > 0): ?>
|
||||
<li><strong><?= $consistency['unbooked_invoices'] ?></strong> bezahlte Rechnung(en) ohne Journalbuchung</li>
|
||||
<?php endif; ?>
|
||||
<?php if ($consistency['unbooked_expenses'] > 0): ?>
|
||||
<li><strong><?= $consistency['unbooked_expenses'] ?></strong> bezahlte Ausgabe(n) ohne Journalbuchung</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<a href="<?= url_for('euer.php?year=' . $year . '&fix_missing=1') ?>"
|
||||
class="button" onclick="return confirm('Fehlende Journalbuchungen jetzt automatisch erstellen?');">
|
||||
Fehlende Buchungen nachholen
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($journal_euer): ?>
|
||||
<section class="euer-section">
|
||||
<p class="euer-desc">Basierend auf Journalbuchungen (Zufluss-/Abflussprinzip)</p>
|
||||
|
||||
<h4>Einnahmen</h4>
|
||||
<table class="list">
|
||||
<tbody>
|
||||
<?php foreach ($journal_euer['erloese_detail'] as $row): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($row['name']) ?></td>
|
||||
<td style="text-align:right;"><?= number_format((float)$row['total'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($journal_euer['sonstiges_einnahmen'] > 0): ?>
|
||||
<tr>
|
||||
<td>Sonstige Einnahmen</td>
|
||||
<td style="text-align:right;"><?= number_format($journal_euer['sonstiges_einnahmen'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<td><strong>Einnahmen gesamt</strong></td>
|
||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['einnahmen_total'], 2, ',', '.') ?> €</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Ausgaben</h4>
|
||||
<table class="list">
|
||||
<tbody>
|
||||
<?php if ($journal_euer['wareneingang'] > 0): ?>
|
||||
<tr>
|
||||
<td>Wareneingang</td>
|
||||
<td style="text-align:right;"><?= number_format($journal_euer['wareneingang'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($journal_euer['aufwand_detail'] as $row): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($row['name']) ?></td>
|
||||
<td style="text-align:right;"><?= number_format((float)$row['total'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($journal_euer['sonstiges_ausgaben'] > 0): ?>
|
||||
<tr>
|
||||
<td>Sonstige Ausgaben</td>
|
||||
<td style="text-align:right;"><?= number_format($journal_euer['sonstiges_ausgaben'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<td><strong>Ausgaben gesamt</strong></td>
|
||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['ausgaben_total'], 2, ',', '.') ?> €</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Steuer</h4>
|
||||
<table class="list">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>MwSt (eingenommen)</td>
|
||||
<td style="text-align:right;"><?= number_format($journal_euer['mwst'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VorSt (gezahlt)</td>
|
||||
<td style="text-align:right;"><?= number_format($journal_euer['vorst'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Steuer-Saldo</strong></td>
|
||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['steuer_saldo'], 2, ',', '.') ?> €</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ($journal_euer['privat_entnahmen'] > 0 || $journal_euer['privat_einlagen'] > 0): ?>
|
||||
<h4>Privatkonten</h4>
|
||||
<table class="list">
|
||||
<tbody>
|
||||
<?php if ($journal_euer['privat_einlagen'] > 0): ?>
|
||||
<tr>
|
||||
<td>Privateinlagen</td>
|
||||
<td style="text-align:right;"><?= number_format($journal_euer['privat_einlagen'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($journal_euer['privat_entnahmen'] > 0): ?>
|
||||
<tr>
|
||||
<td>Privatentnahmen</td>
|
||||
<td style="text-align:right;"><?= number_format($journal_euer['privat_entnahmen'], 2, ',', '.') ?> €</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<h4>Ergebnis</h4>
|
||||
<table class="list">
|
||||
<tbody>
|
||||
<tr class="euer-result <?= $journal_euer['gewinn'] < 0 ? 'negative' : '' ?>">
|
||||
<td><strong>Gewinn / Verlust</strong></td>
|
||||
<td style="text-align:right;"><strong><?= number_format($journal_euer['gewinn'], 2, ',', '.') ?> €</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:8px;">
|
||||
<a href="<?= url_for('euer.php?year=' . $year . '&csv_journal=1') ?>" class="button-secondary">CSV Export</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="euer-section">
|
||||
<p class="euer-desc">Kein Journal für <?= $year ?> vorhanden.</p>
|
||||
<?php if ($has_missing): ?>
|
||||
<p>Klicke oben auf "Fehlende Buchungen nachholen" um das Journal automatisch zu erstellen.</p>
|
||||
<?php else: ?>
|
||||
<p><a href="<?= url_for('settings.php?tab=journal') ?>">Jahr im Journal anlegen</a></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
10
pirp/public/euer_export.php
Normal file
10
pirp/public/euer_export.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// Redirect to new unified EÜR page
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
|
||||
// Extract year from old date params for backwards compatibility
|
||||
$from = $_GET['from'] ?? date('Y-01-01');
|
||||
$year = date('Y', strtotime($from));
|
||||
|
||||
header('Location: ' . url_for('euer.php?year=' . $year));
|
||||
exit;
|
||||
29
pirp/public/expense_file.php
Normal file
29
pirp/public/expense_file.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/expense_functions.php';
|
||||
require_login();
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$expense = get_expense($id);
|
||||
|
||||
if (!$expense || empty($expense['attachment_path'])) {
|
||||
http_response_code(404);
|
||||
echo 'Datei nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$fsPath = __DIR__ . '/' . $expense['attachment_path'];
|
||||
|
||||
if (!is_readable($fsPath)) {
|
||||
http_response_code(404);
|
||||
echo 'Datei nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="Ausgabe-' . $id . '.pdf"');
|
||||
header('Content-Length: ' . filesize($fsPath));
|
||||
readfile($fsPath);
|
||||
exit;
|
||||
|
||||
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>
|
||||
422
pirp/public/index.php
Normal file
422
pirp/public/index.php
Normal file
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
||||
require_once __DIR__ . '/../src/journal_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$pdo = get_db();
|
||||
$year = date('Y');
|
||||
$month = (int)date('m');
|
||||
$prev_month = $month === 1 ? 12 : $month - 1;
|
||||
$prev_month_year = $month === 1 ? (int)$year - 1 : (int)$year;
|
||||
|
||||
// === RECHNUNGEN/AUSGABEN ===
|
||||
// Offene Rechnungen
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM invoices WHERE paid = FALSE");
|
||||
$stmt->execute();
|
||||
$open_invoices_count = (int)$stmt->fetchColumn();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COALESCE(SUM(total_gross),0) FROM invoices WHERE paid = FALSE");
|
||||
$stmt->execute();
|
||||
$open_invoices_sum = (float)$stmt->fetchColumn();
|
||||
|
||||
// Überfällige Rechnungen (> 14 Tage offen)
|
||||
$stmt = $pdo->prepare("SELECT i.*, c.name AS customer_name FROM invoices i JOIN customers c ON c.id = i.customer_id WHERE i.paid = FALSE AND i.invoice_date < NOW() - INTERVAL '14 days' ORDER BY i.invoice_date ASC LIMIT 5");
|
||||
$stmt->execute();
|
||||
$overdue_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Fällige Abo-Rechnungen
|
||||
$pending_recurring = count_pending_recurring_invoices();
|
||||
|
||||
// Konsistenz-Check: Fehlende Buchungen
|
||||
$consistency = check_journal_consistency();
|
||||
$has_unbooked = ($consistency['unbooked_invoices'] > 0 || $consistency['unbooked_expenses'] > 0);
|
||||
|
||||
// GoBD PDF-Status
|
||||
$gobd_status = check_pdf_integrity_status();
|
||||
$gobd_has_problems = ($gobd_status['unarchived'] > 0 || $gobd_status['invalid'] > 0 || $gobd_status['missing_files'] > 0);
|
||||
|
||||
// Letzte 5 Rechnungen
|
||||
$stmt = $pdo->prepare("SELECT i.*, c.name AS customer_name FROM invoices i JOIN customers c ON c.id = i.customer_id ORDER BY i.created_at DESC LIMIT 5");
|
||||
$stmt->execute();
|
||||
$recent_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// === JOURNAL MODUL ===
|
||||
$journal_year = get_journal_year_by_year((int)$year);
|
||||
$journal_year_id = $journal_year ? (int)$journal_year['id'] : null;
|
||||
|
||||
$journal_erloese_monat = 0;
|
||||
$journal_wareneingang_monat = 0;
|
||||
$journal_gewinn_monat = 0;
|
||||
$journal_erloese_jahr = 0;
|
||||
$journal_wareneingang_jahr = 0;
|
||||
$journal_gewinn_jahr = 0;
|
||||
$journal_entries_month = 0;
|
||||
$journal_gewinn_prev = 0;
|
||||
|
||||
if ($journal_year_id) {
|
||||
$month_profit = calculate_yearly_profitability($journal_year_id);
|
||||
if (isset($month_profit[$month])) {
|
||||
$journal_erloese_monat = $month_profit[$month]['erloese'];
|
||||
$journal_wareneingang_monat = $month_profit[$month]['wareneingang'];
|
||||
$journal_gewinn_monat = $month_profit[$month]['gewinn'];
|
||||
}
|
||||
if (isset($month_profit[$prev_month])) {
|
||||
$journal_gewinn_prev = $month_profit[$prev_month]['gewinn'];
|
||||
}
|
||||
|
||||
foreach ($month_profit as $m => $p) {
|
||||
$journal_erloese_jahr += $p['erloese'];
|
||||
$journal_wareneingang_jahr += $p['wareneingang'];
|
||||
$journal_gewinn_jahr += $p['gewinn'];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM journal_entries WHERE year_id = :y AND month = :m");
|
||||
$stmt->execute([':y' => $journal_year_id, ':m' => $month]);
|
||||
$journal_entries_month = (int)$stmt->fetchColumn();
|
||||
|
||||
$mt = calculate_monthly_totals($journal_year_id, $month);
|
||||
$journal_kasse_balance = ($mt['kasse_s'] ?? 0) - ($mt['kasse_h'] ?? 0);
|
||||
$journal_bank_balance = ($mt['bank_s'] ?? 0) - ($mt['bank_h'] ?? 0);
|
||||
} else {
|
||||
$journal_kasse_balance = 0;
|
||||
$journal_bank_balance = 0;
|
||||
}
|
||||
|
||||
// Sparkline-Daten
|
||||
$sparkline_erloese = [];
|
||||
$sparkline_gewinn = [];
|
||||
$sparkline_wareneingang = [];
|
||||
if ($journal_year_id && !empty($month_profit)) {
|
||||
for ($m = 1; $m <= $month; $m++) {
|
||||
$sparkline_erloese[] = $month_profit[$m]['erloese'] ?? 0;
|
||||
$sparkline_gewinn[] = $month_profit[$m]['gewinn'] ?? 0;
|
||||
$sparkline_wareneingang[] = $month_profit[$m]['wareneingang'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Letzte 5 Journal-Einträge
|
||||
$recent_journal = get_recent_journal_entries(5);
|
||||
|
||||
$month_names_full = [
|
||||
1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April',
|
||||
5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August',
|
||||
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember',
|
||||
];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PIRP Dashboard</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>PIRP</h1>
|
||||
<nav>
|
||||
<a href="<?= url_for('index.php') ?>" class="active"><?= 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') ?>"><?= 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 ($has_unbooked): ?>
|
||||
<div class="gobd-warning">
|
||||
<strong>Fehlende Journalbuchungen:</strong>
|
||||
<?php if ($consistency['unbooked_invoices'] > 0): ?>
|
||||
<?= $consistency['unbooked_invoices'] ?> bezahlte Rechnung(en)
|
||||
<?php endif; ?>
|
||||
<?php if ($consistency['unbooked_invoices'] > 0 && $consistency['unbooked_expenses'] > 0): ?> und <?php endif; ?>
|
||||
<?php if ($consistency['unbooked_expenses'] > 0): ?>
|
||||
<?= $consistency['unbooked_expenses'] ?> bezahlte Ausgabe(n)
|
||||
<?php endif; ?>
|
||||
ohne Journaleintrag.
|
||||
<a href="<?= url_for('euer.php?year=' . $year . '&fix_missing=1') ?>">Jetzt nachholen</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Finanz-Übersicht -->
|
||||
<h2><?= htmlspecialchars($year) ?> · <?= $month_names_full[$month] ?></h2>
|
||||
|
||||
<?php if ($journal_year_id): ?>
|
||||
|
||||
<!-- Rechnungen / Allgemein -->
|
||||
<div class="dashboard-grid" style="margin-bottom:6px;">
|
||||
<div class="card">
|
||||
<h3>Offen</h3>
|
||||
<p class="big"><?= number_format($open_invoices_sum, 2, ',', '.') ?> €</p>
|
||||
<span style="font-size:10px;color:var(--text-dim);"><?= $open_invoices_count ?> Rechnungen</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Abo-Rechnungen</h3>
|
||||
<p class="big"><?= $pending_recurring ?> fällig</p>
|
||||
<?php if ($pending_recurring > 0): ?>
|
||||
<a href="<?= url_for('recurring_generate.php') ?>">Generieren</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Erlöse (Jahr)</h3>
|
||||
<p class="big"><?= number_format($journal_erloese_jahr, 2, ',', '.') ?> €</p>
|
||||
<?php if (count($sparkline_erloese) >= 2): ?>
|
||||
<?= generate_sparkline_svg($sparkline_erloese, '#22c55e') ?>
|
||||
<?php endif; ?>
|
||||
<a href="<?= url_for('journal_summary.php?year_id=' . $journal_year_id) ?>" style="font-size:10px;">Jahresübersicht →</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Gewinn (Jahr)</h3>
|
||||
<p class="big" <?= $journal_gewinn_jahr < 0 ? 'style="color:var(--error);"' : '' ?>><?= number_format($journal_gewinn_jahr, 2, ',', '.') ?> €</p>
|
||||
<?php if (count($sparkline_gewinn) >= 2): ?>
|
||||
<?= generate_sparkline_svg($sparkline_gewinn, '#d4882a') ?>
|
||||
<?php endif; ?>
|
||||
<a href="<?= url_for('euer.php?year=' . $year) ?>" style="font-size:10px;">EÜR →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Journal Monat -->
|
||||
<h2 style="margin-bottom:4px;">
|
||||
Journal · <?= $month_names_full[$month] ?>
|
||||
<a href="<?= url_for('journal.php?year_id=' . $journal_year_id . '&month=' . $month) ?>"
|
||||
style="font-size:10px;font-weight:400;text-transform:none;letter-spacing:0;color:var(--accent);margin-left:10px;">
|
||||
<?= $journal_entries_month ?> Buchungen →
|
||||
</a>
|
||||
</h2>
|
||||
<div class="dashboard-grid" style="margin-bottom:4px;">
|
||||
<div class="card">
|
||||
<h3>Erlöse</h3>
|
||||
<p class="big"><?= number_format($journal_erloese_monat, 2, ',', '.') ?> €</p>
|
||||
<?php if ($journal_gewinn_prev != 0): ?>
|
||||
<?php $gwdiff = $journal_gewinn_monat - $journal_gewinn_prev; ?>
|
||||
<span style="font-size:10px;color:<?= $gwdiff >= 0 ? 'var(--success)' : 'var(--error)' ?>;">
|
||||
<?= $gwdiff >= 0 ? '+' : '' ?><?= number_format($gwdiff, 2, ',', '.') ?> Gewinn gg. Vormonat
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Gewinn</h3>
|
||||
<p class="big" <?= $journal_gewinn_monat < 0 ? 'style="color:var(--error);"' : '' ?>><?= number_format($journal_gewinn_monat, 2, ',', '.') ?> €</p>
|
||||
<?php if (count($sparkline_wareneingang) >= 2): ?>
|
||||
<?= generate_sparkline_svg($sparkline_wareneingang, '#737373') ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Kasse</h3>
|
||||
<p class="big" style="color:<?= $journal_kasse_balance < 0 ? 'var(--error)' : 'var(--accent)' ?>;">
|
||||
<?= number_format($journal_kasse_balance, 2, ',', '.') ?> €
|
||||
</p>
|
||||
<a href="<?= url_for('journal.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" style="font-size:10px;">Journal →</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Bank</h3>
|
||||
<p class="big" style="color:<?= $journal_bank_balance < 0 ? 'var(--error)' : 'var(--warning)' ?>;">
|
||||
<?= number_format($journal_bank_balance, 2, ',', '.') ?> €
|
||||
</p>
|
||||
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" style="font-size:10px;">+ Neue Buchung</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monatsvergleich -->
|
||||
<?php if (!empty($month_profit)): ?>
|
||||
<section style="margin-bottom:4px;">
|
||||
<h2>Monatsvergleich <?= $year ?></h2>
|
||||
<div style="padding:8px 0;">
|
||||
<?= generate_monthly_bar_chart_svg($month_profit, $month) ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<h3>Offen</h3>
|
||||
<p class="big"><?= number_format($open_invoices_sum, 2, ',', '.') ?> €</p>
|
||||
<span style="font-size:10px;color:var(--text-dim);"><?= $open_invoices_count ?> Rechnungen</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Abo-Rechnungen</h3>
|
||||
<p class="big"><?= $pending_recurring ?> fällig</p>
|
||||
<?php if ($pending_recurring > 0): ?>
|
||||
<a href="<?= url_for('recurring_generate.php') ?>">Generieren</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<h2>Journal</h2>
|
||||
<div>
|
||||
<p>Kein Journal für <?= $year ?> angelegt.
|
||||
<a href="<?= url_for('settings.php?tab=journal') ?>">Jahr erstellen</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Überfällige Rechnungen -->
|
||||
<?php if ($overdue_invoices): ?>
|
||||
<section>
|
||||
<h2>Überfällige Rechnungen</h2>
|
||||
<div>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Nr.</th>
|
||||
<th>Kunde</th>
|
||||
<th style="text-align:right;">Betrag</th>
|
||||
<th>Tage offen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($overdue_invoices as $oi): ?>
|
||||
<?php $days = (int)((time() - strtotime($oi['invoice_date'])) / 86400); ?>
|
||||
<tr>
|
||||
<td><?= date('d.m.Y', strtotime($oi['invoice_date'])) ?></td>
|
||||
<td><a href="<?= url_for('invoice_pdf.php?id=' . $oi['id']) ?>" target="_blank"><?= htmlspecialchars($oi['invoice_number']) ?></a></td>
|
||||
<td><?= htmlspecialchars($oi['customer_name']) ?></td>
|
||||
<td style="text-align:right;"><?= number_format((float)$oi['total_gross'], 2, ',', '.') ?> €</td>
|
||||
<td style="color:var(--error);"><strong><?= $days ?> Tage</strong></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Zwei-Spalten: Letzte Rechnungen + Letzte Buchungen -->
|
||||
<div class="dashboard-two-col">
|
||||
<section>
|
||||
<h2>Letzte Rechnungen</h2>
|
||||
<div>
|
||||
<table class="list">
|
||||
<thead><tr><th>Datum</th><th>Kunde</th><th style="text-align:right;">Betrag</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent_invoices as $ri): ?>
|
||||
<tr>
|
||||
<td><?= date('d.m', strtotime($ri['invoice_date'])) ?></td>
|
||||
<td><?= htmlspecialchars($ri['customer_name']) ?></td>
|
||||
<td style="text-align:right;"><?= number_format((float)$ri['total_gross'], 2, ',', '.') ?></td>
|
||||
<td><?= $ri['paid'] ? 'bezahlt' : 'offen' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($recent_invoices)): ?>
|
||||
<tr><td colspan="4" style="color:var(--text-dim);">Keine Rechnungen</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="<?= url_for('invoices.php') ?>" style="font-size:10px;">Alle Rechnungen</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Letzte Buchungen</h2>
|
||||
<div>
|
||||
<table class="list">
|
||||
<thead><tr><th>Datum</th><th>Text</th><th style="text-align:right;">Betrag</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent_journal as $rj): ?>
|
||||
<tr>
|
||||
<td><?= date('d.m', strtotime($rj['entry_date'])) ?></td>
|
||||
<td>
|
||||
<a href="<?= url_for('journal_entry.php?id=' . $rj['id']) ?>" style="color:inherit;">
|
||||
<?= htmlspecialchars(mb_strimwidth($rj['description'], 0, 40, '...')) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);font-size:12px;"><?= number_format((float)$rj['amount'], 2, ',', '.') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($recent_journal)): ?>
|
||||
<tr><td colspan="3" style="color:var(--text-dim);">Keine Buchungen</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;">
|
||||
<a href="<?= url_for('journal.php') ?>" style="font-size:10px;">Alle Buchungen →</a>
|
||||
<?php if ($journal_year_id): ?>
|
||||
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" class="button" style="font-size:11px;padding:3px 10px;">+ Neue Buchung</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- GoBD PDF-Status -->
|
||||
<?php if ($gobd_has_problems): ?>
|
||||
<div class="gobd-warning">
|
||||
<h3>GoBD-Warnung: PDF-Archivierung unvollständig</h3>
|
||||
<ul>
|
||||
<?php if ($gobd_status['unarchived'] > 0): ?>
|
||||
<li><strong><?= $gobd_status['unarchived'] ?></strong> Rechnung(en) ohne archivierte PDF</li>
|
||||
<?php endif; ?>
|
||||
<?php if ($gobd_status['missing_files'] > 0): ?>
|
||||
<li style="color:var(--error);"><strong><?= $gobd_status['missing_files'] ?></strong> PDF-Datei(en) fehlen auf dem Server</li>
|
||||
<?php endif; ?>
|
||||
<?php if ($gobd_status['invalid'] > 0): ?>
|
||||
<li style="color:var(--error);"><strong><?= $gobd_status['invalid'] ?></strong> PDF(s) mit fehlgeschlagener Integritätsprüfung</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<?php if (!empty($gobd_status['problems'])): ?>
|
||||
<details>
|
||||
<summary>Betroffene Rechnungen anzeigen</summary>
|
||||
<table class="list">
|
||||
<thead><tr><th>Rechnungsnr.</th><th>Problem</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach (array_slice($gobd_status['problems'], 0, 10) as $prob): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($prob['invoice_number']) ?></td>
|
||||
<td><?= htmlspecialchars($prob['message']) ?></td>
|
||||
<td><a href="<?= url_for('invoice_pdf.php?id=' . $prob['id']) ?>">PDF neu generieren</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (count($gobd_status['problems']) > 10): ?>
|
||||
<tr><td colspan="3"><em>... und <?= count($gobd_status['problems']) - 10 ?> weitere</em></td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($gobd_status['migration_needed'])): ?>
|
||||
<p style="margin-top:4px;margin-bottom:0;">
|
||||
<strong>Datenbank-Migration erforderlich:</strong><br>
|
||||
1. <code>sudo -u postgres psql -d pirp -f /var/www/pirp/tools/migrate_pdf.sql</code><br>
|
||||
2. <code>php /var/www/pirp/tools/run_migration.php</code>
|
||||
</p>
|
||||
<?php elseif ($gobd_status['unarchived'] > 0): ?>
|
||||
<p style="margin-top:4px;margin-bottom:0;">
|
||||
<strong>Migration:</strong> <code>php tools/run_migration.php</code>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="gobd-ok">
|
||||
<strong>GoBD OK:</strong>
|
||||
<?= $gobd_status['archived'] ?>/<?= $gobd_status['total_invoices'] ?> archiviert.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section>
|
||||
<h2>Schnellzugriff</h2>
|
||||
<div class="flex-row" style="gap:8px;flex-wrap:wrap;">
|
||||
<a href="<?= url_for('invoice_new.php') ?>" class="button">Neue Rechnung</a>
|
||||
<a href="<?= url_for('expenses.php') ?>" class="button secondary">Neue Ausgabe</a>
|
||||
<?php if ($journal_year_id): ?>
|
||||
<a href="<?= url_for('journal_entry.php?year_id=' . $journal_year_id . '&month=' . $month) ?>" class="button secondary">Neue Buchung</a>
|
||||
<?php endif; ?>
|
||||
<a href="<?= url_for('euer.php') ?>" class="button secondary">EÜR</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
224
pirp/public/invoice_new.php
Normal file
224
pirp/public/invoice_new.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/invoice_functions.php';
|
||||
require_once __DIR__ . '/../src/customer_functions.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$pdo = get_db();
|
||||
$settings = get_settings();
|
||||
$customers = get_customers();
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$customer_id = (int)($_POST['customer_id'] ?? 0);
|
||||
$invoice_date = $_POST['invoice_date'] ?: date('Y-m-d');
|
||||
$service_date = $_POST['service_date'] ?: null;
|
||||
$notes_internal = $_POST['notes_internal'] ?? '';
|
||||
|
||||
if ($customer_id <= 0) {
|
||||
$error = 'Bitte einen Kunden auswählen.';
|
||||
} else {
|
||||
$vat_mode = $settings['vat_mode'] ?? 'klein';
|
||||
$vat_rate = (float)($settings['default_vat_rate'] ?? 19.0);
|
||||
|
||||
$items = [];
|
||||
$total_net = 0.0;
|
||||
$count = isset($_POST['item_desc']) ? count($_POST['item_desc']) : 0;
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$desc = trim($_POST['item_desc'][$i] ?? '');
|
||||
$qty = (float)($_POST['item_qty'][$i] ?? 0);
|
||||
$price= (float)($_POST['item_price'][$i] ?? 0);
|
||||
if ($desc !== '' && $qty > 0 && $price >= 0) {
|
||||
$line_net = $qty * $price;
|
||||
$total_net += $line_net;
|
||||
$items[] = [
|
||||
'position_no' => count($items) + 1,
|
||||
'description' => $desc,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $price,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($items)) {
|
||||
$error = 'Bitte mindestens eine Position ausfüllen.';
|
||||
} else {
|
||||
if ($vat_mode === 'normal') {
|
||||
$total_vat = round($total_net * $vat_rate / 100, 2);
|
||||
} else {
|
||||
$total_vat = 0.0;
|
||||
}
|
||||
$total_gross = $total_net + $total_vat;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$invoice_number = generate_invoice_number();
|
||||
$stmt = $pdo->prepare("INSERT INTO invoices
|
||||
(invoice_number, customer_id, invoice_date, service_date, vat_mode, vat_rate,
|
||||
payment_terms, notes_internal, total_net, total_vat, total_gross, paid)
|
||||
VALUES (:in, :cid, :idate, :sdate, :vm, :vr, :pt, :ni, :tn, :tv, :tg, FALSE)
|
||||
RETURNING id");
|
||||
$stmt->execute([
|
||||
':in' => $invoice_number,
|
||||
':cid' => $customer_id,
|
||||
':idate'=> $invoice_date,
|
||||
':sdate'=> $service_date,
|
||||
':vm' => $vat_mode,
|
||||
':vr' => $vat_rate,
|
||||
':pt' => $settings['payment_terms'] ?? null,
|
||||
':ni' => $notes_internal,
|
||||
':tn' => $total_net,
|
||||
':tv' => $total_vat,
|
||||
':tg' => $total_gross,
|
||||
]);
|
||||
$invoice_id = $stmt->fetchColumn();
|
||||
|
||||
$stmtItem = $pdo->prepare("INSERT INTO invoice_items
|
||||
(invoice_id, position_no, description, quantity, unit_price, vat_rate)
|
||||
VALUES (:iid, :pn, :d, :q, :up, :vr)");
|
||||
foreach ($items as $it) {
|
||||
$stmtItem->execute([
|
||||
':iid' => $invoice_id,
|
||||
':pn' => $it['position_no'],
|
||||
':d' => $it['description'],
|
||||
':q' => $it['quantity'],
|
||||
':up' => $it['unit_price'],
|
||||
':vr' => $vat_rate,
|
||||
]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// PDF sofort archivieren (GoBD-konform)
|
||||
archive_invoice_pdf($invoice_id);
|
||||
|
||||
header('Location: ' . url_for('invoice_pdf.php?id=' . $invoice_id));
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
$error = 'Fehler beim Speichern der Rechnung: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Neue Rechnung</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<script>
|
||||
function addRow() {
|
||||
const tbody = document.getElementById('items-body');
|
||||
const index = tbody.children.length;
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${index+1}</td>
|
||||
<td><input type="text" name="item_desc[${index}]" size="40"></td>
|
||||
<td><input type="number" step="0.01" name="item_qty[${index}]" value="1"></td>
|
||||
<td><input type="number" step="0.01" name="item_price[${index}]" value="0.00"></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Enter springt zum nächsten Feld statt Formular abzuschicken
|
||||
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, 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') ?>" 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') ?>">Übersicht</a>
|
||||
<a href="<?= url_for('invoice_new.php') ?>" class="active">Neue Rechnung</a>
|
||||
<a href="<?= url_for('recurring.php') ?>">Abo-Rechnungen</a>
|
||||
</div>
|
||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<label>Kunde:
|
||||
<select name="customer_id" required>
|
||||
<option value="">-- wählen --</option>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="flex-row">
|
||||
<label>Rechnungsdatum:
|
||||
<input type="date" name="invoice_date" value="<?= htmlspecialchars(date('Y-m-d')) ?>">
|
||||
</label>
|
||||
<label>Leistungsdatum:
|
||||
<input type="date" name="service_date">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h2>Positionen</h2>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pos.</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Menge</th>
|
||||
<th>Einzelpreis (netto)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-body">
|
||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||
<tr>
|
||||
<td><?= $i+1 ?></td>
|
||||
<td><input type="text" name="item_desc[<?= $i ?>]" size="40"></td>
|
||||
<td><input type="number" step="0.01" name="item_qty[<?= $i ?>]" value="1"></td>
|
||||
<td><input type="number" step="0.01" name="item_price[<?= $i ?>]" value="0.00"></td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" onclick="addRow()">Position hinzufügen</button>
|
||||
|
||||
<label>Interne Notizen (nicht auf Rechnung):
|
||||
<textarea name="notes_internal" rows="3"></textarea>
|
||||
</label>
|
||||
|
||||
<button type="submit">Rechnung speichern & PDF anzeigen</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
85
pirp/public/invoice_pdf.php
Normal file
85
pirp/public/invoice_pdf.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* GoBD-konforme PDF-Auslieferung
|
||||
*
|
||||
* Diese Datei liefert archivierte, unveränderliche Rechnungs-PDFs aus.
|
||||
* Bei erstmaligem Aufruf wird die PDF generiert und permanent gespeichert.
|
||||
*/
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_once __DIR__ . '/../src/pdf_functions.php';
|
||||
require_login();
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
die('Ungültige Rechnungs-ID.');
|
||||
}
|
||||
|
||||
// Prüfe ob archivierte PDF existiert
|
||||
$pdfPath = get_archived_pdf_path($id);
|
||||
|
||||
if (!$pdfPath) {
|
||||
// Noch keine archivierte PDF - jetzt erstellen
|
||||
$pdfPath = archive_invoice_pdf($id);
|
||||
|
||||
if (!$pdfPath) {
|
||||
// Diagnose: Warum konnte die PDF nicht erstellt werden?
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT i.id, i.invoice_number, i.customer_id, c.id AS cust_exists
|
||||
FROM invoices i
|
||||
LEFT JOIN customers c ON c.id = i.customer_id
|
||||
WHERE i.id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$diag = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$diag) {
|
||||
die('Fehler: Rechnung nicht gefunden (ID: ' . $id . ')');
|
||||
}
|
||||
if (!$diag['cust_exists']) {
|
||||
die('Fehler: Kunde zur Rechnung ' . htmlspecialchars($diag['invoice_number']) . ' existiert nicht mehr.');
|
||||
}
|
||||
|
||||
// Prüfe Verzeichnis-Berechtigungen
|
||||
$uploadBase = __DIR__ . '/uploads/invoices';
|
||||
if (!is_dir($uploadBase)) {
|
||||
die('Fehler: Upload-Verzeichnis fehlt (' . $uploadBase . '). Bitte erstellen mit: mkdir -p ' . $uploadBase);
|
||||
}
|
||||
if (!is_writable($uploadBase)) {
|
||||
die('Fehler: Upload-Verzeichnis nicht beschreibbar (' . $uploadBase . ')');
|
||||
}
|
||||
|
||||
die('Fehler beim Generieren der PDF. Bitte Logs prüfen.');
|
||||
}
|
||||
}
|
||||
|
||||
// Vollständiger Dateipfad
|
||||
$fullPath = __DIR__ . '/' . $pdfPath;
|
||||
|
||||
if (!file_exists($fullPath) || !is_readable($fullPath)) {
|
||||
die('PDF-Datei nicht gefunden.');
|
||||
}
|
||||
|
||||
// Integritätsprüfung
|
||||
$isValid = verify_invoice_pdf($id);
|
||||
if ($isValid === false) {
|
||||
// WARNUNG: PDF wurde möglicherweise manipuliert!
|
||||
error_log("WARNUNG: Integritätsprüfung fehlgeschlagen für Rechnung ID $id");
|
||||
// Optional: Warnung anzeigen statt die PDF auszuliefern
|
||||
// die('PDF-Integritätsfehler! Die Datei wurde möglicherweise manipuliert.');
|
||||
}
|
||||
|
||||
// Rechnungsnummer für Dateinamen holen
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT invoice_number FROM invoices WHERE id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$invoiceNumber = $stmt->fetchColumn() ?: 'Rechnung';
|
||||
|
||||
// PDF ausliefern
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="Rechnung-' . $invoiceNumber . '.pdf"');
|
||||
header('Content-Length: ' . filesize($fullPath));
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
readfile($fullPath);
|
||||
exit;
|
||||
129
pirp/public/invoice_storno.php
Normal file
129
pirp/public/invoice_storno.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?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/pdf_functions.php';
|
||||
require_once __DIR__ . '/../src/journal_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if (!$id) {
|
||||
header('Location: ' . url_for('invoices.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$inv = get_invoice_with_customer($id);
|
||||
if (!$inv) {
|
||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Rechnung nicht gefunden.')));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Bereits storniert?
|
||||
if (!empty($inv['is_storno'])) {
|
||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Diese Rechnung ist selbst eine Stornorechnung.')));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prüfe ob schon Storno existiert
|
||||
$pdo = get_db();
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT id FROM invoices WHERE storno_of = :id LIMIT 1");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$existing_storno = $stmt->fetchColumn();
|
||||
} catch (\PDOException $e) {
|
||||
$existing_storno = false;
|
||||
}
|
||||
|
||||
if ($existing_storno) {
|
||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Für diese Rechnung existiert bereits eine Stornorechnung.')));
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$storno_id = create_storno_invoice($id);
|
||||
archive_invoice_pdf($storno_id);
|
||||
|
||||
// Journalbuchung stornieren falls vorhanden
|
||||
$original_entry = get_journal_entry_for_invoice($id);
|
||||
if ($original_entry) {
|
||||
create_storno_journal_entry($id, $storno_id);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT invoice_number FROM invoices WHERE id = :id");
|
||||
$stmt->execute([':id' => $storno_id]);
|
||||
$storno_number = $stmt->fetchColumn();
|
||||
|
||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Stornorechnung ' . $storno_number . ' erstellt.')));
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Storno – <?= htmlspecialchars($inv['invoice_number']) ?></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') ?>">← Zurück zu Rechnungen</a>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<p class="error"><?= htmlspecialchars($error) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<section>
|
||||
<h2>Stornorechnung erstellen</h2>
|
||||
<div style="max-width:520px;">
|
||||
<table class="list" style="margin-bottom:16px;">
|
||||
<tr><td style="color:var(--text-muted);width:140px;">Rechnungsnummer</td><td><strong><?= htmlspecialchars($inv['invoice_number']) ?></strong></td></tr>
|
||||
<tr><td style="color:var(--text-muted);">Kunde</td><td><?= htmlspecialchars($inv['customer_name']) ?></td></tr>
|
||||
<tr><td style="color:var(--text-muted);">Datum</td><td><?= date('d.m.Y', strtotime($inv['invoice_date'])) ?></td></tr>
|
||||
<tr><td style="color:var(--text-muted);">Betrag</td><td><?= number_format($inv['total_gross'], 2, ',', '.') ?> €</td></tr>
|
||||
<tr><td style="color:var(--text-muted);">Status</td><td><?= $inv['paid'] ? 'bezahlt' : 'offen' ?></td></tr>
|
||||
</table>
|
||||
|
||||
<div class="gobd-warning" style="margin-bottom:16px;">
|
||||
<strong>Achtung:</strong> Es wird eine neue Rechnung mit negativen Beträgen (Stornorechnung) erstellt.
|
||||
<?php if ($inv['paid']): ?>
|
||||
Die bestehende Journalbuchung wird automatisch storniert (Gegenbuchung).
|
||||
<?php else: ?>
|
||||
Die Rechnung ist noch nicht bezahlt – es wird keine Journalbuchung storniert.
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<button type="submit" class="button-danger">Stornorechnung erstellen</button>
|
||||
<a href="<?= url_for('invoices.php') ?>" style="margin-left:12px;">Abbrechen</a>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
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>
|
||||
60
pirp/public/invoices_csv.php
Normal file
60
pirp/public/invoices_csv.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_login();
|
||||
|
||||
$pdo = get_db();
|
||||
|
||||
$filter_number = trim($_GET['number'] ?? '');
|
||||
$filter_customer = trim($_GET['customer'] ?? '');
|
||||
$filter_from = trim($_GET['from'] ?? '');
|
||||
$filter_to = trim($_GET['to'] ?? '');
|
||||
|
||||
$sql = "SELECT i.*, c.name AS customer_name
|
||||
FROM invoices i
|
||||
JOIN customers c ON c.id = i.customer_id
|
||||
WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($filter_number !== '') {
|
||||
$sql .= " AND i.invoice_number ILIKE :num";
|
||||
$params[':num'] = '%' . $filter_number . '%';
|
||||
}
|
||||
if ($filter_customer !== '') {
|
||||
$sql .= " AND c.name ILIKE :cust";
|
||||
$params[':cust'] = '%' . $filter_customer . '%';
|
||||
}
|
||||
if ($filter_from !== '') {
|
||||
$sql .= " AND i.invoice_date >= :from";
|
||||
$params[':from'] = $filter_from;
|
||||
}
|
||||
if ($filter_to !== '') {
|
||||
$sql .= " AND i.invoice_date <= :to";
|
||||
$params[':to'] = $filter_to;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY i.invoice_date ASC, i.id ASC";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="invoices_export_' . date('Y-m-d') . '.csv"');
|
||||
|
||||
$out = fopen('php://output', 'w');
|
||||
fputcsv($out, ['Datum', 'Rechnungsnummer', 'Kunde', 'Netto', 'USt', 'Brutto', 'Status'], ';');
|
||||
|
||||
foreach ($rows as $r) {
|
||||
fputcsv($out, [
|
||||
date('d.m.Y', strtotime($r['invoice_date'])),
|
||||
$r['invoice_number'],
|
||||
$r['customer_name'],
|
||||
number_format($r['total_net'], 2, ',', ''),
|
||||
number_format($r['total_vat'], 2, ',', ''),
|
||||
number_format($r['total_gross'], 2, ',', ''),
|
||||
$r['paid'] ? 'bezahlt' : 'offen',
|
||||
], ';');
|
||||
}
|
||||
fclose($out);
|
||||
exit;
|
||||
559
pirp/public/journal.php
Normal file
559
pirp/public/journal.php
Normal file
@@ -0,0 +1,559 @@
|
||||
<?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 = '';
|
||||
|
||||
// ---- Aktionen ----
|
||||
if (isset($_GET['delete'])) {
|
||||
$del_id = (int)$_GET['delete'];
|
||||
delete_journal_entry($del_id);
|
||||
$msg = 'Buchung gelöscht.';
|
||||
}
|
||||
|
||||
// Umsatz-Zusammenfassung speichern
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form'] ?? '') === 'summary_values') {
|
||||
$sv_year_id = (int)$_POST['year_id'];
|
||||
$sv_month = (int)$_POST['month'];
|
||||
$values = $_POST['summary_val'] ?? [];
|
||||
save_journal_monthly_summary_values($sv_year_id, $sv_month, $values);
|
||||
$msg = 'Umsatzübersicht gespeichert.';
|
||||
}
|
||||
|
||||
// ---- Jahr und Monat bestimmen ----
|
||||
$years = get_journal_years();
|
||||
|
||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : null;
|
||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('n');
|
||||
|
||||
if (!$year_id && $years) {
|
||||
$current_cal_year = (int)date('Y');
|
||||
foreach ($years as $y) {
|
||||
if ($y['year'] == $current_cal_year) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Daten laden ----
|
||||
$columns = [];
|
||||
$entries = [];
|
||||
$totals = [];
|
||||
$summary_items = [];
|
||||
$summary_values = [];
|
||||
|
||||
if ($year_id) {
|
||||
$columns = get_journal_columns();
|
||||
$entries = get_journal_entries($year_id, $month);
|
||||
$totals = calculate_monthly_totals($year_id, $month);
|
||||
$summary_items = get_journal_summary_items(true);
|
||||
$summary_values_raw = get_journal_monthly_summary_values($year_id, $month);
|
||||
foreach ($summary_values_raw as $sv) {
|
||||
$summary_values[$sv['summary_item_id']] = $sv['amount'];
|
||||
}
|
||||
}
|
||||
|
||||
// Spaltengruppen für colspan
|
||||
$groups = [];
|
||||
foreach ($columns as $col) {
|
||||
$g = $col['group'];
|
||||
if (!isset($groups[$g])) $groups[$g] = 0;
|
||||
$groups[$g]++;
|
||||
}
|
||||
|
||||
// Konto-Optionen als JSON
|
||||
$acct_options_json = json_encode(get_journal_account_options());
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Journal <?= $current_year ? (int)$current_year['year'] : '' ?> - <?= journal_month_name_full($month) ?></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') ?>"><?= 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') ?>" class="active">Monatsansicht</a>
|
||||
<a href="<?= url_for('journal_summary.php?year_id=' . $year_id) ?>">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: ?>
|
||||
|
||||
<!-- Jahr-Dropdown + Monatstabs -->
|
||||
<div class="journal-controls">
|
||||
<form method="get" class="journal-year-select">
|
||||
<label style="margin:0;font-size:10px;">Jahr:
|
||||
<select name="year_id" onchange="this.form.submit();" style="width:auto;min-width:70px;">
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?>>
|
||||
<?= (int)$y['year'] ?><?= $y['is_closed'] ? ' (geschl.)' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<input type="hidden" name="month" value="<?= $month ?>">
|
||||
</form>
|
||||
|
||||
<div class="journal-month-tabs">
|
||||
<?php for ($m = 1; $m <= 12; $m++): ?>
|
||||
<a href="<?= url_for('journal.php?year_id=' . $year_id . '&month=' . $m) ?>"
|
||||
class="<?= $m === $month ? 'active' : '' ?>">
|
||||
<?= journal_month_name($m) ?>
|
||||
</a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monatssummen -->
|
||||
<div class="journal-month-summary">
|
||||
<span class="journal-soll">S: <?= journal_format_amount($totals['_soll'] ?? 0) ?></span>
|
||||
<span class="journal-haben">H: <?= journal_format_amount($totals['_haben'] ?? 0) ?></span>
|
||||
<?php
|
||||
$diff = ($totals['_soll'] ?? 0) - ($totals['_haben'] ?? 0);
|
||||
if (abs($diff) > 0.005):
|
||||
?>
|
||||
<span class="journal-diff-warning">Diff: <?= journal_format_amount($diff) ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="journal-sep"></span>
|
||||
<?php
|
||||
$kasse_balance = ($totals['kasse_s'] ?? 0) - ($totals['kasse_h'] ?? 0);
|
||||
$bank_balance = ($totals['bank_s'] ?? 0) - ($totals['bank_h'] ?? 0);
|
||||
?>
|
||||
<span class="journal-kasse">Kasse: <?= journal_format_amount($kasse_balance) ?></span>
|
||||
<span class="journal-bank">Bank: <?= journal_format_amount($bank_balance) ?></span>
|
||||
<span style="color:var(--text-dim);margin-left:auto;"><?= count($entries) ?> Buchungen</span>
|
||||
</div>
|
||||
|
||||
<?php if (isset($diff) && abs($diff) > 0.005): ?>
|
||||
<div class="journal-diff-banner">
|
||||
<strong>⚠ Buchung nicht ausgeglichen</strong>
|
||||
— Differenz: <strong><?= journal_format_amount($diff) ?></strong>
|
||||
<span style="margin-left:10px;font-size:10px;opacity:0.65;">Soll: <?= journal_format_amount($totals['_soll'] ?? 0) ?> · Haben: <?= journal_format_amount($totals['_haben'] ?? 0) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Journal-Tabelle -->
|
||||
<div class="journal-table-wrap">
|
||||
<table class="journal-table" id="journal-table">
|
||||
<thead>
|
||||
<!-- Gruppenheader -->
|
||||
<tr class="journal-group-header">
|
||||
<th></th>
|
||||
<th colspan="4"></th>
|
||||
<?php foreach ($groups as $gname => $gcount): ?>
|
||||
<th colspan="<?= $gcount ?>"><?= htmlspecialchars($gname) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<!-- Spaltenheader -->
|
||||
<tr>
|
||||
<th class="journal-col-action"></th>
|
||||
<th class="journal-col-date">Tag</th>
|
||||
<th class="journal-col-att">B</th>
|
||||
<th class="journal-col-text">Text</th>
|
||||
<th class="journal-col-betrag">Betrag</th>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<th class="journal-col-amount journal-<?= $col['side'] ?>"><?= htmlspecialchars($col['label']) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<!-- Monatssummen oben (wie Excel Zeile 2+3) -->
|
||||
<tr class="journal-top-sums">
|
||||
<td></td>
|
||||
<td colspan="4" style="text-align:right;"><strong>S</strong></td>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<?php if ($col['side'] === 'soll'): ?>
|
||||
<strong><?= journal_format_amount($totals[$col['key']] ?? 0) ?></strong>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<tr class="journal-top-sums">
|
||||
<td></td>
|
||||
<td colspan="4" style="text-align:right;"><strong>H</strong></td>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<?php if ($col['side'] === 'haben'): ?>
|
||||
<strong><?= journal_format_amount($totals[$col['key']] ?? 0) ?></strong>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="journal-body">
|
||||
<?php if (empty($entries)): ?>
|
||||
<tr id="empty-row"><td colspan="<?= 6 + count($columns) ?>" style="color:var(--text-dim);padding:6px;">Keine Buchungen.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($entries as $entry_row): ?>
|
||||
<tr data-id="<?= $entry_row['id'] ?>">
|
||||
<td class="journal-col-action">
|
||||
<a href="<?= url_for('journal_entry.php?id=' . $entry_row['id']) ?>" title="Bearbeiten">B</a>
|
||||
<a href="#" onclick="deleteEntry(<?= $entry_row['id'] ?>);return false;" title="Löschen">X</a>
|
||||
<a href="#" onclick="duplicateEntry(<?= $entry_row['id'] ?>);return false;" title="Duplizieren">D</a>
|
||||
</td>
|
||||
<td class="journal-col-date"><?= htmlspecialchars(date('d.m', strtotime($entry_row['entry_date']))) ?></td>
|
||||
<td class="journal-col-att"><?= htmlspecialchars($entry_row['attachment_note'] ?? '') ?></td>
|
||||
<td class="journal-col-text">
|
||||
<?= htmlspecialchars($entry_row['description']) ?>
|
||||
<?php if (($entry_row['source_type'] ?? 'manual') === 'invoice_payment' && !empty($entry_row['invoice_id'])): ?>
|
||||
<a href="<?= url_for('invoice_pdf.php?id=' . $entry_row['invoice_id']) ?>" target="_blank" class="source-badge source-invoice" title="Automatisch aus Rechnungszahlung">RE</a>
|
||||
<?php elseif (($entry_row['source_type'] ?? 'manual') === 'expense_payment' && !empty($entry_row['expense_id'])): ?>
|
||||
<a href="<?= url_for('expenses.php?action=edit&id=' . $entry_row['expense_id']) ?>" class="source-badge source-expense" title="Automatisch aus Ausgabe">AU</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="journal-col-betrag"><?= journal_format_amount($entry_row['amount']) ?></td>
|
||||
<?php
|
||||
$row_amounts = [];
|
||||
if (!empty($entry_row['accounts'])) {
|
||||
foreach ($entry_row['accounts'] as $acct) {
|
||||
$key = map_account_to_column_key($acct, $columns);
|
||||
if ($key) {
|
||||
$row_amounts[$key] = ($row_amounts[$key] ?? 0) + (float)$acct['amount'];
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<?= isset($row_amounts[$col['key']]) ? journal_format_amount($row_amounts[$col['key']]) : '' ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="journal-totals-row">
|
||||
<td></td>
|
||||
<td colspan="4"><strong>Summen</strong></td>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<strong><?= journal_format_amount($totals[$col['key']] ?? 0) ?></strong>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Inline-Eingabe -->
|
||||
<div id="inline-editor" style="margin-bottom:6px;">
|
||||
<div style="display:flex;gap:4px;align-items:center;margin-bottom:3px;">
|
||||
<button type="button" onclick="toggleInlineForm()" id="btn-new">+ Neue Buchung</button>
|
||||
<a href="<?= url_for('journal_entry.php?year_id=' . $year_id . '&month=' . $month) ?>" style="font-size:10px;color:var(--text-dim);">Vollformular</a>
|
||||
</div>
|
||||
<div id="inline-form" style="display:none;">
|
||||
<form id="inline-entry-form" onsubmit="return saveInlineEntry(event);">
|
||||
<input type="hidden" name="year_id" value="<?= $year_id ?>">
|
||||
<input type="hidden" name="id" id="inline-id" value="">
|
||||
<div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:3px;">
|
||||
<?php
|
||||
$last_date = date('Y-m-d');
|
||||
if (!empty($entries)) {
|
||||
$last_entry = end($entries);
|
||||
$last_date = $last_entry['entry_date'];
|
||||
}
|
||||
?>
|
||||
<input type="date" name="entry_date" id="ie-date" value="<?= htmlspecialchars($last_date) ?>" required style="width:130px;">
|
||||
<input type="text" name="description" id="ie-desc" placeholder="Text" required style="flex:2;min-width:150px;">
|
||||
<input type="text" name="attachment_note" id="ie-att" placeholder="Beleg" style="width:60px;">
|
||||
<input type="number" step="0.01" name="amount" id="ie-amount" placeholder="Betrag" style="width:80px;">
|
||||
</div>
|
||||
<div class="journal-inline-accounts" id="ie-accounts">
|
||||
<table>
|
||||
<thead><tr><th>Konto</th><th style="width:20px;">S/H</th><th style="width:80px;">Betrag</th><th style="width:80px;">Notiz</th><th style="width:20px;"></th></tr></thead>
|
||||
<tbody id="ie-acct-body"></tbody>
|
||||
</table>
|
||||
<button type="button" onclick="addInlineAccountRow('','soll','','','','',true);" style="padding:1px 6px;font-size:9px;margin-top:2px;">+Konto</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;margin-top:3px;">
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" onclick="saveAndNew();" class="secondary">Speichern+Neu</button>
|
||||
<button type="button" onclick="cancelInline();" class="secondary">Abbrechen</button>
|
||||
<span id="inline-status" style="font-size:10px;color:var(--text-dim);align-self:center;"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Umsatzübersicht -->
|
||||
<?php if ($summary_items): ?>
|
||||
<section>
|
||||
<h2>Umsatzübersicht <?= journal_month_name_full($month) ?></h2>
|
||||
<div>
|
||||
<?php
|
||||
$er_cats = get_journal_revenue_categories('erloese', true);
|
||||
foreach ($er_cats as $cat):
|
||||
$cat_key = 'er_' . $cat['id'];
|
||||
$cat_total = $totals[$cat_key] ?? 0;
|
||||
?>
|
||||
<span style="margin-right:12px;font-size:11px;">
|
||||
<?= htmlspecialchars($cat['name']) ?>: <strong><?= journal_format_amount($cat_total) ?></strong>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<form method="post" style="margin-top:4px;">
|
||||
<input type="hidden" name="form" value="summary_values">
|
||||
<input type="hidden" name="year_id" value="<?= $year_id ?>">
|
||||
<input type="hidden" name="month" value="<?= $month ?>">
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:flex-end;">
|
||||
<?php foreach ($summary_items as $item): ?>
|
||||
<label style="margin:0;font-size:10px;"><?= htmlspecialchars($item['name']) ?>:
|
||||
<input type="number" step="0.01" name="summary_val[<?= $item['id'] ?>]"
|
||||
value="<?= htmlspecialchars($summary_values[$item['id']] ?? '0.00') ?>"
|
||||
style="max-width:80px;">
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
<button type="submit" style="padding:3px 8px;">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<script src="assets/combobox.js"></script>
|
||||
<script>
|
||||
const accountOptions = <?= $acct_options_json ?>;
|
||||
const yearId = <?= $year_id ?: 0 ?>;
|
||||
const currentMonth = <?= $month ?>;
|
||||
const lastEntryDate = '<?= $last_date ?>';
|
||||
|
||||
function addInlineAccountRow(type, side, amount, revId, expId, note, autoFocus) {
|
||||
type = type || ''; side = side || 'soll'; amount = amount || '';
|
||||
revId = revId || ''; expId = expId || ''; note = note || '';
|
||||
|
||||
const tbody = document.getElementById('ie-acct-body');
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML =
|
||||
'<td><div class="pirp-combobox-wrap"></div>' +
|
||||
'<input type="hidden" name="acct_type[]" class="h-type" value="' + type + '">' +
|
||||
'<input type="hidden" name="acct_side[]" class="h-side" value="' + side + '">' +
|
||||
'<input type="hidden" name="acct_rev_id[]" class="h-rev" value="' + revId + '">' +
|
||||
'<input type="hidden" name="acct_exp_id[]" class="h-exp" value="' + expId + '">' +
|
||||
'</td>' +
|
||||
'<td class="side-label" style="text-align:center;font-weight:700;font-size:10px;">' +
|
||||
(side === 'haben' ? 'H' : (type ? 'S' : '')) + '</td>' +
|
||||
'<td><input type="number" step="0.01" name="acct_amount[]" value="' + amount + '" style="width:70px;"></td>' +
|
||||
'<td><input type="text" name="acct_note[]" value="' + note + '" style="width:70px;"></td>' +
|
||||
'<td><a href="#" onclick="this.closest(\'tr\').remove();return false;" style="color:var(--error);font-weight:700;font-size:10px;">X</a></td>';
|
||||
tbody.appendChild(tr);
|
||||
|
||||
const wrap = tr.querySelector('.pirp-combobox-wrap');
|
||||
const selectedValue = type ? (type + '|' + side + '|' + revId + '|' + expId) : '';
|
||||
var cb = new PirpCombobox(wrap, accountOptions, {
|
||||
placeholder: '--Konto--',
|
||||
selectedValue: selectedValue,
|
||||
onSelect: function(opt) {
|
||||
tr.querySelector('.h-type').value = opt.value;
|
||||
tr.querySelector('.h-side').value = opt.side;
|
||||
tr.querySelector('.h-rev').value = opt.rev_id;
|
||||
tr.querySelector('.h-exp').value = opt.exp_id;
|
||||
tr.querySelector('.side-label').textContent = opt.side === 'haben' ? 'H' : 'S';
|
||||
}
|
||||
});
|
||||
if (!type && autoFocus) cb.input.focus();
|
||||
}
|
||||
|
||||
function toggleInlineForm() {
|
||||
const form = document.getElementById('inline-form');
|
||||
const tableWrap = document.querySelector('.journal-table-wrap');
|
||||
if (form.style.display === 'none') {
|
||||
form.style.display = 'block';
|
||||
resetInlineForm();
|
||||
// Formhöhe messen und Tabelle entsprechend verkleinern
|
||||
const formHeight = form.offsetHeight;
|
||||
tableWrap.style.maxHeight = 'calc(70vh - ' + formHeight + 'px)';
|
||||
document.getElementById('ie-date').focus();
|
||||
} else {
|
||||
form.style.display = 'none';
|
||||
tableWrap.style.maxHeight = '70vh';
|
||||
}
|
||||
}
|
||||
|
||||
function resetInlineForm() {
|
||||
document.getElementById('inline-id').value = '';
|
||||
document.getElementById('ie-date').value = lastEntryDate;
|
||||
document.getElementById('ie-desc').value = '';
|
||||
document.getElementById('ie-att').value = '';
|
||||
document.getElementById('ie-amount').value = '';
|
||||
document.getElementById('ie-acct-body').innerHTML = '';
|
||||
addInlineAccountRow();
|
||||
addInlineAccountRow();
|
||||
document.getElementById('inline-status').textContent = '';
|
||||
}
|
||||
|
||||
function cancelInline() {
|
||||
document.getElementById('inline-form').style.display = 'none';
|
||||
document.querySelector('.journal-table-wrap').style.maxHeight = '70vh';
|
||||
}
|
||||
|
||||
function saveInlineEntry(e) {
|
||||
if (e) e.preventDefault();
|
||||
const form = document.getElementById('inline-entry-form');
|
||||
const fd = new FormData(form);
|
||||
fd.append('action', 'save_entry');
|
||||
|
||||
document.getElementById('inline-status').textContent = 'Speichert...';
|
||||
|
||||
fetch('<?= url_for("journal_api.php") ?>', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
document.getElementById('inline-status').textContent = data.error || 'Fehler';
|
||||
document.getElementById('inline-status').style.color = 'var(--error)';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('inline-status').textContent = 'Netzwerkfehler';
|
||||
document.getElementById('inline-status').style.color = 'var(--error)';
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function saveAndNew() {
|
||||
const form = document.getElementById('inline-entry-form');
|
||||
const fd = new FormData(form);
|
||||
fd.append('action', 'save_entry');
|
||||
|
||||
document.getElementById('inline-status').textContent = 'Speichert...';
|
||||
|
||||
fetch('<?= url_for("journal_api.php") ?>', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
// Seite neu laden mit Parameter um Formular offen zu halten
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('new', '1');
|
||||
window.location.href = url.toString();
|
||||
} else {
|
||||
document.getElementById('inline-status').textContent = data.error || 'Fehler';
|
||||
document.getElementById('inline-status').style.color = 'var(--error)';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('inline-status').textContent = 'Netzwerkfehler';
|
||||
});
|
||||
}
|
||||
|
||||
function pirpConfirm(message, confirmLabel, confirmClass) {
|
||||
confirmLabel = confirmLabel || 'OK';
|
||||
confirmClass = confirmClass || 'danger';
|
||||
return new Promise(function(resolve) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'pirp-confirm-overlay';
|
||||
overlay.innerHTML =
|
||||
'<div class="pirp-confirm-box">' +
|
||||
'<p class="pirp-confirm-msg">' + message + '</p>' +
|
||||
'<div class="pirp-confirm-btns">' +
|
||||
'<button class="pirp-confirm-ok ' + confirmClass + '">' + confirmLabel + '</button>' +
|
||||
'<button class="pirp-confirm-cancel secondary">Abbrechen</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
function cleanup(result) {
|
||||
document.removeEventListener('keydown', onKey);
|
||||
overlay.remove();
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
overlay.querySelector('.pirp-confirm-ok').onclick = function() { cleanup(true); };
|
||||
overlay.querySelector('.pirp-confirm-cancel').onclick = function() { cleanup(false); };
|
||||
overlay.onclick = function(e) { if (e.target === overlay) cleanup(false); };
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === 'Enter') cleanup(true);
|
||||
if (e.key === 'Escape') cleanup(false);
|
||||
}
|
||||
document.addEventListener('keydown', onKey);
|
||||
overlay.querySelector('.pirp-confirm-cancel').focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteEntry(id) {
|
||||
const ok = await pirpConfirm('Buchung löschen?', 'Löschen', 'danger');
|
||||
if (!ok) return;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'delete_entry');
|
||||
fd.append('id', id);
|
||||
|
||||
fetch('<?= url_for("journal_api.php") ?>', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) location.reload();
|
||||
else alert(data.error || 'Fehler');
|
||||
});
|
||||
}
|
||||
|
||||
function duplicateEntry(id) {
|
||||
window.location = '<?= url_for("journal_entry.php") ?>?duplicate=' + id;
|
||||
}
|
||||
|
||||
// Sticky header: top-Werte dynamisch berechnen
|
||||
(function() {
|
||||
const thead = document.querySelector('#journal-table thead');
|
||||
if (!thead) return;
|
||||
const rows = thead.querySelectorAll('tr');
|
||||
let top = 0;
|
||||
rows.forEach(function(row) {
|
||||
row.querySelectorAll('th, td').forEach(function(cell) {
|
||||
cell.style.top = top + 'px';
|
||||
});
|
||||
top += row.offsetHeight;
|
||||
});
|
||||
})();
|
||||
|
||||
// Auto-open form wenn ?new=1 in URL (nach Speichern+Neu)
|
||||
if (new URLSearchParams(window.location.search).get('new') === '1') {
|
||||
// URL-Parameter entfernen ohne Reload
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('new');
|
||||
history.replaceState(null, '', url.toString());
|
||||
// Formular öffnen
|
||||
toggleInlineForm();
|
||||
}
|
||||
</script>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
81
pirp/public/journal_api.php
Normal file
81
pirp/public/journal_api.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'save_entry':
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$data = [
|
||||
'year_id' => (int)$_POST['year_id'],
|
||||
'entry_date' => $_POST['entry_date'] ?? '',
|
||||
'description' => $_POST['description'] ?? '',
|
||||
'attachment_note' => $_POST['attachment_note'] ?? '',
|
||||
'amount' => $_POST['amount'] ?? 0,
|
||||
'supplier_id' => !empty($_POST['supplier_id']) ? (int)$_POST['supplier_id'] : null,
|
||||
'sort_order' => (int)($_POST['sort_order'] ?? 0),
|
||||
];
|
||||
|
||||
if (!$data['entry_date'] || !$data['description']) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Datum und Text sind Pflichtfelder.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$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)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Mindestens eine Kontenbuchung erforderlich.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$saved_id = save_journal_entry($id, $data, $accounts);
|
||||
echo json_encode(['ok' => true, 'id' => $saved_id]);
|
||||
break;
|
||||
|
||||
case 'delete_entry':
|
||||
$del_id = (int)($_POST['id'] ?? 0);
|
||||
if ($del_id) {
|
||||
delete_journal_entry($del_id);
|
||||
echo json_encode(['ok' => true]);
|
||||
} else {
|
||||
echo json_encode(['ok' => false, 'error' => 'Keine ID.']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'get_entry':
|
||||
$get_id = (int)($_GET['id'] ?? 0);
|
||||
$entry = get_journal_entry($get_id);
|
||||
echo json_encode(['ok' => true, 'entry' => $entry]);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['ok' => false, 'error' => 'Unbekannte Aktion.']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
168
pirp/public/journal_detail.php
Normal file
168
pirp/public/journal_detail.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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();
|
||||
|
||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : 0;
|
||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : 0;
|
||||
$col_key = $_GET['col_key'] ?? '';
|
||||
|
||||
if (!$year_id || !$month || !$col_key) {
|
||||
header('Location: ' . url_for('journal_summary.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Jahr laden
|
||||
$years = get_journal_years();
|
||||
$current_year = null;
|
||||
foreach ($years as $y) {
|
||||
if ($y['id'] == $year_id) { $current_year = $y; break; }
|
||||
}
|
||||
|
||||
// Spalte finden
|
||||
$columns = get_journal_columns();
|
||||
$col = null;
|
||||
foreach ($columns as $c) {
|
||||
if ($c['key'] === $col_key) { $col = $c; break; }
|
||||
}
|
||||
|
||||
if (!$col) {
|
||||
header('Location: ' . url_for('journal_summary.php?year_id=' . $year_id));
|
||||
exit;
|
||||
}
|
||||
|
||||
// SQL aufbauen
|
||||
$pdo = get_db();
|
||||
|
||||
$where = "e.year_id = :year_id AND e.month = :month AND a.account_type = :account_type AND a.side = :side";
|
||||
$params = [
|
||||
':year_id' => $year_id,
|
||||
':month' => $month,
|
||||
':account_type' => $col['account_type'],
|
||||
':side' => $col['side'],
|
||||
];
|
||||
|
||||
if (($col['account_type'] === 'wareneingang' || $col['account_type'] === 'erloese') && $col['category_id']) {
|
||||
$where .= " AND a.revenue_category_id = :rev_cat_id";
|
||||
$params[':rev_cat_id'] = $col['category_id'];
|
||||
} elseif (($col['account_type'] === 'expense' || $col['account_type'] === 'deduction') && $col['category_id']) {
|
||||
$where .= " AND a.expense_category_id = :exp_cat_id";
|
||||
$params[':exp_cat_id'] = $col['category_id'];
|
||||
}
|
||||
|
||||
$sql = "SELECT e.id, e.entry_date, e.description, e.attachment_note, e.amount AS entry_amount,
|
||||
a.amount AS col_amount, a.note AS acct_note,
|
||||
s.name AS supplier_name
|
||||
FROM journal_entries e
|
||||
JOIN journal_entry_accounts a ON a.entry_id = e.id
|
||||
LEFT JOIN journal_suppliers s ON e.supplier_id = s.id
|
||||
WHERE $where
|
||||
ORDER BY e.entry_date, e.sort_order, e.id";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$total = array_sum(array_column($entries, 'col_amount'));
|
||||
|
||||
$month_name = journal_month_name_full($month);
|
||||
$year_val = $current_year ? (int)$current_year['year'] : $year_id;
|
||||
$back_url = url_for('journal_summary.php?year_id=' . $year_id);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= htmlspecialchars($col['label']) ?> – <?= $month_name ?> <?= $year_val ?></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') ?>"><?= 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?year_id=' . $year_id) ?>">Monatsansicht</a>
|
||||
<a href="<?= $back_url ?>">Jahressummen</a>
|
||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:8px;">
|
||||
<a href="<?= $back_url ?>" style="font-size:11px;color:var(--text-dim);">← Zurück zu Jahressummen</a>
|
||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<?= htmlspecialchars($col['label']) ?>
|
||||
<span style="font-weight:normal;font-size:13px;color:var(--text-dim);">
|
||||
– <?= $month_name ?> <?= $year_val ?>
|
||||
(<?= htmlspecialchars($col['group']) ?>, <?= $col['side'] === 'soll' ? 'Soll' : 'Haben' ?>)
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<?php if (empty($entries)): ?>
|
||||
<p style="color:var(--text-dim);">Keine Buchungen in diesem Zeitraum.</p>
|
||||
<?php else: ?>
|
||||
<div class="journal-table-wrap">
|
||||
<table class="journal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="journal-col-date">Datum</th>
|
||||
<th class="journal-col-att">B</th>
|
||||
<th class="journal-col-text">Text</th>
|
||||
<th class="journal-col-betrag">Buchungsbetrag</th>
|
||||
<th class="journal-col-amount journal-<?= $col['side'] ?>"><?= htmlspecialchars($col['label']) ?></th>
|
||||
<th class="journal-col-text" style="min-width:80px;">Notiz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($entries as $row): ?>
|
||||
<tr>
|
||||
<td class="journal-col-date"><?= htmlspecialchars(date('d.m.Y', strtotime($row['entry_date']))) ?></td>
|
||||
<td class="journal-col-att"><?= htmlspecialchars($row['attachment_note'] ?? '') ?></td>
|
||||
<td class="journal-col-text">
|
||||
<a href="<?= url_for('journal_entry.php?id=' . $row['id']) ?>" style="color:inherit;">
|
||||
<?= htmlspecialchars($row['description']) ?>
|
||||
</a>
|
||||
<?php if ($row['supplier_name']): ?>
|
||||
<span style="color:var(--text-dim);font-size:10px;"> · <?= htmlspecialchars($row['supplier_name']) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="journal-col-betrag"><?= journal_format_amount((float)$row['entry_amount']) ?></td>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>"><?= journal_format_amount((float)$row['col_amount']) ?></td>
|
||||
<td style="font-size:10px;color:var(--text-dim);"><?= htmlspecialchars($row['acct_note'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="journal-totals-row">
|
||||
<td colspan="4"><strong>Summe</strong></td>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>"><strong><?= journal_format_amount($total) ?></strong></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<p style="font-size:11px;color:var(--text-dim);margin-top:4px;"><?= count($entries) ?> Buchung(en)</p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
311
pirp/public/journal_entry.php
Normal file
311
pirp/public/journal_entry.php
Normal 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 & 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>
|
||||
20
pirp/public/journal_euer.php
Normal file
20
pirp/public/journal_euer.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// Redirect to new unified EÜR page
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/journal_functions.php';
|
||||
|
||||
// Get year from year_id parameter for backwards compatibility
|
||||
$year = date('Y');
|
||||
if (isset($_GET['year_id'])) {
|
||||
$year_id = (int)$_GET['year_id'];
|
||||
$years = get_journal_years();
|
||||
foreach ($years as $y) {
|
||||
if ($y['id'] == $year_id) {
|
||||
$year = (int)$y['year'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: ' . url_for('euer.php?year=' . $year));
|
||||
exit;
|
||||
159
pirp/public/journal_search.php
Normal file
159
pirp/public/journal_search.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?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();
|
||||
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
$from = trim($_GET['from'] ?? '');
|
||||
$to = trim($_GET['to'] ?? '');
|
||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : 0;
|
||||
|
||||
$years = get_journal_years();
|
||||
|
||||
$entries = [];
|
||||
$searched = false;
|
||||
|
||||
if ($q !== '' || $from !== '' || $to !== '' || $year_id) {
|
||||
$searched = true;
|
||||
$pdo = get_db();
|
||||
|
||||
$sql = "SELECT e.id, e.entry_date, e.month, e.description, e.attachment_note,
|
||||
e.amount, e.year_id, y.year AS journal_year,
|
||||
s.name AS supplier_name
|
||||
FROM journal_entries e
|
||||
JOIN journal_years y ON y.id = e.year_id
|
||||
LEFT JOIN journal_suppliers s ON s.id = e.supplier_id
|
||||
WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($q !== '') {
|
||||
$sql .= " AND (e.description ILIKE :q OR e.attachment_note ILIKE :q2)";
|
||||
$params[':q'] = '%' . $q . '%';
|
||||
$params[':q2'] = '%' . $q . '%';
|
||||
}
|
||||
if ($from !== '') {
|
||||
$sql .= " AND e.entry_date >= :from";
|
||||
$params[':from'] = $from;
|
||||
}
|
||||
if ($to !== '') {
|
||||
$sql .= " AND e.entry_date <= :to";
|
||||
$params[':to'] = $to;
|
||||
}
|
||||
if ($year_id) {
|
||||
$sql .= " AND e.year_id = :year_id";
|
||||
$params[':year_id'] = $year_id;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY e.entry_date DESC, e.id DESC LIMIT 200";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Journal-Suche</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') ?>"><?= 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') ?>" class="active">Suche</a>
|
||||
</div>
|
||||
|
||||
<form method="get" class="filters">
|
||||
<label>Suche:
|
||||
<input type="text" name="q" value="<?= htmlspecialchars($q) ?>" placeholder="Beschreibung, Beleg..." autofocus>
|
||||
</label>
|
||||
<label>Von:
|
||||
<input type="date" name="from" value="<?= htmlspecialchars($from) ?>">
|
||||
</label>
|
||||
<label>Bis:
|
||||
<input type="date" name="to" value="<?= htmlspecialchars($to) ?>">
|
||||
</label>
|
||||
<?php if ($years): ?>
|
||||
<label>Jahr:
|
||||
<select name="year_id">
|
||||
<option value="">Alle Jahre</option>
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?>><?= (int)$y['year'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
<button type="submit">Suchen</button>
|
||||
<a href="<?= url_for('journal_search.php') ?>">Zurücksetzen</a>
|
||||
</form>
|
||||
|
||||
<?php if ($searched): ?>
|
||||
<section>
|
||||
<h2>Suchergebnisse <?php if ($entries): ?><span style="font-weight:normal;font-size:12px;color:var(--text-muted);">(<?= count($entries) ?> Treffer<?= count($entries) >= 200 ? ', max. 200' : '' ?>)</span><?php endif; ?></h2>
|
||||
|
||||
<?php if (empty($entries)): ?>
|
||||
<p style="color:var(--text-muted);">Keine Buchungen gefunden.</p>
|
||||
<?php else: ?>
|
||||
<div class="journal-table-wrap">
|
||||
<table class="journal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="journal-col-date">Datum</th>
|
||||
<th class="journal-col-att">B</th>
|
||||
<th class="journal-col-text">Beschreibung</th>
|
||||
<th class="journal-col-betrag">Betrag</th>
|
||||
<th>Jahr / Monat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($entries as $e): ?>
|
||||
<tr>
|
||||
<td class="journal-col-date"><?= date('d.m.Y', strtotime($e['entry_date'])) ?></td>
|
||||
<td class="journal-col-att"><?= htmlspecialchars($e['attachment_note'] ?? '') ?></td>
|
||||
<td class="journal-col-text">
|
||||
<a href="<?= url_for('journal_entry.php?id=' . $e['id']) ?>" style="color:inherit;">
|
||||
<?= htmlspecialchars($e['description']) ?>
|
||||
</a>
|
||||
<?php if ($e['supplier_name']): ?>
|
||||
<span style="color:var(--text-dim);font-size:10px;"> · <?= htmlspecialchars($e['supplier_name']) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="journal-col-betrag"><?= number_format((float)$e['amount'], 2, ',', '.') ?></td>
|
||||
<td>
|
||||
<a href="<?= url_for('journal.php?year_id=' . $e['year_id'] . '&month=' . $e['month']) ?>"
|
||||
style="font-size:11px;color:var(--text-muted);">
|
||||
<?= (int)$e['journal_year'] ?> / <?= journal_month_name_full((int)$e['month']) ?>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
5
pirp/public/journal_settings.php
Normal file
5
pirp/public/journal_settings.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
// Redirect to main settings with journal tab
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
header('Location: ' . url_for('settings.php?tab=journal'));
|
||||
exit;
|
||||
248
pirp/public/journal_summary.php
Normal file
248
pirp/public/journal_summary.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/journal_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$years = get_journal_years();
|
||||
|
||||
$year_id = isset($_GET['year_id']) ? (int)$_GET['year_id'] : null;
|
||||
if (!$year_id && $years) {
|
||||
$current_cal_year = (int)date('Y');
|
||||
foreach ($years as $y) {
|
||||
if ($y['year'] == $current_cal_year) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
$summary = null;
|
||||
$profitability = null;
|
||||
if ($year_id) {
|
||||
$summary = calculate_yearly_summary($year_id);
|
||||
$profitability = calculate_yearly_profitability($year_id);
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Journal Jahresübersicht <?= $current_year ? (int)$current_year['year'] : '' ?></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') ?>"><?= 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?year_id=' . $year_id) ?>">Monatsansicht</a>
|
||||
<a href="<?= url_for('journal_summary.php') ?>" class="active">Jahressummen</a>
|
||||
<a href="<?= url_for('journal_search.php') ?>">Suche</a>
|
||||
</div>
|
||||
|
||||
<?php if (empty($years)): ?>
|
||||
<p class="warning">Bitte zuerst ein <a href="<?= url_for('settings.php?tab=journal') ?>">Jahr anlegen</a>.</p>
|
||||
<?php elseif ($summary): ?>
|
||||
|
||||
<!-- Jahr-Dropdown -->
|
||||
<div class="journal-controls">
|
||||
<form method="get" class="journal-year-select">
|
||||
<label>Jahr:
|
||||
<select name="year_id" onchange="this.form.submit();">
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= $y['id'] ?>" <?= $y['id'] == $year_id ? 'selected' : '' ?>>
|
||||
<?= (int)$y['year'] ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Spaltengruppen berechnen -->
|
||||
<?php
|
||||
$columns = $summary['columns'];
|
||||
$groups = [];
|
||||
foreach ($columns as $col) {
|
||||
$g = $col['group'];
|
||||
if (!isset($groups[$g])) $groups[$g] = 0;
|
||||
$groups[$g]++;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Jahresübersicht Tabelle -->
|
||||
<section>
|
||||
<h2>Monatssummen <?= $current_year ? (int)$current_year['year'] : '' ?></h2>
|
||||
<div class="journal-table-wrap">
|
||||
<table class="journal-table">
|
||||
<thead>
|
||||
<tr class="journal-group-header">
|
||||
<th colspan="2"></th>
|
||||
<th colspan="2">Summen</th>
|
||||
<?php foreach ($groups as $gname => $gcount): ?>
|
||||
<th colspan="<?= $gcount ?>"><?= htmlspecialchars($gname) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Monat</th>
|
||||
<th class="journal-col-betrag">Betrag</th>
|
||||
<th class="journal-soll">Soll</th>
|
||||
<th class="journal-haben">Haben</th>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<th class="journal-col-amount journal-<?= $col['side'] ?>"><?= htmlspecialchars($col['label']) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<!-- Jahressummen oben (wie Excel) -->
|
||||
<tr class="journal-top-sums">
|
||||
<td style="text-align:right;"><strong>S</strong></td>
|
||||
<td class="journal-col-betrag"></td>
|
||||
<td class="journal-col-amount journal-soll"><strong><?= journal_format_amount($summary['yearly_soll']) ?></strong></td>
|
||||
<td class="journal-col-amount journal-haben"></td>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<?php if ($col['side'] === 'soll'): ?>
|
||||
<strong><?= journal_format_amount($summary['yearly_totals'][$col['key']] ?? 0) ?></strong>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<tr class="journal-top-sums">
|
||||
<td style="text-align:right;"><strong>H</strong></td>
|
||||
<td class="journal-col-betrag"></td>
|
||||
<td class="journal-col-amount journal-soll"></td>
|
||||
<td class="journal-col-amount journal-haben"><strong><?= journal_format_amount($summary['yearly_haben']) ?></strong></td>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<?php if ($col['side'] === 'haben'): ?>
|
||||
<strong><?= journal_format_amount($summary['yearly_totals'][$col['key']] ?? 0) ?></strong>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php for ($m = 1; $m <= 12; $m++): ?>
|
||||
<?php $mt = $summary['months'][$m]; ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<?= url_for('journal.php?year_id=' . $year_id . '&month=' . $m) ?>">
|
||||
<?= journal_month_name_full($m) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="journal-col-betrag"><?= journal_format_amount($mt['_amount'] ?? 0) ?></td>
|
||||
<td class="journal-col-amount journal-soll"><?= journal_format_amount($mt['_soll'] ?? 0) ?></td>
|
||||
<td class="journal-col-amount journal-haben"><?= journal_format_amount($mt['_haben'] ?? 0) ?></td>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<?php $col_val = $mt[$col['key']] ?? 0; ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<?php if ($col_val != 0): ?>
|
||||
<a href="<?= url_for('journal_detail.php?year_id=' . $year_id . '&month=' . $m . '&col_key=' . urlencode($col['key'])) ?>" style="color:inherit;text-decoration:none;" title="Details anzeigen">
|
||||
<?= journal_format_amount($col_val) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?= journal_format_amount($col_val) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="journal-totals-row">
|
||||
<td><strong>Jahressumme</strong></td>
|
||||
<td class="journal-col-betrag"><strong><?= journal_format_amount($summary['yearly_amount']) ?></strong></td>
|
||||
<td class="journal-col-amount journal-soll"><strong><?= journal_format_amount($summary['yearly_soll']) ?></strong></td>
|
||||
<td class="journal-col-amount journal-haben"><strong><?= journal_format_amount($summary['yearly_haben']) ?></strong></td>
|
||||
<?php foreach ($columns as $col): ?>
|
||||
<td class="journal-col-amount journal-<?= $col['side'] ?>">
|
||||
<strong><?= journal_format_amount($summary['yearly_totals'][$col['key']] ?? 0) ?></strong>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gewinnberechnung -->
|
||||
<?php if ($profitability): ?>
|
||||
<section>
|
||||
<h2>Gewinnberechnung <?= $current_year ? (int)$current_year['year'] : '' ?></h2>
|
||||
<div>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monat</th>
|
||||
<th style="text-align:right;">Erlöse</th>
|
||||
<th style="text-align:right;">Wareneingang</th>
|
||||
<th style="text-align:right;">Aufwand</th>
|
||||
<th style="text-align:right;">Gewinn</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$sum_er = 0; $sum_we = 0; $sum_au = 0; $sum_gw = 0;
|
||||
for ($m = 1; $m <= 12; $m++):
|
||||
$p = $profitability[$m];
|
||||
$sum_er += $p['erloese'];
|
||||
$sum_we += $p['wareneingang'];
|
||||
$sum_au += $p['aufwand'];
|
||||
$sum_gw += $p['gewinn'];
|
||||
?>
|
||||
<tr>
|
||||
<td><?= journal_month_name_full($m) ?></td>
|
||||
<td style="text-align:right;"><?= journal_format_amount($p['erloese']) ?></td>
|
||||
<td style="text-align:right;"><?= journal_format_amount($p['wareneingang']) ?></td>
|
||||
<td style="text-align:right;"><?= journal_format_amount($p['aufwand']) ?></td>
|
||||
<td style="text-align:right;<?= $p['gewinn'] < 0 ? 'color:var(--error);' : '' ?>">
|
||||
<strong><?= journal_format_amount($p['gewinn']) ?></strong>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>Jahressumme</strong></td>
|
||||
<td style="text-align:right;"><strong><?= journal_format_amount($sum_er) ?></strong></td>
|
||||
<td style="text-align:right;"><strong><?= journal_format_amount($sum_we) ?></strong></td>
|
||||
<td style="text-align:right;"><strong><?= journal_format_amount($sum_au) ?></strong></td>
|
||||
<td style="text-align:right;<?= $sum_gw < 0 ? 'color:var(--error);' : '' ?>">
|
||||
<strong><?= journal_format_amount($sum_gw) ?></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
42
pirp/public/login.php
Normal file
42
pirp/public/login.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$user = $_POST['username'] ?? '';
|
||||
$pass = $_POST['password'] ?? '';
|
||||
if (login($user, $pass)) {
|
||||
header('Location: ' . url_for('index.php'));
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Login fehlgeschlagen.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PIRP Login</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrap">
|
||||
<div class="login-box">
|
||||
<h1>PIRP</h1>
|
||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
||||
<form method="post">
|
||||
<label>Benutzer:
|
||||
<input type="text" name="username" required autofocus>
|
||||
</label>
|
||||
<label>Passwort:
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<button type="submit" style="width:100%;margin-top:4px;">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
6
pirp/public/logout.php
Normal file
6
pirp/public/logout.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
logout();
|
||||
header('Location: ' . url_for('login.php'));
|
||||
exit;
|
||||
128
pirp/public/mahnung_new.php
Normal file
128
pirp/public/mahnung_new.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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/pdf_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$invoice_id = isset($_GET['invoice_id']) ? (int)$_GET['invoice_id'] : 0;
|
||||
if (!$invoice_id) {
|
||||
header('Location: ' . url_for('invoices.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$inv = get_invoice_with_customer($invoice_id);
|
||||
if (!$inv || $inv['paid']) {
|
||||
header('Location: ' . url_for('invoices.php?msg=' . urlencode('Mahnung nur für offene Rechnungen möglich.')));
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = get_db();
|
||||
|
||||
// Mahnstufe auto-bestimmen
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM mahnungen WHERE invoice_id = :id");
|
||||
$stmt->execute([':id' => $invoice_id]);
|
||||
$existing_count = (int)$stmt->fetchColumn();
|
||||
$next_level = min($existing_count + 1, 3);
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$mahnung_date = $_POST['mahnung_date'] ?? date('Y-m-d');
|
||||
$level = (int)($_POST['level'] ?? $next_level);
|
||||
$fee_amount = (float)str_replace(',', '.', $_POST['fee_amount'] ?? '0');
|
||||
|
||||
if ($level < 1 || $level > 3) $level = $next_level;
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO mahnungen (invoice_id, mahnung_date, level, fee_amount)
|
||||
VALUES (:iid, :mdate, :level, :fee)
|
||||
RETURNING id");
|
||||
$stmt->execute([
|
||||
':iid' => $invoice_id,
|
||||
':mdate' => $mahnung_date,
|
||||
':level' => $level,
|
||||
':fee' => $fee_amount,
|
||||
]);
|
||||
$mahnung_id = (int)$stmt->fetchColumn();
|
||||
|
||||
archive_mahnung_pdf($mahnung_id);
|
||||
|
||||
header('Location: ' . url_for('mahnung_pdf.php?id=' . $mahnung_id));
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$level_labels = [1 => 'Mahnung', 2 => '2. Mahnung', 3 => '3. Mahnung (Letzte)'];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mahnung – <?= htmlspecialchars($inv['invoice_number']) ?></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') ?>">← Zurück zu Rechnungen</a>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<p class="error"><?= htmlspecialchars($error) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<section>
|
||||
<h2>Mahnung erstellen</h2>
|
||||
<div style="max-width:460px;">
|
||||
<table class="list" style="margin-bottom:16px;">
|
||||
<tr><td style="color:var(--text-muted);width:140px;">Rechnung</td><td><strong><?= htmlspecialchars($inv['invoice_number']) ?></strong></td></tr>
|
||||
<tr><td style="color:var(--text-muted);">Kunde</td><td><?= htmlspecialchars($inv['customer_name']) ?></td></tr>
|
||||
<tr><td style="color:var(--text-muted);">Rechnungsdatum</td><td><?= date('d.m.Y', strtotime($inv['invoice_date'])) ?></td></tr>
|
||||
<tr><td style="color:var(--text-muted);">Betrag</td><td><?= number_format($inv['total_gross'], 2, ',', '.') ?> €</td></tr>
|
||||
</table>
|
||||
|
||||
<form method="post" style="display:flex;flex-direction:column;gap:12px;">
|
||||
<label>Mahnstufe:
|
||||
<select name="level">
|
||||
<?php foreach ($level_labels as $l => $lbl): ?>
|
||||
<option value="<?= $l ?>" <?= $l == $next_level ? 'selected' : '' ?>><?= $lbl ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Mahndatum:
|
||||
<input type="date" name="mahnung_date" value="<?= date('Y-m-d') ?>" required>
|
||||
</label>
|
||||
<label>Mahngebühr (€):
|
||||
<input type="text" name="fee_amount" value="0,00" style="width:120px;">
|
||||
</label>
|
||||
<div>
|
||||
<button type="submit">Mahnung erstellen & PDF öffnen</button>
|
||||
<a href="<?= url_for('invoices.php') ?>" style="margin-left:12px;">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
45
pirp/public/mahnung_pdf.php
Normal file
45
pirp/public/mahnung_pdf.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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/pdf_functions.php';
|
||||
require_login();
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if (!$id) {
|
||||
header('Location: ' . url_for('invoices.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT m.*, i.invoice_number FROM mahnungen m JOIN invoices i ON i.id = m.invoice_id WHERE m.id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
header('Location: ' . url_for('invoices.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// PDF vorhanden?
|
||||
if (empty($row['pdf_path'])) {
|
||||
archive_mahnung_pdf($id);
|
||||
// Neu laden
|
||||
$stmt->execute([':id' => $id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$fullPath = __DIR__ . '/' . ($row['pdf_path'] ?? '');
|
||||
if (!file_exists($fullPath)) {
|
||||
http_response_code(404);
|
||||
echo 'PDF nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$safe_name = 'MAHNUNG-' . preg_replace('/[^A-Za-z0-9\-]/', '_', $row['invoice_number']) . '-L' . $row['level'] . '.pdf';
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . $safe_name . '"');
|
||||
header('Content-Length: ' . filesize($fullPath));
|
||||
readfile($fullPath);
|
||||
exit;
|
||||
147
pirp/public/recurring.php
Normal file
147
pirp/public/recurring.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$msg = '';
|
||||
$error = '';
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// Aktionen verarbeiten
|
||||
if ($action === 'delete' && $id) {
|
||||
delete_recurring_template($id);
|
||||
$msg = 'Abo-Vorlage gelöscht.';
|
||||
}
|
||||
|
||||
if ($action === 'toggle' && $id) {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("UPDATE recurring_templates SET is_active = NOT is_active WHERE id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$msg = 'Status geändert.';
|
||||
}
|
||||
|
||||
// Filter
|
||||
$show_all = isset($_GET['show_all']);
|
||||
$templates = get_recurring_templates(!$show_all);
|
||||
$pending_count = count_pending_recurring_invoices();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Abo-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') ?>">Übersicht</a>
|
||||
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
|
||||
<a href="<?= url_for('recurring.php') ?>" class="active">Abo-Rechnungen</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 ($pending_count > 0): ?>
|
||||
<div class="warning">
|
||||
<strong><?= $pending_count ?> Abo-Rechnung<?= $pending_count > 1 ? 'en' : '' ?> fällig!</strong>
|
||||
<a href="<?= url_for('recurring_generate.php') ?>" class="button" style="margin-left:10px;">Jetzt generieren</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section>
|
||||
<h2>Abo-Vorlagen</h2>
|
||||
<div>
|
||||
<p>
|
||||
<a href="<?= url_for('recurring_edit.php') ?>" class="button">Neue Abo-Vorlage</a>
|
||||
<a href="<?= url_for('recurring_generate.php') ?>" class="button-secondary">Rechnungen generieren</a>
|
||||
<?php if ($show_all): ?>
|
||||
<a href="<?= url_for('recurring.php') ?>" class="button-secondary">Nur aktive anzeigen</a>
|
||||
<?php else: ?>
|
||||
<a href="<?= url_for('recurring.php?show_all=1') ?>" class="button-secondary">Alle anzeigen</a>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Kunde</th>
|
||||
<th>Intervall</th>
|
||||
<th>Nächste Fälligkeit</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($templates as $t): ?>
|
||||
<?php
|
||||
$is_due = $t['is_active'] && strtotime($t['next_due_date']) <= time();
|
||||
?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($t['template_name']) ?></td>
|
||||
<td>
|
||||
<?= htmlspecialchars($t['customer_name']) ?>
|
||||
<?php if ($t['customer_number']): ?>
|
||||
<br><small><?= htmlspecialchars($t['customer_number']) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="interval-badge interval-<?= $t['interval_type'] ?>">
|
||||
<?= get_interval_label($t['interval_type']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?= date('d.m.Y', strtotime($t['next_due_date'])) ?>
|
||||
<?php if ($is_due): ?>
|
||||
<span class="badge badge-warning">Fällig</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($t['is_active']): ?>
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-danger">Inaktiv</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= url_for('recurring_edit.php?id=' . $t['id']) ?>">Bearbeiten</a>
|
||||
<a href="<?= url_for('recurring.php?action=toggle&id=' . $t['id']) ?>">
|
||||
<?= $t['is_active'] ? 'Deaktivieren' : 'Aktivieren' ?>
|
||||
</a>
|
||||
<a href="<?= url_for('recurring.php?action=delete&id=' . $t['id']) ?>"
|
||||
onclick="return confirm('Abo-Vorlage wirklich löschen?');">Löschen</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($templates)): ?>
|
||||
<tr><td colspan="6">Keine Abo-Vorlagen gefunden.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
277
pirp/public/recurring_edit.php
Normal file
277
pirp/public/recurring_edit.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?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/customer_functions.php';
|
||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$settings = get_settings();
|
||||
$customers = get_customers();
|
||||
$error = '';
|
||||
$msg = '';
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
$template = null;
|
||||
$items = [];
|
||||
|
||||
if ($id) {
|
||||
$template = get_recurring_template($id);
|
||||
if (!$template) {
|
||||
header('Location: ' . url_for('recurring.php'));
|
||||
exit;
|
||||
}
|
||||
$items = get_recurring_template_items($id);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$data = [
|
||||
'template_name' => trim($_POST['template_name'] ?? ''),
|
||||
'customer_id' => (int)($_POST['customer_id'] ?? 0),
|
||||
'interval_type' => $_POST['interval_type'] ?? 'monthly',
|
||||
'start_date' => $_POST['start_date'] ?? date('Y-m-d'),
|
||||
'end_date' => $_POST['end_date'] ?: null,
|
||||
'next_due_date' => $_POST['next_due_date'] ?? date('Y-m-d'),
|
||||
'vat_mode' => $settings['vat_mode'] ?? 'klein',
|
||||
'vat_rate' => (float)($settings['default_vat_rate'] ?? 19.0),
|
||||
'is_active' => !empty($_POST['is_active']),
|
||||
'notes_internal' => $_POST['notes_internal'] ?? ''
|
||||
];
|
||||
|
||||
// Validierung
|
||||
if (empty($data['template_name'])) {
|
||||
$error = 'Bitte einen Namen eingeben.';
|
||||
} elseif ($data['customer_id'] <= 0) {
|
||||
$error = 'Bitte einen Kunden auswählen.';
|
||||
} else {
|
||||
// Positionen sammeln
|
||||
$newItems = [];
|
||||
$count = isset($_POST['item_desc']) ? count($_POST['item_desc']) : 0;
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$desc = trim($_POST['item_desc'][$i] ?? '');
|
||||
$qty = (float)($_POST['item_qty'][$i] ?? 0);
|
||||
$price = (float)($_POST['item_price'][$i] ?? 0);
|
||||
|
||||
if ($desc !== '' && $qty > 0) {
|
||||
$newItems[] = [
|
||||
'position_no' => count($newItems) + 1,
|
||||
'description' => $desc,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $price
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($newItems)) {
|
||||
$error = 'Bitte mindestens eine Position ausfüllen.';
|
||||
} else {
|
||||
$template_id = save_recurring_template($id, $data);
|
||||
save_recurring_template_items($template_id, $newItems);
|
||||
|
||||
header('Location: ' . url_for('recurring.php'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= $id ? 'Abo-Vorlage bearbeiten' : 'Neue Abo-Vorlage' ?></title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<script>
|
||||
function addRow() {
|
||||
const tbody = document.getElementById('items-body');
|
||||
const index = tbody.children.length;
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${index+1}</td>
|
||||
<td><input type="text" name="item_desc[${index}]" size="40"></td>
|
||||
<td><input type="number" step="0.01" name="item_qty[${index}]" value="1"></td>
|
||||
<td><input type="number" step="0.01" name="item_price[${index}]" value="0.00"></td>
|
||||
<td><button type="button" onclick="this.closest('tr').remove()">X</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
</script>
|
||||
</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') ?>">Übersicht</a>
|
||||
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
|
||||
<a href="<?= url_for('recurring.php') ?>" class="active">Abo-Rechnungen</a>
|
||||
</div>
|
||||
<?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<section>
|
||||
<h2>Abo-Details</h2>
|
||||
<div>
|
||||
<label>Name der Vorlage:
|
||||
<input type="text" name="template_name"
|
||||
value="<?= htmlspecialchars($template['template_name'] ?? '') ?>" required>
|
||||
</label>
|
||||
|
||||
<label>Kunde:
|
||||
<select name="customer_id" required>
|
||||
<option value="">-- wählen --</option>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"
|
||||
<?= ($template['customer_id'] ?? 0) == $c['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($c['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>Intervall:
|
||||
<select name="interval_type" required>
|
||||
<option value="monthly" <?= ($template['interval_type'] ?? '') === 'monthly' ? 'selected' : '' ?>>
|
||||
Monatlich
|
||||
</option>
|
||||
<option value="quarterly" <?= ($template['interval_type'] ?? '') === 'quarterly' ? 'selected' : '' ?>>
|
||||
Quartalsweise (alle 3 Monate)
|
||||
</option>
|
||||
<option value="yearly" <?= ($template['interval_type'] ?? '') === 'yearly' ? 'selected' : '' ?>>
|
||||
Jährlich
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="flex-row">
|
||||
<label>Startdatum:
|
||||
<input type="date" name="start_date"
|
||||
value="<?= htmlspecialchars($template['start_date'] ?? date('Y-m-d')) ?>" required>
|
||||
</label>
|
||||
<label>Enddatum (optional):
|
||||
<input type="date" name="end_date"
|
||||
value="<?= htmlspecialchars($template['end_date'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Nächste Fälligkeit:
|
||||
<input type="date" name="next_due_date"
|
||||
value="<?= htmlspecialchars($template['next_due_date'] ?? date('Y-m-d')) ?>" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" value="1"
|
||||
<?= ($template['is_active'] ?? true) ? 'checked' : '' ?>>
|
||||
Aktiv (Rechnungen werden generiert)
|
||||
</label>
|
||||
|
||||
<label>Interne Notizen:
|
||||
<textarea name="notes_internal" rows="2"><?= htmlspecialchars($template['notes_internal'] ?? '') ?></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Positionen</h2>
|
||||
<div>
|
||||
<table class="list" id="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%">Pos.</th>
|
||||
<th>Beschreibung</th>
|
||||
<th style="width:12%">Menge</th>
|
||||
<th style="width:15%">Einzelpreis (netto)</th>
|
||||
<th style="width:5%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-body">
|
||||
<?php
|
||||
// Bestehende Positionen oder leere Zeilen
|
||||
$displayItems = !empty($items) ? $items : [
|
||||
['description' => '', 'quantity' => 1, 'unit_price' => 0],
|
||||
['description' => '', 'quantity' => 1, 'unit_price' => 0],
|
||||
['description' => '', 'quantity' => 1, 'unit_price' => 0]
|
||||
];
|
||||
foreach ($displayItems as $i => $item):
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $i + 1 ?></td>
|
||||
<td><input type="text" name="item_desc[<?= $i ?>]"
|
||||
value="<?= htmlspecialchars($item['description']) ?>" size="40"></td>
|
||||
<td><input type="number" step="0.01" name="item_qty[<?= $i ?>]"
|
||||
value="<?= number_format($item['quantity'], 2, '.', '') ?>"></td>
|
||||
<td><input type="number" step="0.01" name="item_price[<?= $i ?>]"
|
||||
value="<?= number_format($item['unit_price'], 2, '.', '') ?>"></td>
|
||||
<td><button type="button" onclick="this.closest('tr').remove()">X</button></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<button type="button" onclick="addRow()" class="button-secondary">Position hinzufügen</button>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p>
|
||||
<button type="submit">Speichern</button>
|
||||
<a href="<?= url_for('recurring.php') ?>" class="button-secondary">Abbrechen</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<?php if ($id): ?>
|
||||
<section>
|
||||
<h2>Generierte Rechnungen</h2>
|
||||
<div>
|
||||
<?php $log = get_recurring_log($id); ?>
|
||||
<?php if (empty($log)): ?>
|
||||
<p>Noch keine Rechnungen generiert.</p>
|
||||
<?php else: ?>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Fälligkeit</th>
|
||||
<th>Rechnungsnr.</th>
|
||||
<th>Betrag</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($log as $entry): ?>
|
||||
<tr>
|
||||
<td><?= date('d.m.Y H:i', strtotime($entry['generated_at'])) ?></td>
|
||||
<td><?= date('d.m.Y', strtotime($entry['due_date'])) ?></td>
|
||||
<td><?= htmlspecialchars($entry['invoice_number'] ?? '-') ?></td>
|
||||
<td><?= $entry['total_gross'] ? number_format($entry['total_gross'], 2, ',', '.') . ' €' : '-' ?></td>
|
||||
<td>
|
||||
<?php if ($entry['invoice_id']): ?>
|
||||
<a href="<?= url_for('invoice_pdf.php?id=' . $entry['invoice_id']) ?>" target="_blank">PDF</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
158
pirp/public/recurring_generate.php
Normal file
158
pirp/public/recurring_generate.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_once __DIR__ . '/../src/recurring_functions.php';
|
||||
require_once __DIR__ . '/../src/icons.php';
|
||||
require_login();
|
||||
|
||||
$msg = '';
|
||||
$error = '';
|
||||
$generated = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$selected = $_POST['template_ids'] ?? [];
|
||||
|
||||
if (empty($selected)) {
|
||||
$error = 'Bitte mindestens eine Vorlage auswählen.';
|
||||
} else {
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($selected as $tid) {
|
||||
$tid = (int)$tid;
|
||||
$invoice_id = generate_invoice_from_template($tid);
|
||||
|
||||
if ($invoice_id) {
|
||||
$success++;
|
||||
$generated[] = $invoice_id;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($success > 0) {
|
||||
$msg = "$success Rechnung(en) erfolgreich generiert.";
|
||||
}
|
||||
if ($failed > 0) {
|
||||
$error = "$failed Rechnung(en) konnten nicht generiert werden.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pending = get_pending_recurring_invoices();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Abo-Rechnungen generieren</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<script>
|
||||
function toggleAll(checkbox) {
|
||||
document.querySelectorAll('input[name="template_ids[]"]').forEach(cb => {
|
||||
cb.checked = checkbox.checked;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</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') ?>">Übersicht</a>
|
||||
<a href="<?= url_for('invoice_new.php') ?>">Neue Rechnung</a>
|
||||
<a href="<?= url_for('recurring.php') ?>" class="active">Abo-Rechnungen</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($generated)): ?>
|
||||
<section>
|
||||
<h2>Generierte Rechnungen</h2>
|
||||
<div>
|
||||
<ul>
|
||||
<?php foreach ($generated as $inv_id): ?>
|
||||
<li><a href="<?= url_for('invoice_pdf.php?id=' . $inv_id) ?>" target="_blank">Rechnung #<?= $inv_id ?> anzeigen</a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section>
|
||||
<h2>Fällige Abo-Rechnungen</h2>
|
||||
<div>
|
||||
<?php if (empty($pending)): ?>
|
||||
<p class="success">Keine fälligen Abo-Rechnungen. Alles erledigt!</p>
|
||||
<p><a href="<?= url_for('recurring.php') ?>" class="button-secondary">Zurück zur Übersicht</a></p>
|
||||
<?php else: ?>
|
||||
<form method="post">
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px"><input type="checkbox" onclick="toggleAll(this)" checked></th>
|
||||
<th>Vorlage</th>
|
||||
<th>Kunde</th>
|
||||
<th>Intervall</th>
|
||||
<th>Fällig seit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pending as $p): ?>
|
||||
<?php
|
||||
$items = get_recurring_template_items($p['id']);
|
||||
$total = 0;
|
||||
foreach ($items as $item) {
|
||||
$total += $item['quantity'] * $item['unit_price'];
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="template_ids[]" value="<?= $p['id'] ?>" checked></td>
|
||||
<td>
|
||||
<?= htmlspecialchars($p['template_name']) ?>
|
||||
<br><small>ca. <?= number_format($total, 2, ',', '.') ?> € netto</small>
|
||||
</td>
|
||||
<td>
|
||||
<?= htmlspecialchars($p['customer_name']) ?>
|
||||
<?php if ($p['customer_number']): ?>
|
||||
<br><small><?= htmlspecialchars($p['customer_number']) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="interval-badge interval-<?= $p['interval_type'] ?>">
|
||||
<?= get_interval_label($p['interval_type']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= date('d.m.Y', strtotime($p['next_due_date'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:15px">
|
||||
<button type="submit">Ausgewählte Rechnungen generieren</button>
|
||||
<a href="<?= url_for('recurring.php') ?>" class="button-secondary">Abbrechen</a>
|
||||
</p>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
81
pirp/public/search_api.php
Normal file
81
pirp/public/search_api.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/config.php';
|
||||
require_once __DIR__ . '/../src/auth.php';
|
||||
require_once __DIR__ . '/../src/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
if (mb_strlen($q) < 2) {
|
||||
echo json_encode(['ok' => true, 'results' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = get_db();
|
||||
$like = '%' . $q . '%';
|
||||
$results = [];
|
||||
|
||||
// Rechnungen
|
||||
$stmt = $pdo->prepare("SELECT i.id, i.invoice_number, i.total_gross, c.name AS customer_name
|
||||
FROM invoices i
|
||||
JOIN customers c ON c.id = i.customer_id
|
||||
WHERE i.invoice_number ILIKE :q OR c.name ILIKE :q2
|
||||
ORDER BY i.created_at DESC
|
||||
LIMIT 5");
|
||||
$stmt->execute([':q' => $like, ':q2' => $like]);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$results[] = [
|
||||
'type' => 'invoice',
|
||||
'title' => $row['invoice_number'],
|
||||
'subtitle' => $row['customer_name'] . ' - ' . number_format((float)$row['total_gross'], 2, ',', '.') . ' €',
|
||||
'url' => url_for('invoice_pdf.php?id=' . $row['id']),
|
||||
];
|
||||
}
|
||||
|
||||
// Kunden
|
||||
$stmt = $pdo->prepare("SELECT id, name, city FROM customers
|
||||
WHERE name ILIKE :q OR city ILIKE :q2
|
||||
ORDER BY name
|
||||
LIMIT 5");
|
||||
$stmt->execute([':q' => $like, ':q2' => $like]);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$results[] = [
|
||||
'type' => 'customer',
|
||||
'title' => $row['name'],
|
||||
'subtitle' => $row['city'] ?? '',
|
||||
'url' => url_for('customers.php?action=edit&id=' . $row['id']),
|
||||
];
|
||||
}
|
||||
|
||||
// Ausgaben
|
||||
$stmt = $pdo->prepare("SELECT id, description, amount, category FROM expenses
|
||||
WHERE description ILIKE :q OR category ILIKE :q2
|
||||
ORDER BY expense_date DESC
|
||||
LIMIT 5");
|
||||
$stmt->execute([':q' => $like, ':q2' => $like]);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$results[] = [
|
||||
'type' => 'expense',
|
||||
'title' => $row['description'],
|
||||
'subtitle' => ($row['category'] ?? '') . ' - ' . number_format((float)$row['amount'], 2, ',', '.') . ' €',
|
||||
'url' => url_for('expenses.php?action=edit&id=' . $row['id']),
|
||||
];
|
||||
}
|
||||
|
||||
// Journal-Einträge
|
||||
$stmt = $pdo->prepare("SELECT id, description, amount, entry_date FROM journal_entries
|
||||
WHERE description ILIKE :q
|
||||
ORDER BY entry_date DESC
|
||||
LIMIT 5");
|
||||
$stmt->execute([':q' => $like]);
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$results[] = [
|
||||
'type' => 'journal',
|
||||
'title' => $row['description'],
|
||||
'subtitle' => date('d.m.Y', strtotime($row['entry_date'])) . ' - ' . number_format((float)$row['amount'], 2, ',', '.') . ' €',
|
||||
'url' => url_for('journal_entry.php?id=' . $row['id']),
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'results' => $results]);
|
||||
802
pirp/public/settings.php
Normal file
802
pirp/public/settings.php
Normal file
@@ -0,0 +1,802 @@
|
||||
<?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();
|
||||
|
||||
$settings = get_settings();
|
||||
$msg = '';
|
||||
$error = '';
|
||||
|
||||
// Aktiver Tab
|
||||
$tab = $_GET['tab'] ?? 'allgemein';
|
||||
$journal_sub = $_GET['jsub'] ?? 'jahre';
|
||||
|
||||
// ---- POST-Aktionen ----
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$form = $_POST['form'] ?? '';
|
||||
|
||||
// === ALLGEMEIN TAB ===
|
||||
if ($form === 'allgemein') {
|
||||
$data = [
|
||||
'company_name' => $_POST['company_name'] ?? '',
|
||||
'company_address' => $_POST['company_address'] ?? '',
|
||||
'company_zip' => $_POST['company_zip'] ?? '',
|
||||
'company_city' => $_POST['company_city'] ?? '',
|
||||
'company_country' => $_POST['company_country'] ?? '',
|
||||
'tax_id' => $_POST['tax_id'] ?? '',
|
||||
'vat_mode' => $_POST['vat_mode'] ?? 'klein',
|
||||
'default_vat_rate'=> $_POST['default_vat_rate'] ?? 19.0,
|
||||
'payment_terms' => $_POST['payment_terms'] ?? '',
|
||||
'footer_text' => $_POST['footer_text'] ?? '',
|
||||
'logo_path' => $settings['logo_path'] ?? null,
|
||||
'iban' => $_POST['iban'] ?? '',
|
||||
'phone' => $_POST['phone'] ?? '',
|
||||
'email' => $_POST['email'] ?? '',
|
||||
'website' => $_POST['website'] ?? '',
|
||||
];
|
||||
|
||||
if (!empty($_FILES['logo']['tmp_name'])) {
|
||||
$targetDir = __DIR__ . '/uploads/';
|
||||
if (!is_dir($targetDir)) {
|
||||
mkdir($targetDir, 0775, true);
|
||||
}
|
||||
$targetFile = $targetDir . 'logo.png';
|
||||
if (move_uploaded_file($_FILES['logo']['tmp_name'], $targetFile)) {
|
||||
$data['logo_path'] = 'uploads/logo.png';
|
||||
}
|
||||
}
|
||||
|
||||
save_settings($data);
|
||||
$settings = get_settings();
|
||||
$msg = 'Einstellungen gespeichert.';
|
||||
}
|
||||
|
||||
// === JOURNAL TAB ===
|
||||
// Jahr erstellen
|
||||
if ($form === 'year') {
|
||||
$y = (int)($_POST['year'] ?? 0);
|
||||
if ($y >= 2000 && $y <= 2099) {
|
||||
$existing = get_journal_year_by_year($y);
|
||||
if ($existing) {
|
||||
$error = "Jahr $y existiert bereits.";
|
||||
} else {
|
||||
create_journal_year($y, $_POST['notes'] ?? '');
|
||||
$msg = "Jahr $y erstellt.";
|
||||
}
|
||||
} else {
|
||||
$error = 'Ungültiges Jahr.';
|
||||
}
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Jahr öffnen/schließen
|
||||
if ($form === 'toggle_year') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id) {
|
||||
toggle_journal_year_closed($id);
|
||||
$msg = 'Jahresstatus geändert.';
|
||||
}
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Lieferant speichern
|
||||
if ($form === 'supplier') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$data = [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
||||
'is_active' => isset($_POST['is_active']),
|
||||
];
|
||||
if ($data['name']) {
|
||||
save_journal_supplier($id, $data);
|
||||
$msg = 'Lieferant gespeichert.';
|
||||
} else {
|
||||
$error = 'Name ist Pflichtfeld.';
|
||||
}
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Lieferant löschen
|
||||
if ($form === 'delete_supplier') {
|
||||
delete_journal_supplier((int)$_POST['id']);
|
||||
$msg = 'Lieferant gelöscht.';
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Wareneingang-Kategorie speichern
|
||||
if ($form === 'rev_cat') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$data = [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'category_type' => $_POST['category_type'] ?? 'wareneingang',
|
||||
'vat_rate' => $_POST['vat_rate'] ?? 19,
|
||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
||||
'is_active' => isset($_POST['is_active']),
|
||||
];
|
||||
if ($data['name']) {
|
||||
save_journal_revenue_category($id, $data);
|
||||
$msg = 'Kategorie gespeichert.';
|
||||
} else {
|
||||
$error = 'Name ist Pflichtfeld.';
|
||||
}
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Wareneingang-/Erlös-Kategorie löschen
|
||||
if ($form === 'delete_rev_cat') {
|
||||
delete_journal_revenue_category((int)$_POST['id']);
|
||||
$msg = 'Kategorie gelöscht.';
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Aufwandskategorie speichern
|
||||
if ($form === 'exp_cat') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$data = [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'side' => $_POST['side'] ?? 'soll',
|
||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
||||
'is_active' => isset($_POST['is_active']),
|
||||
];
|
||||
if ($data['name']) {
|
||||
save_journal_expense_category($id, $data);
|
||||
$msg = 'Aufwandskategorie gespeichert.';
|
||||
} else {
|
||||
$error = 'Name ist Pflichtfeld.';
|
||||
}
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Aufwandskategorie löschen
|
||||
if ($form === 'delete_exp_cat') {
|
||||
delete_journal_expense_category((int)$_POST['id']);
|
||||
$msg = 'Aufwandskategorie gelöscht.';
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Abzug speichern
|
||||
if ($form === 'ded_cat') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$data = [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
||||
'is_active' => isset($_POST['is_active']),
|
||||
];
|
||||
if ($data['name']) {
|
||||
save_journal_deduction_category($id, $data);
|
||||
$msg = 'Abzug gespeichert.';
|
||||
} else {
|
||||
$error = 'Name ist Pflichtfeld.';
|
||||
}
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Abzug löschen
|
||||
if ($form === 'delete_ded_cat') {
|
||||
delete_journal_deduction_category((int)$_POST['id']);
|
||||
$msg = 'Abzug gelöscht.';
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Zusammenfassungsposten speichern
|
||||
if ($form === 'summary_item') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$data = [
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'sort_order' => $_POST['sort_order'] ?? 0,
|
||||
'is_active' => isset($_POST['is_active']),
|
||||
];
|
||||
if ($data['name']) {
|
||||
save_journal_summary_item($id, $data);
|
||||
$msg = 'Posten gespeichert.';
|
||||
} else {
|
||||
$error = 'Name ist Pflichtfeld.';
|
||||
}
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// Zusammenfassungsposten löschen
|
||||
if ($form === 'delete_summary_item') {
|
||||
delete_journal_summary_item((int)$_POST['id']);
|
||||
$msg = 'Posten gelöscht.';
|
||||
$tab = 'journal';
|
||||
}
|
||||
|
||||
// === KONTO TAB ===
|
||||
// Benutzername ändern
|
||||
if ($form === 'change_username') {
|
||||
$new_username = trim($_POST['new_username'] ?? '');
|
||||
if (strlen($new_username) < 3) {
|
||||
$error = 'Benutzername muss mindestens 3 Zeichen haben.';
|
||||
} elseif (!update_username($_SESSION['user_id'], $new_username)) {
|
||||
$error = 'Benutzername existiert bereits.';
|
||||
} else {
|
||||
$msg = 'Benutzername geändert.';
|
||||
}
|
||||
$tab = 'konto';
|
||||
}
|
||||
|
||||
// Passwort ändern
|
||||
if ($form === 'change_password') {
|
||||
$current_pw = $_POST['current_password'] ?? '';
|
||||
$new_pw = $_POST['new_password'] ?? '';
|
||||
$confirm_pw = $_POST['confirm_password'] ?? '';
|
||||
if (strlen($new_pw) < 6) {
|
||||
$error = 'Neues Passwort muss mindestens 6 Zeichen haben.';
|
||||
} elseif ($new_pw !== $confirm_pw) {
|
||||
$error = 'Passwörter stimmen nicht überein.';
|
||||
} elseif (!update_password($_SESSION['user_id'], $current_pw, $new_pw)) {
|
||||
$error = 'Aktuelles Passwort ist falsch.';
|
||||
} else {
|
||||
$msg = 'Passwort geändert.';
|
||||
}
|
||||
$tab = 'konto';
|
||||
}
|
||||
}
|
||||
|
||||
// Journal-Daten laden
|
||||
$years = get_journal_years();
|
||||
$suppliers = get_journal_suppliers();
|
||||
$we_cats = get_journal_revenue_categories('wareneingang');
|
||||
$er_cats = get_journal_revenue_categories('erloese');
|
||||
$exp_cats = get_journal_expense_categories();
|
||||
$ded_cats = get_journal_deduction_categories();
|
||||
$summary_items = get_journal_summary_items();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Einstellungen</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') ?>"><?= 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') ?>" class="active"><?= 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>
|
||||
<!-- Tab-Navigation -->
|
||||
<div class="settings-tabs">
|
||||
<a href="<?= url_for('settings.php?tab=allgemein') ?>" class="<?= $tab === 'allgemein' ? 'active' : '' ?>">Allgemein</a>
|
||||
<a href="<?= url_for('settings.php?tab=journal') ?>" class="<?= $tab === 'journal' ? 'active' : '' ?>">Journal</a>
|
||||
<a href="<?= url_for('settings.php?tab=konto') ?>" class="<?= $tab === 'konto' ? 'active' : '' ?>">Konto</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 ($tab === 'allgemein'): ?>
|
||||
<!-- ==================== ALLGEMEIN TAB ==================== -->
|
||||
<section>
|
||||
<h2>Firmeneinstellungen</h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="form" value="allgemein">
|
||||
<label>Firmenname:
|
||||
<input type="text" name="company_name" value="<?= htmlspecialchars($settings['company_name'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<label>Adresse (mehrzeilig):
|
||||
<textarea name="company_address" rows="3"><?= htmlspecialchars($settings['company_address'] ?? '') ?></textarea>
|
||||
</label>
|
||||
|
||||
<div class="flex-row">
|
||||
<label>PLZ:
|
||||
<input type="text" name="company_zip" value="<?= htmlspecialchars($settings['company_zip'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Ort:
|
||||
<input type="text" name="company_city" value="<?= htmlspecialchars($settings['company_city'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Land:
|
||||
<input type="text" name="company_country" value="<?= htmlspecialchars($settings['company_country'] ?? '') ?>">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>Steuernummer/USt-IdNr:
|
||||
<input type="text" name="tax_id" value="<?= htmlspecialchars($settings['tax_id'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<label>Umsatzsteuer-Modus:
|
||||
<select name="vat_mode">
|
||||
<option value="klein" <?= ($settings['vat_mode'] ?? '') === 'klein' ? 'selected' : '' ?>>Kleinunternehmer</option>
|
||||
<option value="normal" <?= ($settings['vat_mode'] ?? '') === 'normal' ? 'selected' : '' ?>>Normal</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>Standard-USt-Satz (%):
|
||||
<input type="number" step="0.01" name="default_vat_rate" value="<?= htmlspecialchars($settings['default_vat_rate'] ?? '19.00') ?>">
|
||||
</label>
|
||||
|
||||
<label>IBAN:
|
||||
<input type="text" name="iban" value="<?= htmlspecialchars($settings['iban'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<label>Telefon:
|
||||
<input type="text" name="phone" value="<?= htmlspecialchars($settings['phone'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<label>E-Mail:
|
||||
<input type="email" name="email" value="<?= htmlspecialchars($settings['email'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<label>Website:
|
||||
<input type="text" name="website" value="<?= htmlspecialchars($settings['website'] ?? '') ?>">
|
||||
</label>
|
||||
|
||||
<label>Zahlungsbedingungen:
|
||||
<textarea name="payment_terms" rows="2"><?= htmlspecialchars($settings['payment_terms'] ?? '') ?></textarea>
|
||||
</label>
|
||||
|
||||
<label>Fußtext:
|
||||
<textarea name="footer_text" rows="2"><?= htmlspecialchars($settings['footer_text'] ?? '') ?></textarea>
|
||||
</label>
|
||||
|
||||
<label>Logo (PNG):
|
||||
<input type="file" name="logo" accept="image/png">
|
||||
</label>
|
||||
<?php if (!empty($settings['logo_path'])): ?>
|
||||
<p>Aktuelles Logo:<br>
|
||||
<img src="<?= htmlspecialchars($settings['logo_path']) ?>" style="max-height:60px;"></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit">Speichern</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php elseif ($tab === 'journal'): ?>
|
||||
<!-- ==================== JOURNAL TAB ==================== -->
|
||||
|
||||
<!-- Journal Sub-Tabs -->
|
||||
<div class="journal-settings-subtabs">
|
||||
<a href="<?= url_for('settings.php?tab=journal&jsub=jahre') ?>" class="<?= $journal_sub === 'jahre' ? 'active' : '' ?>">Jahre</a>
|
||||
<a href="<?= url_for('settings.php?tab=journal&jsub=einnahmen') ?>" class="<?= $journal_sub === 'einnahmen' ? 'active' : '' ?>">Einnahmen</a>
|
||||
<a href="<?= url_for('settings.php?tab=journal&jsub=ausgaben') ?>" class="<?= $journal_sub === 'ausgaben' ? 'active' : '' ?>">Ausgaben</a>
|
||||
<a href="<?= url_for('settings.php?tab=journal&jsub=stammdaten') ?>" class="<?= $journal_sub === 'stammdaten' ? 'active' : '' ?>">Sonstiges</a>
|
||||
</div>
|
||||
|
||||
<?php if ($journal_sub === 'jahre'): ?>
|
||||
<!-- ========== JAHRE ========== -->
|
||||
<section>
|
||||
<h2>Journal-Jahre</h2>
|
||||
<p class="settings-help">Hier verwalten Sie die Buchungsjahre. Ein geschlossenes Jahr kann nicht mehr bearbeitet werden.</p>
|
||||
<div>
|
||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
||||
<input type="hidden" name="form" value="year">
|
||||
<label>Jahr:
|
||||
<input type="number" name="year" value="<?= date('Y') ?>" min="2000" max="2099" required style="max-width:100px;">
|
||||
</label>
|
||||
<label>Notizen:
|
||||
<input type="text" name="notes" value="">
|
||||
</label>
|
||||
<label>
|
||||
<button type="submit">Jahr erstellen</button>
|
||||
</label>
|
||||
</form>
|
||||
<?php if ($years): ?>
|
||||
<table class="list">
|
||||
<thead><tr><th>Jahr</th><th>Status</th><th>Notizen</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($years as $y): ?>
|
||||
<tr>
|
||||
<td><strong><?= (int)$y['year'] ?></strong></td>
|
||||
<td><?= $y['is_closed'] ? '<span class="badge badge-danger">Geschlossen</span>' : '<span class="badge badge-success">Offen</span>' ?></td>
|
||||
<td><?= htmlspecialchars($y['notes'] ?? '') ?></td>
|
||||
<td>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="form" value="toggle_year">
|
||||
<input type="hidden" name="id" value="<?= $y['id'] ?>">
|
||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">
|
||||
<?= $y['is_closed'] ? 'Öffnen' : 'Schließen' ?>
|
||||
</button>
|
||||
</form>
|
||||
<a href="<?= url_for('journal.php?year_id=' . $y['id']) ?>" style="margin-left:6px;font-size:10px;">Zum Journal</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<p class="info">Noch keine Jahre angelegt. Erstellen Sie ein Jahr, um mit der Buchführung zu beginnen.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php elseif ($journal_sub === 'einnahmen'): ?>
|
||||
<!-- ========== EINNAHMEN ========== -->
|
||||
|
||||
<!-- Erlös-Kategorien -->
|
||||
<section>
|
||||
<h2>Erlös-Kategorien</h2>
|
||||
<p class="settings-help">Kategorien für Einnahmen/Erlöse (z.B. "Umsatz 7%", "Umsatz 19%"). Diese erscheinen als Spalten im Journal.</p>
|
||||
<div>
|
||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
||||
<input type="hidden" name="form" value="rev_cat">
|
||||
<input type="hidden" name="category_type" value="erloese">
|
||||
<label>Name:
|
||||
<input type="text" name="name" required placeholder="z.B. Umsatz 19%">
|
||||
</label>
|
||||
<label>MwSt %:
|
||||
<input type="number" step="0.01" name="vat_rate" value="19" style="max-width:80px;">
|
||||
</label>
|
||||
<label>Sort.:
|
||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" checked> Aktiv
|
||||
</label>
|
||||
<label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</label>
|
||||
</form>
|
||||
<?php if ($er_cats): ?>
|
||||
<table class="list">
|
||||
<thead><tr><th>Name</th><th>MwSt</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($er_cats as $cat): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" style="display:inline;" class="flex-row">
|
||||
<input type="hidden" name="form" value="rev_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<input type="hidden" name="category_type" value="erloese">
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:150px;">
|
||||
</td>
|
||||
<td><input type="number" step="0.01" name="vat_rate" value="<?= htmlspecialchars($cat['vat_rate']) ?>" style="max-width:70px;"></td>
|
||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
||||
<td>
|
||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="form" value="delete_rev_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Wareneingang-Kategorien -->
|
||||
<section>
|
||||
<h2>Wareneingang-Kategorien</h2>
|
||||
<p class="settings-help">Kategorien für Wareneinkauf (z.B. "WE 7%", "WE 19%"). Diese erscheinen als Spalten im Journal.</p>
|
||||
<div>
|
||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
||||
<input type="hidden" name="form" value="rev_cat">
|
||||
<input type="hidden" name="category_type" value="wareneingang">
|
||||
<label>Name:
|
||||
<input type="text" name="name" required placeholder="z.B. WE 19%">
|
||||
</label>
|
||||
<label>MwSt %:
|
||||
<input type="number" step="0.01" name="vat_rate" value="19" style="max-width:80px;">
|
||||
</label>
|
||||
<label>Sort.:
|
||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" checked> Aktiv
|
||||
</label>
|
||||
<label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</label>
|
||||
</form>
|
||||
<?php if ($we_cats): ?>
|
||||
<table class="list">
|
||||
<thead><tr><th>Name</th><th>MwSt</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($we_cats as $cat): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" style="display:inline;" class="flex-row">
|
||||
<input type="hidden" name="form" value="rev_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<input type="hidden" name="category_type" value="wareneingang">
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:150px;">
|
||||
</td>
|
||||
<td><input type="number" step="0.01" name="vat_rate" value="<?= htmlspecialchars($cat['vat_rate']) ?>" style="max-width:70px;"></td>
|
||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
||||
<td>
|
||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="form" value="delete_rev_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php elseif ($journal_sub === 'ausgaben'): ?>
|
||||
<!-- ========== AUSGABEN / ABZÜGE ========== -->
|
||||
|
||||
<!-- Aufwandskategorien -->
|
||||
<section>
|
||||
<h2>Aufwandskategorien</h2>
|
||||
<p class="settings-help">Kategorien für Betriebsausgaben (z.B. "Miete", "Versicherung", "Telefon"). Diese erscheinen als Spalten im Journal und werden in der EÜR berücksichtigt.</p>
|
||||
<div>
|
||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
||||
<input type="hidden" name="form" value="exp_cat">
|
||||
<label>Name:
|
||||
<input type="text" name="name" required placeholder="z.B. Miete">
|
||||
</label>
|
||||
<label>Typ:
|
||||
<select name="side" style="max-width:120px;">
|
||||
<option value="soll">Soll</option>
|
||||
<option value="soll_haben">Soll+Haben</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Sort.:
|
||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" checked> Aktiv
|
||||
</label>
|
||||
<label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</label>
|
||||
</form>
|
||||
<?php if ($exp_cats): ?>
|
||||
<table class="list">
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($exp_cats as $cat): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" style="display:inline;" class="flex-row">
|
||||
<input type="hidden" name="form" value="exp_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:150px;">
|
||||
</td>
|
||||
<td>
|
||||
<select name="side" style="max-width:100px;">
|
||||
<option value="soll" <?= $cat['side'] === 'soll' ? 'selected' : '' ?>>Soll</option>
|
||||
<option value="soll_haben" <?= $cat['side'] === 'soll_haben' ? 'selected' : '' ?>>Soll+Haben</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
||||
<td>
|
||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="form" value="delete_exp_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Abzüge -->
|
||||
<section>
|
||||
<h2>Abzüge</h2>
|
||||
<p class="settings-help">Abzugskategorien (z.B. "Skonto", "Lotto"). Diese erscheinen als Haben-Spalten im Journal und werden in der EÜR berücksichtigt.</p>
|
||||
<div>
|
||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
||||
<input type="hidden" name="form" value="ded_cat">
|
||||
<label>Name:
|
||||
<input type="text" name="name" required placeholder="z.B. Skonto">
|
||||
</label>
|
||||
<label>Sort.:
|
||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" checked> Aktiv
|
||||
</label>
|
||||
<label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</label>
|
||||
</form>
|
||||
<?php if ($ded_cats): ?>
|
||||
<table class="list">
|
||||
<thead><tr><th>Name</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($ded_cats as $cat): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" style="display:inline;" class="flex-row">
|
||||
<input type="hidden" name="form" value="ded_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($cat['name']) ?>" style="max-width:200px;">
|
||||
</td>
|
||||
<td><input type="number" name="sort_order" value="<?= (int)$cat['sort_order'] ?>" style="max-width:50px;"></td>
|
||||
<td><input type="checkbox" name="is_active" <?= $cat['is_active'] ? 'checked' : '' ?>></td>
|
||||
<td>
|
||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="form" value="delete_ded_cat">
|
||||
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php elseif ($journal_sub === 'stammdaten'): ?>
|
||||
<!-- ========== STAMMDATEN ========== -->
|
||||
|
||||
<!-- Lieferanten -->
|
||||
<section>
|
||||
<h2>Lieferanten</h2>
|
||||
<p class="settings-help">Lieferanten können bei Buchungen ausgewählt werden, um die Zuordnung zu erleichtern.</p>
|
||||
<div>
|
||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
||||
<input type="hidden" name="form" value="supplier">
|
||||
<label>Name:
|
||||
<input type="text" name="name" required placeholder="z.B. Metro">
|
||||
</label>
|
||||
<label>Sort.:
|
||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" checked> Aktiv
|
||||
</label>
|
||||
<label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</label>
|
||||
</form>
|
||||
<?php if ($suppliers): ?>
|
||||
<table class="list">
|
||||
<thead><tr><th>Name</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($suppliers as $sup): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" style="display:inline;" class="flex-row">
|
||||
<input type="hidden" name="form" value="supplier">
|
||||
<input type="hidden" name="id" value="<?= $sup['id'] ?>">
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($sup['name']) ?>" style="max-width:200px;">
|
||||
</td>
|
||||
<td><input type="number" name="sort_order" value="<?= (int)$sup['sort_order'] ?>" style="max-width:50px;"></td>
|
||||
<td><input type="checkbox" name="is_active" <?= $sup['is_active'] ? 'checked' : '' ?>></td>
|
||||
<td>
|
||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="form" value="delete_supplier">
|
||||
<input type="hidden" name="id" value="<?= $sup['id'] ?>">
|
||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Umsatz-Zusammenfassungsposten -->
|
||||
<section>
|
||||
<h2>Umsatz-Zusammenfassungsposten</h2>
|
||||
<p class="settings-help">Zusätzliche Posten für die monatliche Umsatzübersicht im Journal (z.B. "Reinigung", "RMV").</p>
|
||||
<div>
|
||||
<form method="post" class="flex-row" style="margin-bottom:12px;">
|
||||
<input type="hidden" name="form" value="summary_item">
|
||||
<label>Name:
|
||||
<input type="text" name="name" required placeholder="z.B. Reinigung">
|
||||
</label>
|
||||
<label>Sort.:
|
||||
<input type="number" name="sort_order" value="0" style="max-width:60px;">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" checked> Aktiv
|
||||
</label>
|
||||
<label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</label>
|
||||
</form>
|
||||
<?php if ($summary_items): ?>
|
||||
<table class="list">
|
||||
<thead><tr><th>Name</th><th>Sort.</th><th>Aktiv</th><th>Aktion</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($summary_items as $item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<form method="post" style="display:inline;" class="flex-row">
|
||||
<input type="hidden" name="form" value="summary_item">
|
||||
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($item['name']) ?>" style="max-width:200px;">
|
||||
</td>
|
||||
<td><input type="number" name="sort_order" value="<?= (int)$item['sort_order'] ?>" style="max-width:50px;"></td>
|
||||
<td><input type="checkbox" name="is_active" <?= $item['is_active'] ? 'checked' : '' ?>></td>
|
||||
<td>
|
||||
<button type="submit" class="secondary" style="padding:3px 8px;font-size:10px;">Speichern</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="form" value="delete_summary_item">
|
||||
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
||||
<button type="submit" class="danger" style="padding:3px 8px;font-size:10px;" onclick="return confirm('Wirklich löschen?');">X</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php elseif ($tab === 'konto'): ?>
|
||||
<!-- ==================== KONTO TAB ==================== -->
|
||||
<?php $current_user = get_logged_in_user(); ?>
|
||||
|
||||
<section>
|
||||
<h2>Benutzername ändern</h2>
|
||||
<div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="form" value="change_username">
|
||||
<label>Aktueller Benutzername:
|
||||
<input type="text" value="<?= htmlspecialchars($current_user['username'] ?? '') ?>" disabled>
|
||||
</label>
|
||||
<label>Neuer Benutzername:
|
||||
<input type="text" name="new_username" required minlength="3" style="max-width:300px;">
|
||||
</label>
|
||||
<button type="submit">Benutzername ändern</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Passwort ändern</h2>
|
||||
<div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="form" value="change_password">
|
||||
<label>Aktuelles Passwort:
|
||||
<input type="password" name="current_password" required style="max-width:300px;">
|
||||
</label>
|
||||
<label>Neues Passwort:
|
||||
<input type="password" name="new_password" required minlength="6" style="max-width:300px;">
|
||||
</label>
|
||||
<label>Neues Passwort bestätigen:
|
||||
<input type="password" name="confirm_password" required minlength="6" style="max-width:300px;">
|
||||
</label>
|
||||
<button type="submit">Passwort ändern</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<script src="assets/command-palette.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
pirp/public/uploads/logo.png
Normal file
BIN
pirp/public/uploads/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
Reference in New Issue
Block a user