Steuerkram/public/index.php
fds2610-bot 117c9a5c88 Fix #xml-rechnung: XML-Download (EN 16931 / XRechnung 3.0) in der Rechnungs-Detailansicht
XML-Symbol neben dem PDF-Symbol lädt die Rechnung als reine XML-Datei
(Profil XRechnung 3.0, CIUS von EN 16931). Fehlende Stamm- oder
Rechnungsdaten verhindern den Download nicht, werden aber mit BT-Nummer
in der Detailansicht ausgewiesen. Die PDF-Einbettung bleibt bei
Factur-X EN16931.
2026-07-31 18:20:16 +02:00

253 lines
18 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);
});
set_exception_handler(function (\Throwable $e) use ($steuerkramShowGenericError): void {
error_log(sprintf(
'Unhandled exception: %s in %s:%d%s%s',
$e->getMessage(),
$e->getFile(),
$e->getLine(),
PHP_EOL,
$e->getTraceAsString()
));
$steuerkramShowGenericError();
});
register_shutdown_function(function () use ($steuerkramShowGenericError): 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']));
$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));
// ── 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/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();