render('layout/base.php', [ 'pageTitle' => 'Einstellungen', 'version' => $version, 'topbarActions' => '', 'content' => $this->renderView('settings/form', [ 'settings' => $settings, 'errors' => [], ]), ]); } public function update(array $params): void { $data = $_POST; $errors = []; if (empty($data['firma'])) $errors['firma'] = 'Pflichtfeld'; if (empty($data['strasse'])) $errors['strasse'] = 'Pflichtfeld'; if (empty($data['plz'])) $errors['plz'] = 'Pflichtfeld'; if (empty($data['ort'])) $errors['ort'] = 'Pflichtfeld'; // Passwortfelder: leer gelassen = bestehendes (verschlüsseltes) Passwort behalten $current = Setting::get(); foreach (['nextcloud_pass', 'mail_smtp_pass'] as $pwField) { $newVal = trim($data[$pwField] ?? ''); $data[$pwField] = $newVal !== '' ? $newVal : ($current[$pwField] ?? ''); } if ($errors) { $vf = dirname(__DIR__) . '/version.txt'; $version = file_exists($vf) ? trim(file_get_contents($vf)) : ''; $view = new View(); $view->render('layout/base.php', [ 'pageTitle' => 'Einstellungen', 'version' => $version, 'content' => $this->renderView('settings/form', [ 'settings' => $data, 'errors' => $errors, ]), ]); return; } Setting::update($data); View::redirect('/settings', 'Einstellungen gespeichert.'); } public function uploadLogo(array $params): void { if (empty($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) { View::redirect('/settings', 'Kein gültiges Bild übermittelt.', 'error'); return; } $file = $_FILES['logo']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = (string)finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); if (!in_array($mime, ['image/png', 'image/jpeg'])) { View::redirect('/settings', 'Logo muss PNG oder JPEG sein.', 'error'); return; } $ext = ['image/png' => 'png', 'image/jpeg' => 'jpg'][$mime]; $relPath = 'static/uploads/logo/logo.' . $ext; $absDir = dirname(__DIR__, 2) . '/public/static/uploads/logo'; if (!is_dir($absDir) && !mkdir($absDir, 0755, true)) { View::redirect('/settings', 'Upload-Verzeichnis konnte nicht erstellt werden.', 'error'); return; } $dest = dirname(__DIR__, 2) . '/public/' . $relPath; if (!move_uploaded_file($file['tmp_name'], $dest)) { View::redirect('/settings', 'Datei konnte nicht gespeichert werden. Bitte Schreibrechte prüfen.', 'error'); return; } Setting::setLogo($relPath); View::redirect('/settings', 'Logo hochgeladen.'); } public function mailTest(array $params): void { header('Content-Type: application/json'); $settings = Setting::get(); $to = $settings['email'] ?? ''; if (empty($to)) { echo json_encode(['error' => 'Keine Firmen-E-Mail hinterlegt (Einstellungen → Firmendaten).']); return; } try { (new EmailService())->sendTestMail($settings, $to); echo json_encode(['ok' => true]); } catch (\RuntimeException $e) { echo json_encode(['error' => $e->getMessage()]); } } public function deleteLogo(array $params): void { Setting::deleteLogo(); View::redirect('/settings', 'Logo gelöscht.'); } public function generateAppKey(array $params): void { header('Content-Type: application/json'); echo json_encode(['key' => bin2hex(random_bytes(32))]); } public function llmTest(array $params): void { header('Content-Type: application/json'); $result = (new \Steuerkram\Services\LlmService())->testConnection(); echo json_encode($result); } public function bankImport(array $params): void { $formats = BankSource::all('csv'); $suggestion = $_SESSION['bank_import_suggestion'] ?? null; $view = new View(); $view->render('layout/base.php', [ 'pageTitle' => 'CSV Bankimport Formate', 'content' => $this->renderView('settings/bankimport', [ 'formats' => $formats, 'suggestion' => $suggestion, ]), ]); } public function deleteBankImportFormat(array $params): void { header('Content-Type: application/json'); $id = (int)($params['id'] ?? 0); $fmt = BankSource::find($id); if (!$fmt || $fmt['type'] !== 'csv') { http_response_code(404); echo json_encode(['error' => 'Format nicht gefunden.']); return; } BankSource::delete($id); echo json_encode(['ok' => true]); } public function storeSuggestion(array $params): void { header('Content-Type: application/json'); $data = json_decode(file_get_contents('php://input'), true); if (is_array($data)) { $_SESSION['bank_import_suggestion'] = $data; echo json_encode(['ok' => true]); } else { http_response_code(400); echo json_encode(['error' => 'Keine Daten.']); } } public function dismissSuggestion(array $params): void { header('Content-Type: application/json'); unset($_SESSION['bank_import_suggestion']); echo json_encode(['ok' => true]); } public function createBankImportFormat(array $params): void { $dateFormats = ['d.m.Y', 'd.m.y', 'Y-m-d', 'd/m/Y', 'm/d/Y', 'Y.m.d', 'd-m-Y']; $name = trim($_POST['name'] ?? ''); $kuerzel = trim($_POST['kuerzel'] ?? ''); $farbe = $_POST['farbe'] ?? '#6366f1'; $skipRows = (int)($_POST['skip_rows'] ?? 0); $hasHeader = in_array($_POST['has_header_row'] ?? '1', ['0', '1'], true) ? (int)$_POST['has_header_row'] : 1; $delimiter = $_POST['delimiter'] ?? ';'; $enclosure = $_POST['enclosure'] ?? '"'; $dateFmt = $_POST['date_format'] ?? 'd.m.Y'; $decSep = $_POST['decimal_separator'] ?? ','; $thouSep = $_POST['thousands_separator'] ?? '.'; $errors = []; if ($name === '' || mb_strlen($name) > 100) { $errors[] = 'Name darf nicht leer sein (max. 100 Zeichen).'; } if ($kuerzel === '' || mb_strlen($kuerzel) > 10) { $errors[] = 'Kürzel darf nicht leer sein (max. 10 Zeichen).'; } if (!preg_match('/^#[0-9a-fA-F]{6}$/', $farbe)) { $farbe = '#6366f1'; } if ($skipRows < 0 || $skipRows > 99) { $skipRows = 0; } if (mb_strlen($delimiter) !== 1) { $errors[] = 'Trennzeichen muss genau ein Zeichen sein.'; } if (mb_strlen($enclosure) !== 1) { $errors[] = 'Anführungszeichen muss genau ein Zeichen sein.'; } if (!in_array($dateFmt, $dateFormats, true)) { $dateFmt = 'd.m.Y'; } if (mb_strlen($decSep) !== 1) { $errors[] = 'Dezimaltrennzeichen muss genau ein Zeichen sein.'; } if (mb_strlen($thouSep) > 1) { $thouSep = ''; } if (!empty($errors)) { // Bei Validierungsfehler zurück mit Fehlermeldung $suggestion = $_SESSION['bank_import_suggestion'] ?? null; $formats = BankSource::all('csv'); $view = new View(); $view->render('layout/base.php', [ 'pageTitle' => 'CSV Bankimport Formate', 'content' => $this->renderView('settings/bankimport', [ 'formats' => $formats, 'suggestion' => $suggestion, 'errors' => $errors, ]), ]); return; } $tplDebit = mb_substr(trim($_POST['buchungstext_template_debit'] ?? ''), 0, 500); $tplCredit = mb_substr(trim($_POST['buchungstext_template_credit'] ?? ''), 0, 500); $nullInt = function (string $key): ?int { $v = $_POST[$key] ?? ''; if ($v === '') return null; return (int)$v; }; $intCol = function (string $key, int $default = 0): int { $v = $_POST[$key] ?? ''; return $v !== '' ? (int)$v : $default; }; $id = BankSource::create([ 'type' => 'csv', 'bezeichnung' => $name, 'kuerzel' => $kuerzel, 'farbe' => $farbe, 'skip_rows' => $skipRows, 'has_header_row' => $hasHeader, 'delimiter' => $delimiter, 'enclosure' => $enclosure, 'date_format' => $dateFmt, 'decimal_separator' => $decSep, 'thousands_separator' => $thouSep, 'col_buchungstag' => $intCol('col_buchungstag', 0), 'col_wertstellung' => $intCol('col_wertstellung', 1), 'col_vorgang' => $intCol('col_vorgang', 2), 'col_buchungstext' => 0, 'col_wert' => $intCol('col_wert', 4), 'col_zahlungspflichtiger' => $nullInt('col_zahlungspflichtiger'), 'col_zahlungsempfaenger' => $nullInt('col_zahlungsempfaenger'), 'col_verwendungszweck' => $nullInt('col_verwendungszweck'), 'buchungstext_template_debit' => $tplDebit !== '' ? $tplDebit : null, 'buchungstext_template_credit' => $tplCredit !== '' ? $tplCredit : null, ]); unset($_SESSION['bank_import_suggestion']); header('Location: /settings/bankimport#format-' . $id); exit; } public function updateBankImportField(array $params): void { header('Content-Type: application/json'); $id = (int)($params['id'] ?? 0); $fmt = BankSource::find($id); if (!$fmt || $fmt['type'] !== 'csv') { http_response_code(404); echo json_encode(['error' => 'Format nicht gefunden.']); return; } $field = $_POST['field'] ?? ''; $raw = $_POST['value'] ?? ''; $intCols = ['col_buchungstag', 'col_wertstellung', 'col_vorgang', 'col_buchungstext', 'col_wert']; $nullIntCols = ['col_zahlungspflichtiger', 'col_zahlungsempfaenger', 'col_verwendungszweck']; $charCols = ['delimiter', 'enclosure', 'decimal_separator']; $dateFormats = ['d.m.Y', 'd.m.y', 'Y-m-d', 'd/m/Y', 'm/d/Y', 'Y.m.d', 'd-m-Y']; $tplCols = ['buchungstext_template_debit', 'buchungstext_template_credit']; [$ok, $value, $err] = match (true) { $field === 'name' => (trim($raw) !== '' && mb_strlen(trim($raw)) <= 100) ? [true, trim($raw), ''] : [false, null, 'Name darf nicht leer sein (max. 100 Zeichen).'], $field === 'kuerzel' => (trim($raw) !== '' && mb_strlen(trim($raw)) <= 10) ? [true, trim($raw), ''] : [false, null, 'Kürzel darf nicht leer sein (max. 10 Zeichen).'], $field === 'farbe' => (bool)preg_match('/^#[0-9a-fA-F]{6}$/', $raw) ? [true, $raw, ''] : [false, null, 'Ungültige Farbe (Format: #RRGGBB).'], $field === 'skip_rows' => (ctype_digit($raw) && (int)$raw >= 0 && (int)$raw <= 99) ? [true, (int)$raw, ''] : [false, null, 'Übersprungene Zeilen: ganze Zahl 0–99 erforderlich.'], $field === 'has_header_row' => in_array($raw, ['0', '1'], true) ? [true, (int)$raw, ''] : [false, null, 'Ungültiger Wert.'], in_array($field, $charCols, true) => (mb_strlen($raw) === 1) ? [true, $raw, ''] : [false, null, 'Genau ein Zeichen erforderlich.'], $field === 'thousands_separator' => (mb_strlen($raw) <= 1) ? [true, $raw, ''] : [false, null, 'Maximal ein Zeichen.'], in_array($field, $intCols, true) => (ctype_digit($raw) && (int)$raw >= 0 && (int)$raw <= 99) ? [true, (int)$raw, ''] : [false, null, 'Gültige Spaltennummer (0–99) erforderlich.'], in_array($field, $nullIntCols, true) => ($raw === '' || $raw === null) ? [true, null, ''] : (ctype_digit($raw) && (int)$raw >= 0 && (int)$raw <= 99 ? [true, (int)$raw, ''] : [false, null, 'Gültige Spaltennummer (0–99) oder leer für nicht gesetzt.']), $field === 'date_format' => in_array($raw, $dateFormats, true) ? [true, $raw, ''] : [false, null, 'Unbekanntes Datumsformat.'], in_array($field, $tplCols, true) => (mb_strlen($raw) <= 500) ? [true, $raw === '' ? null : $raw, ''] : [false, null, 'Template zu lang (max. 500 Zeichen).'], default => [false, null, 'Unbekanntes Feld.'], }; if (!$ok) { http_response_code(422); echo json_encode(['error' => $err]); return; } // Mapping: settings/bankimport-View kennt "name", bank_sources nutzt "bezeichnung". if ($field === 'name') { $field = 'bezeichnung'; } BankSource::updateField($id, $field, $value); echo json_encode(['ok' => true]); } }