Finanzmanager/src/Controllers/EinstellungenController.php
fds2610-bot 2c86e8522b Import und Export in QIF und CSV, Housekeeping für Importdateien
Aus der Kachel QIF-Import wird Import/Export mit je einem Knopf für beide
Richtungen; davor steht die Wahl des Dateiformats (QIF oder CSV), die als
Dialog und ohne Skript als eigene Seite funktioniert.

- CsvBuchungsImport/CsvTabelle: Buchungen samt Splitzeilen, Spalten über die
  Kopfzeile, dieselbe Dublettenerkennung und der Trockenlauf wie beim QIF.
- BuchungsExport/ExportController: Filter nach Zeitraum, Konto, Kategorie und
  Klasse; die Datei entsteht im Speicher, ein Exportverzeichnis gibt es nicht.
- Housekeeping: Hochgeladene Importdateien und data/tmp werden nach der
  eingestellten Frist gelöscht (Vorgabe 14 Tage, 0 schaltet ab).

Migration 013, Test 29 sowie E2E-Spec importexport.spec.js.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:01:30 +02:00

156 lines
5.3 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);
namespace Finanzmanager\Controllers;
use Finanzmanager\Models\Einstellung;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Services\Cockpit;
use Finanzmanager\Services\DebugLog;
use Finanzmanager\Services\Wertpapierkosten;
use Finanzmanager\View;
/**
* Der zentrale Einstellungsdialog. Jeder Bereich aus Einstellung::BEREICHE
* bekommt einen Reiter; neue Module ergänzen dort ihre Felder, statt eigene
* Dialoge aufzumachen.
*/
class EinstellungenController extends BaseController
{
public function index(): void
{
$this->verlangeAnmeldung();
$bereich = $this->query('bereich', 'allgemein');
if (!array_key_exists($bereich, Einstellung::BEREICHE)) {
$bereich = 'allgemein';
}
$this->render('einstellungen/index.php', [
'titel' => 'Einstellungen',
'bereich' => $bereich,
'werte' => Einstellung::alle(),
'logDateien' => $this->logDateien(),
'kacheln' => Cockpit::sichtbar(),
'kategorien' => Kategorie::baumFlach(),
]);
}
public function speichern(): void
{
$this->verlangeAnmeldung();
$bereich = $this->post('bereich', 'allgemein');
if (!array_key_exists($bereich, Einstellung::BEREICHE)) {
View::redirect('/einstellungen', 'Unbekannter Bereich.', 'fehler');
}
// Nur Felder des angezeigten Reiters übernehmen – so überschreibt ein
// Speichern nicht die Werte der anderen Reiter mit Leerstrings.
foreach ($this->felderFuer($bereich) as $schluessel => $typ) {
$wert = match ($typ) {
'bool' => isset($_POST[$schluessel]) ? '1' : '0',
default => $this->post($schluessel),
};
Einstellung::set($schluessel, $wert, $bereich);
}
// Die Kachelauswahl ist eine Mehrfachauswahl und passt nicht in das
// Schlüssel-Wert-Raster von felderFuer(): Sie kommt als Liste an und
// wird als Liste gespeichert.
if ($bereich === 'darstellung') {
$gewaehlt = $_POST[Cockpit::EINSTELLUNG] ?? [];
Cockpit::setzeSichtbar(
is_array($gewaehlt) ? array_map('strval', array_filter($gewaehlt, 'is_scalar')) : []
);
}
if ($bereich === 'diagnose') {
DebugLog::setAktiv(Einstellung::getBool('debug_log_aktiv'));
}
View::redirect('/einstellungen?bereich=' . urlencode($bereich), 'Einstellungen gespeichert.');
}
public function logLeeren(): void
{
$this->verlangeAnmeldung();
$entfernt = 0;
foreach ($this->logDateien() as $datei) {
if (@unlink($datei['pfad'])) {
$entfernt++;
}
}
View::redirect(
'/einstellungen?bereich=diagnose',
$entfernt === 0 ? 'Es gab nichts zu löschen.' : "{$entfernt} Logdatei(en) gelöscht."
);
}
/**
* Welche Felder zu welchem Reiter gehören, inklusive Typ.
* Bereiche ohne Einträge sind Platzhalter für spätere Phasen.
*/
private function felderFuer(string $bereich): array
{
return match ($bereich) {
'allgemein' => ['mandant_name' => 'text', 'waehrung' => 'text'],
'darstellung' => ['darstellung_thema' => 'text', 'zeilen_pro_seite' => 'text'],
'buchhaltung' => ['erster_monat' => 'text'],
'banking' => [
'banking_automatisch_buchen' => 'bool',
'banking_regeln_beim_abruf' => 'bool',
'banking_abgleich_tage' => 'text',
'banking_produkt_id' => 'text',
],
'import' => [
'import_qif_verzeichnis' => 'text',
'import_stichtag' => 'text',
'housekeeping_tage' => 'text',
],
'wertpapiere' => [
'wertpapiere_kursdienst' => 'text',
'wertpapiere_api_key' => 'text',
'wertpapiere_kursabruf_aktiv' => 'bool',
] + $this->kostenfelder(),
'diagnose' => ['debug_log_aktiv' => 'bool'],
default => [],
};
}
/**
* Je Kostenart eines Wertpapiergeschäfts ein Feld. Sie stehen nicht in
* felderFuer(), weil sie aus Wertpapierkosten::arten() kommen – eine neue
* Art soll nicht an zwei Stellen nachgetragen werden müssen.
*
* @return array<string,string>
*/
private function kostenfelder(): array
{
$felder = [];
foreach (array_keys(Wertpapierkosten::arten()) as $art) {
$felder[Wertpapierkosten::schluessel($art)] = 'text';
}
return $felder;
}
/** @return list<array{name:string,pfad:string,groesse:int,geaendert:int}> */
private function logDateien(): array
{
$dateien = [];
foreach (glob(rtrim((string)\Finanzmanager\Config::get('log_dir'), '/') . '/*.log') ?: [] as $pfad) {
$dateien[] = [
'name' => basename($pfad),
'pfad' => $pfad,
'groesse' => (int)filesize($pfad),
'geaendert' => (int)filemtime($pfad),
];
}
usort($dateien, fn($a, $b) => $b['geaendert'] <=> $a['geaendert']);
return $dateien;
}
}