283 lines
19 KiB
PHP
283 lines
19 KiB
PHP
<?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();
|
||
}
|
||
|
||
$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();
|