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>
156 lines
5.3 KiB
PHP
156 lines
5.3 KiB
PHP
<?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;
|
||
}
|
||
}
|