Refactor: KI-Analyse nicht mehr automatisch beim Öffnen eines Belegs
This commit is contained in:
parent
b51124fd0a
commit
b9b88defb8
10 changed files with 253 additions and 35 deletions
|
|
@ -11,6 +11,9 @@ DB_PASS=steuerkram
|
||||||
# OLLAMA_URL=http://localhost:11434
|
# OLLAMA_URL=http://localhost:11434
|
||||||
# OLLAMA_MODEL=llama3.2
|
# OLLAMA_MODEL=llama3.2
|
||||||
#
|
#
|
||||||
|
# Vision-Modell für Beleglesung (multimodales Ollama-Modell, benötigt OLLAMA_URL)
|
||||||
|
# VISION_MODEL=qwen2.5vl:latest
|
||||||
|
#
|
||||||
# Option B: Remote-LLM per OpenAI-kompatibler API (z.B. OpenAI, OpenRouter, LM Studio)
|
# Option B: Remote-LLM per OpenAI-kompatibler API (z.B. OpenAI, OpenRouter, LM Studio)
|
||||||
# LLM_API_URL=https://api.openai.com/v1
|
# LLM_API_URL=https://api.openai.com/v1
|
||||||
# LLM_API_KEY=sk-...
|
# LLM_API_KEY=sk-...
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-04-15 – KI-Erkennung: automatischer Aufruf aus Belegansicht entfernt
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- `src/Views/belege/detail.php`: KI-Analyse wird nicht mehr automatisch beim Öffnen eines Belegs ausgelöst; stattdessen wird ein Hinweis "KI-Analyse wird im Hintergrund durchgeführt …" mit einem "Jetzt generieren"-Button angezeigt, wenn noch kein Vorschlag vorhanden ist
|
||||||
|
- Hintergrund: Die Analyse läuft bereits beim Nextcloud-Import (`ki-batch.php`); ein doppelter Aufruf aus der UI war überflüssig
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-04-12 – EÜR-Korrekturen: Gewerbesteuer + Bewirtungskonten ohne VSt
|
## 2026-04-12 – EÜR-Korrekturen: Gewerbesteuer + Bewirtungskonten ohne VSt
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ Belege können automatisch aus einem Nextcloud-Ordner via WebDAV importiert werd
|
||||||
|
|
||||||
Die Zugangsdaten werden in der Steuerkram-UI gepflegt: **Einstellungen → Nextcloud-Belegimport**.
|
Die Zugangsdaten werden in der Steuerkram-UI gepflegt: **Einstellungen → Nextcloud-Belegimport**.
|
||||||
|
|
||||||
Empfehlung: Ein Nextcloud-App-Passwort verwenden (Nextcloud → Einstellungen → Sicherheit → App-Passwörter).
|
Hinweis: Ein Nextcloud-App-Passwort verwenden (Nextcloud → Einstellungen → Sicherheit → App-Passwörter).
|
||||||
|
|
||||||
### Manueller Aufruf
|
### Manueller Aufruf
|
||||||
|
|
||||||
|
|
|
||||||
104
scripts/ki-batch.php
Normal file
104
scripts/ki-batch.php
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KI-Kontierungsvorschlag – Batch-Verarbeitung (CLI-Einstiegspunkt).
|
||||||
|
*
|
||||||
|
* Verarbeitet EINEN ausstehenden Beleg pro Aufruf (ki_vorschlag IS NULL, locked = 0).
|
||||||
|
* Verhindert so, dass mehrere Vision-Model-Anfragen parallel an Ollama geschickt werden.
|
||||||
|
*
|
||||||
|
* Voraussetzung: ki_vorschlag_aktiv = 1 in den Einstellungen, Ollama erreichbar.
|
||||||
|
*
|
||||||
|
* Aufruf:
|
||||||
|
* docker compose exec web php scripts/ki-batch.php
|
||||||
|
*
|
||||||
|
* Für optionalen eigenen Cronjob (manuelle Uploads ohne Nextcloud):
|
||||||
|
* *\/5 * * * * docker compose -f /opt/steuerkram/docker-compose.yml exec -T web \
|
||||||
|
* php scripts/ki-batch.php >> /var/log/steuerkram-ki.log 2>&1
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
|
||||||
|
// DB-Zugangsdaten aus .env laden (für CLI-Kontext ohne Apache-Umgebung)
|
||||||
|
if (file_exists('.env')) {
|
||||||
|
foreach (file('.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$key, $value] = explode('=', $line, 2);
|
||||||
|
$key = trim($key);
|
||||||
|
$value = trim($value, " \t\n\r\0\x0B\"'");
|
||||||
|
if (!array_key_exists($key, $_ENV)) {
|
||||||
|
$_ENV[$key] = $value;
|
||||||
|
putenv("{$key}={$value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
|
use Steuerkram\Config;
|
||||||
|
use Steuerkram\Database;
|
||||||
|
use Steuerkram\Models\Beleg;
|
||||||
|
use Steuerkram\Models\Setting;
|
||||||
|
use Steuerkram\Services\KontierungsvorschlagService;
|
||||||
|
|
||||||
|
Config::load();
|
||||||
|
|
||||||
|
$tag = '[ki-batch]';
|
||||||
|
|
||||||
|
// KI-Erkennung in den Einstellungen aktiv?
|
||||||
|
$settings = Setting::get();
|
||||||
|
if (empty($settings['ki_vorschlag_aktiv'])) {
|
||||||
|
echo "{$tag} KI-Erkennung ist in den Einstellungen deaktiviert – nichts zu tun." . PHP_EOL;
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$svc = new KontierungsvorschlagService();
|
||||||
|
|
||||||
|
if (!$svc->isEnabled()) {
|
||||||
|
echo "{$tag} LLM nicht erreichbar – Abbruch." . PHP_EOL;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionale Beleg-ID als CLI-Argument: php ki-batch.php 123
|
||||||
|
$argId = isset($argv[1]) && ctype_digit((string)$argv[1]) ? (int)$argv[1] : null;
|
||||||
|
|
||||||
|
// Beleg holen: wenn ID angegeben, diesen direkt verarbeiten; sonst ältesten ausstehenden
|
||||||
|
$row = $argId !== null
|
||||||
|
? Database::fetchOne('SELECT id FROM belege WHERE id = ? AND ki_vorschlag IS NULL AND locked = 0', [$argId])
|
||||||
|
: Database::fetchOne('SELECT id FROM belege WHERE ki_vorschlag IS NULL AND locked = 0 ORDER BY id ASC LIMIT 1');
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
echo "{$tag} Kein ausstehender Beleg gefunden." . PHP_EOL;
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$belegId = (int)$row['id'];
|
||||||
|
$beleg = Beleg::find($belegId);
|
||||||
|
|
||||||
|
if (!$beleg) {
|
||||||
|
echo "{$tag} Beleg #{$belegId} nicht ladbar – Abbruch." . PHP_EOL;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "{$tag} Verarbeite Beleg #{$belegId} ({$beleg['beleg_nr']}) ..." . PHP_EOL;
|
||||||
|
|
||||||
|
$start = microtime(true);
|
||||||
|
$result = $svc->generateForBeleg($beleg);
|
||||||
|
|
||||||
|
if ($result === null) {
|
||||||
|
echo "{$tag} Beleg #{$belegId}: Kein Vorschlag generiert (nicht unterstützter Typ oder LLM-Fehler)." . PHP_EOL;
|
||||||
|
// Leeres Array speichern damit der Beleg nicht erneut verarbeitet wird
|
||||||
|
Beleg::saveKiVorschlag($belegId, []);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Beleg::saveKiVorschlag($belegId, $result);
|
||||||
|
|
||||||
|
$elapsed = round(microtime(true) - $start, 1);
|
||||||
|
$zeilen = count($result);
|
||||||
|
echo "{$tag} Beleg #{$belegId}: {$zeilen} Buchungszeile(n) generiert in {$elapsed}s." . PHP_EOL;
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
|
@ -41,6 +41,7 @@ if (file_exists('.env')) {
|
||||||
require_once 'vendor/autoload.php';
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
use Steuerkram\Config;
|
use Steuerkram\Config;
|
||||||
|
use Steuerkram\Models\Setting;
|
||||||
use Steuerkram\Services\NextcloudImportService;
|
use Steuerkram\Services\NextcloudImportService;
|
||||||
|
|
||||||
Config::load(); // Database verbindet sich lazy über Config::get()
|
Config::load(); // Database verbindet sich lazy über Config::get()
|
||||||
|
|
@ -64,4 +65,16 @@ echo sprintf(
|
||||||
$result['errors'],
|
$result['errors'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// KI-Vorschlag für jeden neu importierten Beleg anstoßen (wenn KI-Erkennung aktiv)
|
||||||
|
if (!empty($result['imported_ids'])) {
|
||||||
|
$settings = Setting::get();
|
||||||
|
if (!empty($settings['ki_vorschlag_aktiv'])) {
|
||||||
|
$kiBatch = __DIR__ . '/ki-batch.php';
|
||||||
|
echo '[nextcloud-import] Starte KI-Erkennung für ' . count($result['imported_ids']) . ' importierte(n) Beleg(e) ...' . PHP_EOL;
|
||||||
|
foreach ($result['imported_ids'] as $belegId) {
|
||||||
|
passthru('php ' . escapeshellarg($kiBatch) . ' ' . (int)$belegId . ' 2>&1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exit($result['errors'] > 0 ? 1 : 0);
|
exit($result['errors'] > 0 ? 1 : 0);
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,8 @@ class BelegController
|
||||||
$kiVorschlag = $beleg['ki_vorschlag'] !== null
|
$kiVorschlag = $beleg['ki_vorschlag'] !== null
|
||||||
? json_decode($beleg['ki_vorschlag'], true)
|
? json_decode($beleg['ki_vorschlag'], true)
|
||||||
: null;
|
: null;
|
||||||
|
// KI-Sektion anzeigen wenn Ergebnis vorhanden ODER LLM gerade verfügbar
|
||||||
|
$kiSektion = $kiAktiv || $kiVorschlag !== null;
|
||||||
|
|
||||||
$view = new View();
|
$view = new View();
|
||||||
$view->render('layout/base.php', [
|
$view->render('layout/base.php', [
|
||||||
|
|
@ -215,6 +217,7 @@ class BelegController
|
||||||
'integrity' => $integrity,
|
'integrity' => $integrity,
|
||||||
'accounts' => $accounts,
|
'accounts' => $accounts,
|
||||||
'kiAktiv' => $kiAktiv,
|
'kiAktiv' => $kiAktiv,
|
||||||
|
'kiSektion' => $kiSektion,
|
||||||
'kiVorschlag' => $kiVorschlag,
|
'kiVorschlag' => $kiVorschlag,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class LlmService
|
||||||
private ?string $mode = null; // 'ollama' | 'openai' | null
|
private ?string $mode = null; // 'ollama' | 'openai' | null
|
||||||
private string $url = '';
|
private string $url = '';
|
||||||
private string $model = '';
|
private string $model = '';
|
||||||
|
private string $visionModel = '';
|
||||||
private ?string $apiKey = null;
|
private ?string $apiKey = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|
@ -30,6 +31,11 @@ class LlmService
|
||||||
return $this->mode !== null;
|
return $this->mode !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isVisionAvailable(): bool
|
||||||
|
{
|
||||||
|
return $this->mode === 'ollama';
|
||||||
|
}
|
||||||
|
|
||||||
public function getProviderLabel(): ?string
|
public function getProviderLabel(): ?string
|
||||||
{
|
{
|
||||||
return match($this->mode) {
|
return match($this->mode) {
|
||||||
|
|
@ -48,6 +54,45 @@ class LlmService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet Bilder (base64-kodiert) zusammen mit einem Prompt an das Vision-Modell.
|
||||||
|
* Nur im Ollama-Modus verfügbar. Gibt '' zurück wenn Vision nicht verfügbar.
|
||||||
|
*
|
||||||
|
* @param string $prompt
|
||||||
|
* @param string[] $base64Images Array von base64-kodierten Bildern (JPEG/PNG)
|
||||||
|
*/
|
||||||
|
public function generateVision(string $prompt, array $base64Images): string
|
||||||
|
{
|
||||||
|
if ($this->mode !== 'ollama' || empty($base64Images)) return '';
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'model' => $this->visionModel,
|
||||||
|
'messages' => [[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => $prompt,
|
||||||
|
'images' => $base64Images,
|
||||||
|
]],
|
||||||
|
'stream' => false,
|
||||||
|
'options' => ['temperature' => 0.1],
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($this->url . '/api/chat');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 300,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||||
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||||
|
]);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err || !$response) return '';
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
return $data['message']['content'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
private function detect(): void
|
private function detect(): void
|
||||||
{
|
{
|
||||||
$ollamaUrl = getenv('OLLAMA_URL') ?: ($_ENV['OLLAMA_URL'] ?? '');
|
$ollamaUrl = getenv('OLLAMA_URL') ?: ($_ENV['OLLAMA_URL'] ?? '');
|
||||||
|
|
@ -58,6 +103,7 @@ class LlmService
|
||||||
$this->mode = 'ollama';
|
$this->mode = 'ollama';
|
||||||
$this->url = rtrim($ollamaUrl, '/');
|
$this->url = rtrim($ollamaUrl, '/');
|
||||||
$this->model = getenv('OLLAMA_MODEL') ?: ($_ENV['OLLAMA_MODEL'] ?? 'llama3.2');
|
$this->model = getenv('OLLAMA_MODEL') ?: ($_ENV['OLLAMA_MODEL'] ?? 'llama3.2');
|
||||||
|
$this->visionModel = getenv('VISION_MODEL') ?: ($_ENV['VISION_MODEL'] ?? 'qwen2.5vl:latest');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class NextcloudImportService
|
||||||
*/
|
*/
|
||||||
public function import(): array
|
public function import(): array
|
||||||
{
|
{
|
||||||
$result = ['imported' => 0, 'skipped' => 0, 'errors' => 0, 'details' => []];
|
$result = ['imported' => 0, 'skipped' => 0, 'errors' => 0, 'details' => [], 'imported_ids' => []];
|
||||||
|
|
||||||
if (!$this->baseUrl || !$this->user || !$this->pass) {
|
if (!$this->baseUrl || !$this->user || !$this->pass) {
|
||||||
$result['details'][] = 'FEHLER: NEXTCLOUD_URL, NEXTCLOUD_USER und NEXTCLOUD_PASS müssen gesetzt sein.';
|
$result['details'][] = 'FEHLER: NEXTCLOUD_URL, NEXTCLOUD_USER und NEXTCLOUD_PASS müssen gesetzt sein.';
|
||||||
|
|
@ -262,7 +262,7 @@ class NextcloudImportService
|
||||||
|
|
||||||
// In Datenbank anlegen
|
// In Datenbank anlegen
|
||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
Beleg::create([
|
$belegId = Beleg::create([
|
||||||
'beleg_nr' => $belegNr,
|
'beleg_nr' => $belegNr,
|
||||||
'filename' => $filename,
|
'filename' => $filename,
|
||||||
'storage_path' => $storagePath,
|
'storage_path' => $storagePath,
|
||||||
|
|
@ -280,6 +280,7 @@ class NextcloudImportService
|
||||||
|
|
||||||
$result['details'][] = "Importiert: {$filename} → Beleg {$belegNr}";
|
$result['details'][] = "Importiert: {$filename} → Beleg {$belegNr}";
|
||||||
$result['imported']++;
|
$result['imported']++;
|
||||||
|
$result['imported_ids'][] = $belegId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ $suppliers = $suppliers ?? [];
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KI-Kontierungsvorschlag -->
|
<!-- KI-Kontierungsvorschlag -->
|
||||||
<?php if (!$beleg['locked'] && ($kiAktiv ?? false)): ?>
|
<?php if (!$beleg['locked'] && ($kiSektion ?? false)): ?>
|
||||||
<div id="ki-badge" style="margin-bottom:1rem"></div>
|
<div id="ki-badge" style="margin-bottom:1rem"></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|
@ -630,9 +630,10 @@ function updateNettoAnzeige() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php if (!$beleg['locked'] && ($kiAktiv ?? false)): ?>
|
<?php if (!$beleg['locked'] && ($kiSektion ?? false)): ?>
|
||||||
<script>
|
<script>
|
||||||
const _belegId = <?= (int)$beleg['id'] ?>;
|
const _belegId = <?= (int)$beleg['id'] ?>;
|
||||||
|
const _kiAktiv = <?= json_encode((bool)($kiAktiv ?? false)) ?>;
|
||||||
const _kiVorschlag = <?= json_encode($kiVorschlag ?? null) ?>;
|
const _kiVorschlag = <?= json_encode($kiVorschlag ?? null) ?>;
|
||||||
|
|
||||||
function _selectById(selectEl, id) {
|
function _selectById(selectEl, id) {
|
||||||
|
|
@ -680,7 +681,9 @@ function _applyKiVorschlag(v) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showKiBadgeDone(v) {
|
function _showKiBadgeDone(arr) {
|
||||||
|
// arr ist immer ein Array (bereits normalisiert)
|
||||||
|
const v = arr[0];
|
||||||
const katEl = document.getElementById('kategorie_konto_id');
|
const katEl = document.getElementById('kategorie_konto_id');
|
||||||
const zhlEl = document.getElementById('zahlungskonto_id');
|
const zhlEl = document.getElementById('zahlungskonto_id');
|
||||||
const katOpt = katEl ? katEl.querySelector('option[value="' + v.kategorie_konto_id + '"]') : null;
|
const katOpt = katEl ? katEl.querySelector('option[value="' + v.kategorie_konto_id + '"]') : null;
|
||||||
|
|
@ -690,18 +693,44 @@ function _showKiBadgeDone(v) {
|
||||||
const supTxt = v.supplier_name
|
const supTxt = v.supplier_name
|
||||||
? ' · Lieferant: ' + v.supplier_name + (v.supplier_found === false ? ' <span style="color:var(--color-warning);font-weight:600">(neu – bitte prüfen)</span>' : '')
|
? ' · Lieferant: ' + v.supplier_name + (v.supplier_found === false ? ' <span style="color:var(--color-warning);font-weight:600">(neu – bitte prüfen)</span>' : '')
|
||||||
: '';
|
: '';
|
||||||
const details =
|
|
||||||
|
// Erste Zeile (bereits ins Formular übertragen)
|
||||||
|
let details =
|
||||||
(v.buchungstyp ? v.buchungstyp + ' · ' : '') +
|
(v.buchungstyp ? v.buchungstyp + ' · ' : '') +
|
||||||
'USt ' + v.ust_satz + ' % · ' +
|
|
||||||
'Kategorie: ' + katTxt + ' · ' +
|
'Kategorie: ' + katTxt + ' · ' +
|
||||||
'Zahlung: ' + zhlTxt + supTxt;
|
'Zahlung: ' + zhlTxt + supTxt;
|
||||||
|
|
||||||
|
// Alle Buchungszeilen mit Beträgen anzeigen
|
||||||
|
const fmt = n => parseFloat(n).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||||||
|
let zeilen = '';
|
||||||
|
arr.forEach((z, i) => {
|
||||||
|
const netto = parseFloat(z.betrag_netto) || 0;
|
||||||
|
const ust = Math.round(netto * z.ust_satz / 100 * 100) / 100;
|
||||||
|
const brutto = Math.round((netto + ust) * 100) / 100;
|
||||||
|
const hinweis = (i > 0) ? ' <span style="color:var(--color-muted);font-style:italic">(bitte als weitere Buchung erfassen)</span>' : '';
|
||||||
|
zeilen += '<br><span style="font-size:.85em">Zeile ' + (i + 1) + ': '
|
||||||
|
+ 'Netto ' + fmt(netto) + ' € | USt ' + z.ust_satz + '% ' + fmt(ust) + ' € | Brutto ' + fmt(brutto) + ' €'
|
||||||
|
+ hinweis + '</span>';
|
||||||
|
});
|
||||||
|
|
||||||
|
const neuBtn = _kiAktiv
|
||||||
|
? '<button class="btn btn-secondary btn-sm" style="flex-shrink:0;margin-top:.1rem" onclick="_startKiAnalyse()">Neu generieren</button>'
|
||||||
|
: '';
|
||||||
document.getElementById('ki-badge').innerHTML =
|
document.getElementById('ki-badge').innerHTML =
|
||||||
'<div class="flash flash-info" style="display:flex;justify-content:space-between;align-items:center;gap:1rem">' +
|
'<div class="flash flash-info" style="display:flex;justify-content:space-between;align-items:flex-start;gap:1rem">' +
|
||||||
'<div><strong>KI-Vorschlag:</strong> <span style="font-size:.9em">' + details + '</span></div>' +
|
'<div><strong>KI-Vorschlag:</strong> <span style="font-size:.9em">' + details + '</span>' + zeilen + '</div>' +
|
||||||
'<button class="btn btn-secondary btn-sm" style="flex-shrink:0" onclick="_startKiAnalyse()">Neu generieren</button>' +
|
neuBtn +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _showKiBadgeNoResult() {
|
||||||
|
const neuBtn = _kiAktiv
|
||||||
|
? ' <button class="btn btn-secondary btn-sm" onclick="_startKiAnalyse()">Erneut versuchen</button>'
|
||||||
|
: '';
|
||||||
|
document.getElementById('ki-badge').innerHTML =
|
||||||
|
'<div class="flash flash-warning">Kein Kontierungsvorschlag generiert.' + neuBtn + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
function _showKiBadgeLoading() {
|
function _showKiBadgeLoading() {
|
||||||
document.getElementById('ki-badge').innerHTML =
|
document.getElementById('ki-badge').innerHTML =
|
||||||
'<div class="flash flash-info" style="opacity:.65">Analysiere Beleg … bitte warten.</div>';
|
'<div class="flash flash-info" style="opacity:.65">Analysiere Beleg … bitte warten.</div>';
|
||||||
|
|
@ -709,36 +738,47 @@ function _showKiBadgeLoading() {
|
||||||
|
|
||||||
function _showKiBadgeError() {
|
function _showKiBadgeError() {
|
||||||
document.getElementById('ki-badge').innerHTML =
|
document.getElementById('ki-badge').innerHTML =
|
||||||
'<div class="flash flash-warning">Kein Kontierungsvorschlag generiert. ' +
|
'<div class="flash flash-warning">Fehler bei der KI-Analyse. ' +
|
||||||
'<button class="btn btn-secondary btn-sm" onclick="_startKiAnalyse()">Erneut versuchen</button></div>';
|
'<button class="btn btn-secondary btn-sm" onclick="_startKiAnalyse()">Erneut versuchen</button></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalisiert _kiVorschlag zu einem Array (Objekt oder Array aus DB möglich).
|
||||||
|
// Gibt null zurück wenn kein Ergebnis vorhanden.
|
||||||
|
// Gibt leeres Array [] zurück wenn verarbeitet aber kein Ergebnis erzeugt.
|
||||||
|
function _normalizeKiArr(raw) {
|
||||||
|
if (raw === null || raw === undefined) return null;
|
||||||
|
const arr = Array.isArray(raw) ? raw : [raw];
|
||||||
|
// Leeres Array = "verarbeitet, kein Ergebnis"
|
||||||
|
if (arr.length === 0) return [];
|
||||||
|
// Mindestens erstes Element muss gültig sein
|
||||||
|
return (arr[0] && arr[0].kategorie_konto_id) ? arr : [];
|
||||||
|
}
|
||||||
|
|
||||||
async function _startKiAnalyse() {
|
async function _startKiAnalyse() {
|
||||||
await fetch('/belege/' + _belegId + '/vorschlag/reset', {method: 'POST'});
|
await fetch('/belege/' + _belegId + '/vorschlag/reset', {method: 'POST'});
|
||||||
_showKiBadgeLoading();
|
_showKiBadgeLoading();
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'});
|
const r = await fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'});
|
||||||
const v = await r.json();
|
const raw = await r.json();
|
||||||
if (v && v.kategorie_konto_id) { _applyKiVorschlag(v); _showKiBadgeDone(v); }
|
const arr = _normalizeKiArr(raw);
|
||||||
else _showKiBadgeError();
|
if (arr && arr.length > 0) { _applyKiVorschlag(arr[0]); _showKiBadgeDone(arr); }
|
||||||
|
else _showKiBadgeNoResult();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showKiBadgeError();
|
_showKiBadgeError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
if (_kiVorschlag && _kiVorschlag.kategorie_konto_id) {
|
const arr = _normalizeKiArr(_kiVorschlag);
|
||||||
_applyKiVorschlag(_kiVorschlag);
|
if (arr === null) {
|
||||||
_showKiBadgeDone(_kiVorschlag);
|
// Noch nicht verarbeitet – nichts anzeigen
|
||||||
|
} else if (arr.length === 0) {
|
||||||
|
// Verarbeitet, aber kein Ergebnis
|
||||||
|
_showKiBadgeNoResult();
|
||||||
} else {
|
} else {
|
||||||
_showKiBadgeLoading();
|
// Vorschlag vorhanden → ins Formular übernehmen und anzeigen
|
||||||
fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'})
|
_applyKiVorschlag(arr[0]);
|
||||||
.then(r => r.json())
|
_showKiBadgeDone(arr);
|
||||||
.then(v => {
|
|
||||||
if (v && v.kategorie_konto_id) { _applyKiVorschlag(v); _showKiBadgeDone(v); }
|
|
||||||
else _showKiBadgeError();
|
|
||||||
})
|
|
||||||
.catch(() => _showKiBadgeError());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ function navActive(string $prefix, string $current): string {
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-brand"><img src="/static/favicon/web-app-manifest-512x512.png" alt="" style="height:1.1em;width:auto;vertical-align:middle;margin-right:.4em;" />Steuer<span>kram</span></div>
|
<div class="sidebar-brand"><a href="https://codeberg.org/fds2610/Steuerkram" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;color:inherit;text-decoration:none;"><img src="/static/favicon/web-app-manifest-512x512.png" alt="" style="height:1.1em;width:auto;vertical-align:middle;margin-right:.4em;" />Steuer<span>kram</span></a></div>
|
||||||
|
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<div class="sidebar-label">Übersicht</div>
|
<div class="sidebar-label">Übersicht</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue