Steuerkram/public/index.php

286 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
// ── Zentraler Fehler-Handler ─────────────────────────────────────
// Fängt unerwartete Fehler/Exceptions ab, loggt sie serverseitig und
// zeigt dem Client nur eine generische 500-Seite ohne Details
// (Stacktrace, Pfade, SQL-Fehler) – siehe auch php/error-handling.ini.
$steuerkramShowGenericError = static function (): void {
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: text/html; charset=utf-8');
}
echo '<!DOCTYPE html><html lang="de"><head><meta charset="utf-8">'
. '<title>Fehler</title></head>'
. '<body style="font-family:sans-serif;text-align:center;padding:4rem">'
. '<h1>Ein Fehler ist aufgetreten</h1>'
. '<p>Bitte versuchen Sie es später erneut.</p>'
. '</body></html>';
};
set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
if (!(error_reporting() & $severity)) {
return false;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
// Fehler landen zusätzlich in der Ereignisliste, damit sie ohne Zugriff auf
// das Server-Log sichtbar sind (Issue #165). Der Aufruf darf nie selbst
// scheitern – ist die Datenbank die Fehlerursache, bleibt es beim error_log.
$steuerkramLogEvent = static function (string $channel, string $message, string $level): void {
if (!class_exists(\Steuerkram\Models\EventLog::class, true)) {
return;
}
\Steuerkram\Models\EventLog::tryAdd($channel, $message, $level);
};
set_exception_handler(function (\Throwable $e) use ($steuerkramShowGenericError, $steuerkramLogEvent): void {
error_log(sprintf(
'Unhandled exception: %s in %s:%d%s%s',
$e->getMessage(),
$e->getFile(),
$e->getLine(),
PHP_EOL,
$e->getTraceAsString()
));
$steuerkramLogEvent('system', sprintf(
'%s bei %s %s: %s (%s:%d)',
(new \ReflectionClass($e))->getShortName(),
$_SERVER['REQUEST_METHOD'] ?? 'CLI',
parse_url($_SERVER['REQUEST_URI'] ?? '-', PHP_URL_PATH) ?: '-',
$e->getMessage(),
basename($e->getFile()),
$e->getLine()
), \Steuerkram\Models\EventLog::LEVEL_ERROR);
$steuerkramShowGenericError();
});
register_shutdown_function(function () use ($steuerkramShowGenericError, $steuerkramLogEvent): void {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
error_log(sprintf('Fatal error: %s in %s:%d', $error['message'], $error['file'], $error['line']));
$steuerkramLogEvent('system', sprintf(
'Schwerer Fehler bei %s %s: %s (%s:%d)',
$_SERVER['REQUEST_METHOD'] ?? 'CLI',
parse_url($_SERVER['REQUEST_URI'] ?? '-', PHP_URL_PATH) ?: '-',
$error['message'],
basename($error['file']),
$error['line']
), \Steuerkram\Models\EventLog::LEVEL_ERROR);
$steuerkramShowGenericError();
}
});
define('BASE_DIR', dirname(__DIR__));
require BASE_DIR . '/vendor/autoload.php';
header('X-Content-Type-Options: nosniff');
// Cache-Buster für statische Assets: Git-Commit-Hash aus Datei lesen
$_assetVersionFile = BASE_DIR . '/public/static/.asset-version';
define('ASSET_VERSION', file_exists($_assetVersionFile) ? trim(file_get_contents($_assetVersionFile)) : 'dev');
unset($_assetVersionFile);
use Steuerkram\Auth;
use Steuerkram\Config;
use Steuerkram\Router;
use Steuerkram\Controllers\AccountController;
use Steuerkram\Controllers\BelegController;
use Steuerkram\Controllers\BookingController;
use Steuerkram\Controllers\CustomerController;
use Steuerkram\Controllers\InvoiceController;
use Steuerkram\Controllers\SupplierController;
use Steuerkram\Controllers\UstvaController;
use Steuerkram\Controllers\EuerController;
use Steuerkram\Controllers\SettingsController;
use Steuerkram\Controllers\LoginController;
use Steuerkram\Controllers\AssetController;
use Steuerkram\Controllers\UserAdminController;
use Steuerkram\Controllers\BankController;
use Steuerkram\Controllers\BuchungsvorlageController;
use Steuerkram\Controllers\EventController;
Config::load();
$requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
if ($requestPath !== '/login') {
Auth::require();
}
if (str_starts_with($requestPath ?? '', '/useradmin')) {
Auth::requireAdmin();
}
$router = new Router();
// ── Dashboard ────────────────────────────────────────────────
$router->get('/', fn($p) => (new AccountController())->dashboard($p));
$router->get('/dashboard', fn($p) => (new AccountController())->dashboard($p));
$router->get('/dashboard/buchungen/{bucket}/{year}', fn($p) => (new AccountController())->bucketBookings($p));
$router->get('/dashboard/buchungen/{bucket}/{year}/{month}', fn($p) => (new AccountController())->bucketBookings($p));
// ── Kontenrahmen ─────────────────────────────────────────────
$router->get('/accounts', fn($p) => (new AccountController())->index($p));
$router->get('/accounts/new', fn($p) => (new AccountController())->create($p));
$router->post('/accounts', fn($p) => (new AccountController())->store($p));
$router->get('/accounts/{id}/journal', fn($p) => (new AccountController())->journal($p));
$router->get('/accounts/{id}/edit', fn($p) => (new AccountController())->edit($p));
$router->post('/accounts/{id}', fn($p) => (new AccountController())->update($p));
$router->post('/accounts/{id}/delete', fn($p) => (new AccountController())->destroy($p));
$router->get('/accounts/search', fn($p) => (new AccountController())->search($p));
// ── Belege ───────────────────────────────────────────────────
$router->get('/belege', fn($p) => (new BelegController())->index($p));
$router->get('/belege/export', fn($p) => (new BelegController())->export($p));
$router->get('/belege/upload', fn($p) => (new BelegController())->uploadForm($p));
$router->post('/belege/upload', fn($p) => (new BelegController())->upload($p));
$router->get('/belege/{id}', fn($p) => (new BelegController())->show($p));
$router->get('/belege/{id}/file', fn($p) => (new BelegController())->serveFile($p));
$router->post('/belege/{id}/delete', fn($p) => (new BelegController())->destroy($p));
$router->post('/belege/{id}/meta', fn($p) => (new BelegController())->updateMeta($p));
$router->post('/belege/{id}/supplier', fn($p) => (new BelegController())->updateSupplier($p));
$router->post('/belege/{id}/crop', fn($p) => (new BelegController())->cropImage($p));
$router->post('/belege/{id}/vorschlag/generate', fn($p) => (new BelegController())->generateVorschlag($p));
$router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController())->resetVorschlag($p));
// ── Bank buchen ──────────────────────────────────────────────
$router->get('/bank', fn($p) => (new BankController())->index($p));
$router->get('/bank/direkt', fn($p) => (new BankController())->direkt($p));
$router->post('/bank/direkt', fn($p) => (new BankController())->direktStore($p));
$router->post('/bank/direkt/_order', fn($p) => (new BankController())->direktOrder($p));
$router->get('/bank/direkt/{id}/run', fn($p) => (new BankController())->direktRun($p));
$router->post('/bank/direkt/{id}', fn($p) => (new BankController())->direktUpdate($p));
$router->post('/bank/direkt/{id}/delete', fn($p) => (new BankController())->direktDelete($p));
$router->post('/bank/direkt/{id}/tan', fn($p) => (new BankController())->direktTan($p));
$router->post('/bank/direkt/{id}/accounts', fn($p) => (new BankController())->direktAccounts($p));
$router->get('/bank/upload', fn($p) => (new BankController())->uploadForm($p));
$router->post('/bank/upload', fn($p) => (new BankController())->upload($p));
$router->post('/bank/detect-format', fn($p) => (new BankController())->detectFormat($p));
$router->post('/bank/upload/confirm', fn($p) => (new BankController())->uploadConfirm($p));
$router->get('/bank/{id}', fn($p) => (new BankController())->show($p));
$router->post('/bank/{id}/status', fn($p) => (new BankController())->updateStatus($p));
$router->post('/bank/{id}/link', fn($p) => (new BankController())->linkBeleg($p));
$router->post('/bank/{id}/link-invoice', fn($p) => (new BankController())->linkInvoice($p));
$router->post('/bank/{id}/unlink', fn($p) => (new BankController())->unlinkBeleg($p));
$router->post('/bank/{id}/beleg', fn($p) => (new BankController())->uploadBeleg($p));
// ── Buchungen ────────────────────────────────────────────────
$router->get('/journal', fn($p) => (new BookingController())->journal($p));
$router->get('/journal/export', fn($p) => (new BookingController())->export($p));
$router->post('/belege/{beleg_id}/bookings', fn($p) => (new BookingController())->store($p));
$router->post('/bookings/{id}/reverse', fn($p) => (new BookingController())->reverse($p));
$router->post('/bookings/{id}/delete', fn($p) => (new BookingController())->destroy($p));
// ── Lieferanten ──────────────────────────────────────────────
$router->get('/suppliers', fn($p) => (new SupplierController())->index($p));
$router->get('/suppliers/new', fn($p) => (new SupplierController())->create($p));
$router->post('/suppliers', fn($p) => (new SupplierController())->store($p));
$router->get('/suppliers/search', fn($p) => (new SupplierController())->search($p));
$router->get('/suppliers/{id}/edit', fn($p) => (new SupplierController())->edit($p));
$router->post('/suppliers/{id}', fn($p) => (new SupplierController())->update($p));
$router->post('/suppliers/{id}/delete', fn($p) => (new SupplierController())->destroy($p));
// ── Kunden ───────────────────────────────────────────────────
$router->get('/customers', fn($p) => (new CustomerController())->index($p));
$router->get('/customers/new', fn($p) => (new CustomerController())->create($p));
$router->post('/customers', fn($p) => (new CustomerController())->store($p));
$router->get('/customers/{id}', fn($p) => (new CustomerController())->show($p));
$router->get('/customers/{id}/edit', fn($p) => (new CustomerController())->edit($p));
$router->post('/customers/{id}', fn($p) => (new CustomerController())->update($p));
$router->post('/customers/{id}/archive', fn($p) => (new CustomerController())->archive($p));
// ── Rechnungen ───────────────────────────────────────────────
$router->get('/invoices', fn($p) => (new InvoiceController())->index($p));
$router->get('/invoices/new', fn($p) => (new InvoiceController())->create($p));
$router->post('/invoices', fn($p) => (new InvoiceController())->store($p));
$router->get('/invoices/{id}', fn($p) => (new InvoiceController())->show($p));
$router->get('/invoices/{id}/edit', fn($p) => (new InvoiceController())->edit($p));
$router->post('/invoices/{id}', fn($p) => (new InvoiceController())->update($p));
$router->post('/invoices/{id}/finalize', fn($p) => (new InvoiceController())->finalize($p));
$router->get('/invoices/{id}/pdf', fn($p) => (new InvoiceController())->pdf($p));
$router->get('/invoices/{id}/xml', fn($p) => (new InvoiceController())->xml($p));
$router->get('/invoices/{id}/book', fn($p) => (new InvoiceController())->book($p));
$router->post('/invoices/{id}/bookings', fn($p) => (new InvoiceController())->storeBooking($p));
$router->post('/invoices/{id}/mark-paid', fn($p) => (new InvoiceController())->markPaid($p));
$router->post('/invoices/{id}/cancel', fn($p) => (new InvoiceController())->cancel($p));
$router->post('/invoices/{id}/delete', fn($p) => (new InvoiceController())->destroy($p));
$router->post('/invoices/{id}/lines', fn($p) => (new InvoiceController())->addLine($p));
$router->post('/invoices/{id}/lines/{line_id}/delete', fn($p) => (new InvoiceController())->deleteLine($p));
$router->post('/invoices/{id}/send', fn($p) => (new InvoiceController())->send($p));
$router->post('/invoices/{id}/quick-book', fn($p) => (new InvoiceController())->quickBook($p));
// ── Anlagevermögen ───────────────────────────────────────────
$router->get('/assets', fn($p) => (new AssetController())->index($p));
$router->get('/assets/new', fn($p) => (new AssetController())->create($p));
$router->post('/assets', fn($p) => (new AssetController())->store($p));
$router->get('/assets/depreciation-preview', fn($p) => (new AssetController())->depreciationPreview($p));
$router->get('/assets/{id}/edit', fn($p) => (new AssetController())->edit($p));
$router->post('/assets/{id}', fn($p) => (new AssetController())->update($p));
$router->post('/assets/{id}/delete', fn($p) => (new AssetController())->destroy($p));
$router->post('/assets/{id}/depreciation', fn($p) => (new AssetController())->saveDepreciation($p));
$router->post('/assets/{id}/depreciation/{rowId}/book', fn($p) => (new AssetController())->bookAfa($p));
$router->post('/assets/{id}/depreciation/{rowId}/reverse', fn($p) => (new AssetController())->reverseAfaBooking($p));
$router->post('/assets/{id}/depreciation/{rowId}/delete', fn($p) => (new AssetController())->deleteAfaBooking($p));
// ── UStVA ────────────────────────────────────────────────────
$router->get('/tax/ustva', fn($p) => (new UstvaController())->index($p));
$router->get('/tax/ustva/{year}/{month}', fn($p) => (new UstvaController())->show($p));
$router->post('/tax/ustva/{year}/{month}/save', fn($p) => (new UstvaController())->save($p));
$router->get('/tax/ustva/{year}/{month}/xml', fn($p) => (new UstvaController())->downloadXml($p));
$router->get('/tax/ustva/{year}/{month}/kz/{kz}', fn($p) => (new UstvaController())->showKzBookings($p));
// ── EÜR ─────────────────────────────────────────────────────
$router->get('/tax/euer', fn($p) => (new EuerController())->index($p));
$router->get('/tax/euer/{year}', fn($p) => (new EuerController())->show($p));
$router->get('/tax/euer/{year}/pdf', fn($p) => (new EuerController())->pdf($p));
$router->get('/tax/euer/{year}/konto/{konto_id}', fn($p) => (new EuerController())->kontoBookings($p));
$router->get('/tax/euer/{year}/anlageliste/csv', fn($p) => (new EuerController())->anlageCsv($p));
$router->get('/tax/euer/{year}/anlageliste/pdf', fn($p) => (new EuerController())->anlagePdf($p));
// ── Einstellungen ────────────────────────────────────────────
$router->get('/settings', fn($p) => (new SettingsController())->show($p));
$router->post('/settings', fn($p) => (new SettingsController())->update($p));
$router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailTest($p));
$router->get('/settings/logo/file', fn($p) => (new SettingsController())->serveLogo($p));
$router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p));
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p));
$router->post('/settings/generate-app-key', fn($p) => (new SettingsController())->generateAppKey($p));
$router->post('/settings/llm-test', fn($p) => (new SettingsController())->llmTest($p));
$router->get('/settings/bankimport', fn($p) => (new SettingsController())->bankImport($p));
$router->post('/settings/bankimport/suggest', fn($p) => (new SettingsController())->storeSuggestion($p));
$router->post('/settings/bankimport/create', fn($p) => (new SettingsController())->createBankImportFormat($p));
$router->post('/settings/bankimport/dismiss-suggestion', fn($p) => (new SettingsController())->dismissSuggestion($p));
$router->post('/settings/bankimport/{id}/field', fn($p) => (new SettingsController())->updateBankImportField($p));
$router->post('/settings/bankimport/{id}/delete', fn($p) => (new SettingsController())->deleteBankImportFormat($p));
// ── Buchungsvorlagen ──────────────────────────────────────────
$router->get('/einstellungen/buchungsvorlagen', fn($p) => (new BuchungsvorlageController())->index($p));
$router->get('/einstellungen/buchungsvorlagen/neu', fn($p) => (new BuchungsvorlageController())->create($p));
$router->post('/einstellungen/buchungsvorlagen', fn($p) => (new BuchungsvorlageController())->store($p));
$router->get('/einstellungen/buchungsvorlagen/{id}/bearbeiten', fn($p) => (new BuchungsvorlageController())->edit($p));
$router->post('/einstellungen/buchungsvorlagen/{id}', fn($p) => (new BuchungsvorlageController())->update($p));
$router->post('/einstellungen/buchungsvorlagen/{id}/loeschen', fn($p) => (new BuchungsvorlageController())->destroy($p));
$router->get('/api/buchungsvorlagen/{id}/modal', fn($p) => (new BuchungsvorlageController())->modal($p));
$router->post('/belege/{belegId}/vorlage/{vorlageId}/buchen', fn($p) => (new BuchungsvorlageController())->buchen($p));
// ── Ereignisse ───────────────────────────────────────────────
$router->get('/ereignisse', fn($p) => (new EventController())->index($p));
$router->get('/ereignisse/data', fn($p) => (new EventController())->indexData($p));
$router->get('/ereignisse/nextcloud-import', fn($p) => (new EventController())->nextcloudImport($p));
$router->get('/ereignisse/nextcloud-import/data', fn($p) => (new EventController())->nextcloudImportData($p));
// ── Login / Logout ────────────────────────────────────────────
$router->get('/login', fn($p) => (new LoginController())->showLogin($p));
$router->post('/login', fn($p) => (new LoginController())->handleLogin($p));
$router->post('/logout', fn($p) => (new LoginController())->handleLogout($p));
// ── Benutzerverwaltung ────────────────────────────────────────
$router->get('/useradmin', fn($p) => (new UserAdminController())->index($p));
$router->post('/useradmin', fn($p) => (new UserAdminController())->store($p));
$router->post('/useradmin/{id}', fn($p) => (new UserAdminController())->update($p));
$router->post('/useradmin/{id}/delete', fn($p) => (new UserAdminController())->delete($p));
$router->post('/useradmin/{id}/toggle', fn($p) => (new UserAdminController())->toggleActive($p));
$router->dispatch();