205 lines
9.5 KiB
PHP
205 lines
9.5 KiB
PHP
<?php
|
||
use Steuerkram\View;
|
||
|
||
// Jede Kachelzahl führt auf die Einzelbuchungen, aus denen sie besteht (#167).
|
||
$detail = function (string $bucket, ?int $month = null) use ($stats): string {
|
||
$url = '/dashboard/buchungen/' . $bucket . '/'
|
||
. ($month === null ? $stats['year'] : ($month === $stats['month'] ? $stats['year'] : $stats['prev_year']));
|
||
return $month === null ? $url : $url . '/' . $month;
|
||
};
|
||
?>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">Umsatz <?= $h($stats['year']) ?></div>
|
||
<div class="stat-value">
|
||
<a href="<?= $h($detail('einnahmen')) ?>" style="color:var(--color-success)"
|
||
title="Einzelbuchungen anzeigen"><?= $fmt($stats['umsatz']) ?></a>
|
||
</div>
|
||
<div class="stat-meta">Betriebseinnahmen (netto)</div>
|
||
<div class="stat-meta" style="font-size:.75rem;margin-top:.25rem;color:var(--color-muted)">
|
||
<?= $h($stats['vormonat_label']) ?>: <a href="<?= $h($detail('einnahmen', $stats['prev_month'])) ?>" style="color:inherit"><?= $fmt($stats['umsatz_vormonat']) ?></a>
|
||
·
|
||
<?= $h($stats['month_label']) ?>: <a href="<?= $h($detail('einnahmen', $stats['month'])) ?>" style="color:inherit"><?= $fmt($stats['umsatz_monat']) ?></a>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Ausgaben <?= $h($stats['year']) ?></div>
|
||
<div class="stat-value">
|
||
<a href="<?= $h($detail('ausgaben')) ?>" style="color:var(--color-danger)"
|
||
title="Einzelbuchungen anzeigen"><?= $fmt($stats['ausgaben']) ?></a>
|
||
</div>
|
||
<div class="stat-meta">Betriebsausgaben (netto)</div>
|
||
<div class="stat-meta" style="font-size:.75rem;margin-top:.25rem;color:var(--color-muted)">
|
||
<?= $h($stats['vormonat_label']) ?>: <a href="<?= $h($detail('ausgaben', $stats['prev_month'])) ?>" style="color:inherit"><?= $fmt($stats['ausgaben_vormonat']) ?></a>
|
||
·
|
||
<?= $h($stats['month_label']) ?>: <a href="<?= $h($detail('ausgaben', $stats['month'])) ?>" style="color:inherit"><?= $fmt($stats['ausgaben_monat']) ?></a>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Gewinn / Verlust <?= $h($stats['year']) ?></div>
|
||
<div class="stat-value">
|
||
<a href="<?= $h($detail('ergebnis')) ?>" title="Einzelbuchungen anzeigen"
|
||
style="color:<?= $stats['gewinn'] >= 0 ? 'var(--color-success)' : 'var(--color-danger)' ?>">
|
||
<?= $fmt($stats['gewinn']) ?>
|
||
</a>
|
||
</div>
|
||
<div class="stat-meta">EÜR-Ergebnis</div>
|
||
<div class="stat-meta" style="font-size:.75rem;margin-top:.25rem;color:var(--color-muted)">
|
||
<?= $h($stats['vormonat_label']) ?>: <a href="<?= $h($detail('ergebnis', $stats['prev_month'])) ?>" style="color:<?= $stats['gewinn_vormonat'] >= 0 ? 'var(--color-success)' : 'var(--color-danger)' ?>"><?= $fmt($stats['gewinn_vormonat']) ?></a>
|
||
·
|
||
<?= $h($stats['month_label']) ?>: <a href="<?= $h($detail('ergebnis', $stats['month'])) ?>" style="color:<?= $stats['gewinn_monat'] >= 0 ? 'var(--color-success)' : 'var(--color-danger)' ?>"><?= $fmt($stats['gewinn_monat']) ?></a>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Offene Rechnungen</div>
|
||
<div class="stat-value"><?= $h($stats['offene_rechnungen']) ?></div>
|
||
<div class="stat-meta"><?= $fmt($stats['offene_rechnungen_sum']) ?> ausstehend</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Ungebuchte Belege</div>
|
||
<div class="stat-value" style="color:<?= $stats['ungebuchte_belege'] > 0 ? 'var(--color-warning)' : 'inherit' ?>">
|
||
<?= $h($stats['ungebuchte_belege']) ?>
|
||
</div>
|
||
<div class="stat-meta">
|
||
<?php if ($stats['ungebuchte_belege'] > 0): ?>
|
||
<a href="/belege?booked=0">Belege buchen →</a>
|
||
<?php else: ?>
|
||
Alle Belege gebucht
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Drag & Drop Upload -->
|
||
<div id="dnd-zone" style="
|
||
border:2px dashed var(--color-border);
|
||
border-radius:var(--radius);
|
||
background:var(--color-surface);
|
||
padding:2rem;
|
||
text-align:center;
|
||
margin-bottom:1.5rem;
|
||
cursor:pointer;
|
||
transition:border-color .15s, background .15s;
|
||
position:relative;
|
||
">
|
||
<input type="file" id="dnd-file-input" accept=".pdf,.png,.jpg,.jpeg,.tif,.tiff,.xml"
|
||
style="position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%">
|
||
<div id="dnd-icon" style="font-size:2rem;line-height:1;margin-bottom:.5rem;pointer-events:none">📎</div>
|
||
<div style="font-weight:600;pointer-events:none">Beleg hierher ziehen oder klicken</div>
|
||
<div class="text-muted text-sm" style="pointer-events:none">PDF, PNG, JPEG, TIFF, XML · max. 20 MB</div>
|
||
<div id="dnd-status" style="margin-top:.75rem;min-height:1.5rem;pointer-events:none"></div>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
(function () {
|
||
const zone = document.getElementById('dnd-zone');
|
||
const input = document.getElementById('dnd-file-input');
|
||
const status = document.getElementById('dnd-status');
|
||
const icon = document.getElementById('dnd-icon');
|
||
|
||
function setActive(on) {
|
||
zone.style.borderColor = on ? 'var(--color-primary)' : 'var(--color-border)';
|
||
zone.style.background = on ? '#eff6ff' : 'var(--color-surface)';
|
||
}
|
||
|
||
function setStatus(msg, color) {
|
||
// msg kann Server-Fehlertexte enthalten → nur als Text einfügen (Fix #213)
|
||
const span = document.createElement('span');
|
||
span.style.color = color || 'var(--color-muted)';
|
||
span.textContent = msg;
|
||
status.replaceChildren(span);
|
||
}
|
||
|
||
zone.addEventListener('dragover', (e) => { e.preventDefault(); setActive(true); });
|
||
zone.addEventListener('dragleave', (e) => { if (!zone.contains(e.relatedTarget)) setActive(false); });
|
||
zone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
setActive(false);
|
||
const file = e.dataTransfer.files[0];
|
||
if (file) uploadFile(file);
|
||
});
|
||
|
||
input.addEventListener('change', () => {
|
||
if (input.files[0]) uploadFile(input.files[0]);
|
||
input.value = '';
|
||
});
|
||
|
||
function uploadFile(file) {
|
||
const allowed = ['application/pdf','image/png','image/jpeg','image/tiff',
|
||
'application/xml','text/xml'];
|
||
if (!allowed.includes(file.type) && !file.name.match(/\.(pdf|png|jpe?g|tif{1,2}|xml)$/i)) {
|
||
setStatus('Nicht unterstütztes Dateiformat.', 'var(--color-danger)');
|
||
return;
|
||
}
|
||
|
||
icon.textContent = '⏳';
|
||
setStatus('Wird hochgeladen…');
|
||
zone.style.pointerEvents = 'none';
|
||
|
||
const fd = new FormData();
|
||
fd.append('beleg_file', file);
|
||
fd.append('belegtyp', 'Eingangsrechnung');
|
||
|
||
fd.append('beleg_datum', new Date().toISOString().slice(0,10));
|
||
|
||
fetch('/belege/upload', {
|
||
method: 'POST',
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||
body: fd,
|
||
})
|
||
.then(async (res) => {
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Unbekannter Fehler');
|
||
icon.textContent = '✅';
|
||
setStatus(`${data.beleg_nr} hochgeladen – weiterleiten…`, 'var(--color-success)');
|
||
setTimeout(() => { window.location.href = '/belege/' + data.id; }, 600);
|
||
})
|
||
.catch((err) => {
|
||
icon.textContent = '📎';
|
||
zone.style.pointerEvents = '';
|
||
setStatus('Fehler: ' + err.message, 'var(--color-danger)');
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
<!-- Letzte Buchungen -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">Letzte Buchungen</div>
|
||
<a href="/journal" class="btn btn-secondary btn-sm">Alle anzeigen →</a>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Datum</th>
|
||
<th>Belegnr.</th>
|
||
<th>Typ</th>
|
||
<th>Kategorie</th>
|
||
<th>Zahlungskonto</th>
|
||
<th>Buchungstext</th>
|
||
<th class="td-right">Brutto</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($recentBookings as $b): ?>
|
||
<tr>
|
||
<td class="td-muted"><?= View::formatDate($b['buchungs_datum']) ?></td>
|
||
<td><a href="/belege/<?= $h($b['beleg_id']) ?>"><?= $h($b['beleg_nr']) ?></a></td>
|
||
<td><span class="badge <?= $b['buchungstyp'] === 'Einnahme' ? 'badge-green' : 'badge-red' ?>"><?= $h($b['buchungstyp']) ?></span></td>
|
||
<td class="td-mono"><?= $h($b['kategorie_nr']) ?> <span class="text-muted">– <?= $h($b['kategorie_name']) ?></span></td>
|
||
<td class="td-mono"><?= $h($b['zahlungs_nr']) ?> <span class="text-muted">– <?= $h($b['zahlungs_name']) ?></span></td>
|
||
<td><?= $h($b['buchungstext']) ?><?php if ($b['is_reversal']): ?> <span class="badge badge-red">Storno</span><?php endif; ?></td>
|
||
<td class="td-right td-mono"><?= $fmt($b['betrag_brutto']) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($recentBookings)): ?>
|
||
<tr><td colspan="7" class="text-center text-muted" style="padding:2rem">Noch keine Buchungen vorhanden.</td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|