sammelxport
This commit is contained in:
@@ -310,41 +310,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshLiveDataAsync()
|
private Task RefreshLiveDataAsync()
|
||||||
{
|
{
|
||||||
var runningSiteIds = _dashboardRows
|
|
||||||
.Where(r => Orchestrator.IsExporting(r.SiteId))
|
|
||||||
.Select(r => r.SiteId)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (runningSiteIds.Count == 0)
|
|
||||||
{
|
|
||||||
_anyRunning = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
|
||||||
var appLogs = await db.AppEventLogs
|
|
||||||
.Where(l => l.SiteId != null && runningSiteIds.Contains(l.SiteId.Value))
|
|
||||||
.OrderByDescending(l => l.Timestamp)
|
|
||||||
.Take(200)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var latestAppLogsBySite = appLogs
|
|
||||||
.GroupBy(l => l.SiteId!.Value)
|
|
||||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First());
|
|
||||||
|
|
||||||
foreach (var row in _dashboardRows)
|
foreach (var row in _dashboardRows)
|
||||||
{
|
{
|
||||||
if (!latestAppLogsBySite.TryGetValue(row.SiteId, out var appLog))
|
if (!Orchestrator.IsExporting(row.SiteId))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
row.LiveMessage = $"{appLog.Category}: {appLog.Message}";
|
row.LiveMessage = Orchestrator.GetExportStatus(row.SiteId);
|
||||||
row.LiveDetails = appLog.Details ?? string.Empty;
|
row.LiveDetails = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DashboardRow
|
private class DashboardRow
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
# TrafagSalesExporter Handoff
|
||||||
|
|
||||||
|
Stand: 2026-04-15
|
||||||
|
|
||||||
|
## Zielbild
|
||||||
|
|
||||||
|
Die App wurde von einem reinen BI1/HANA-Exporter zu einer kombinierten Plattform erweitert:
|
||||||
|
|
||||||
|
- `BI1` und `SAGE` bleiben auf direktem HANA-Zugriff
|
||||||
|
- `SAP` läuft separat über SAP Gateway / OData
|
||||||
|
- SAP-Quellen können gelesen, gejoint und auf das zentrale `SalesRecord`-Schema gemappt werden
|
||||||
|
- Standort-Exporte werden lokal als Excel geschrieben
|
||||||
|
- Zusätzlich werden Datensätze in eine zentrale SQLite-Tabelle geschrieben
|
||||||
|
- Ein konsolidierter Export liest aus dieser zentralen Tabelle
|
||||||
|
|
||||||
|
## Wichtigste umgesetzte Funktionen
|
||||||
|
|
||||||
|
### 1. Zentrale Credentials pro Quellsystem
|
||||||
|
|
||||||
|
Es gibt zentrale Zugangsdaten in `ExportSettings` für:
|
||||||
|
|
||||||
|
- `SAP`
|
||||||
|
- `BI1`
|
||||||
|
- `SAGE`
|
||||||
|
|
||||||
|
Zusätzlich gibt es pro Standort optionale Overrides:
|
||||||
|
|
||||||
|
- `UsernameOverride`
|
||||||
|
- `PasswordOverride`
|
||||||
|
|
||||||
|
Auflösungsreihenfolge:
|
||||||
|
|
||||||
|
1. Standort-Override
|
||||||
|
2. zentrale Credentials des Quellsystems
|
||||||
|
3. bei HANA zusätzlich Fallback auf alten `HanaServer.Username/Password`
|
||||||
|
|
||||||
|
## 2. SAP von BI1/HANA getrennt
|
||||||
|
|
||||||
|
`SAP` nutzt nicht mehr den HANA-Pfad, sondern eine eigene Gateway/OData-Strecke.
|
||||||
|
|
||||||
|
Pro SAP-Standort gibt es:
|
||||||
|
|
||||||
|
- `SapServiceUrl`
|
||||||
|
- `SapEntitySet`
|
||||||
|
- `SapEntitySetsCache`
|
||||||
|
- `SapEntitySetsRefreshedAtUtc`
|
||||||
|
|
||||||
|
Refresh der SAP-Quellen erfolgt nur auf Knopfdruck.
|
||||||
|
|
||||||
|
Beispiel Service URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://travt762.sap.trafag.com:8000/sap/opu/odata/sap/ZPOWERBI_EINKAUF_SRV/
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- Service URL immer nur bis zum Service
|
||||||
|
- Entity Set separat auswählen
|
||||||
|
|
||||||
|
## 3. SAP-Quellen, Joins und Feldmappings
|
||||||
|
|
||||||
|
Für SAP gibt es mehrere neue Modelle:
|
||||||
|
|
||||||
|
- `SapSourceDefinition`
|
||||||
|
- `SapJoinDefinition`
|
||||||
|
- `SapFieldMapping`
|
||||||
|
|
||||||
|
Unterstützt wird:
|
||||||
|
|
||||||
|
- mehrere SAP-Quellen pro Standort
|
||||||
|
- Alias pro Quelle
|
||||||
|
- Primärquelle
|
||||||
|
- Join-Definitionen
|
||||||
|
- Mapping von `Alias.Feldname` auf zentrales Schema
|
||||||
|
|
||||||
|
UI-Erweiterungen:
|
||||||
|
|
||||||
|
- `Quellen refreshen`
|
||||||
|
- `Felder aus Quellen laden`
|
||||||
|
- Join-Key-Auswahl aus Metadaten
|
||||||
|
- `Auto-Match` für gleiche Feldnamen zwischen Primärquelle und anderen Quellen
|
||||||
|
|
||||||
|
## 4. Zentrale Datenspeicherung
|
||||||
|
|
||||||
|
Neue Tabelle:
|
||||||
|
|
||||||
|
- `CentralSalesRecords`
|
||||||
|
|
||||||
|
Verwendung:
|
||||||
|
|
||||||
|
- pro Standort werden alte zentrale Sätze dieses Standorts ersetzt
|
||||||
|
- konsolidierte Excel liest aus `CentralSalesRecords`
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- zentrale Excel wird nicht appendet
|
||||||
|
- sie wird aus dem aktuellen Zustand der zentralen Tabelle neu erstellt
|
||||||
|
|
||||||
|
## 5. Exportpfade
|
||||||
|
|
||||||
|
Neue Konfigurationsmöglichkeiten:
|
||||||
|
|
||||||
|
Zentral in `Settings`:
|
||||||
|
|
||||||
|
- `LocalSiteExportFolder`
|
||||||
|
- `LocalConsolidatedExportFolder`
|
||||||
|
|
||||||
|
Pro Standort:
|
||||||
|
|
||||||
|
- `LocalExportFolderOverride`
|
||||||
|
|
||||||
|
Fallback wenn leer:
|
||||||
|
|
||||||
|
```text
|
||||||
|
./output
|
||||||
|
```
|
||||||
|
|
||||||
|
relativ zum App-Verzeichnis.
|
||||||
|
|
||||||
|
## 6. SharePoint
|
||||||
|
|
||||||
|
SharePoint-Upload ist optional.
|
||||||
|
|
||||||
|
Wenn keine vollständige SharePoint-Konfiguration vorhanden ist:
|
||||||
|
|
||||||
|
- Excel wird trotzdem lokal erzeugt
|
||||||
|
- kein Upload nach SharePoint
|
||||||
|
|
||||||
|
Benötigte SharePoint-Werte:
|
||||||
|
|
||||||
|
- `Tenant ID`
|
||||||
|
- `Client ID`
|
||||||
|
- `Client Secret`
|
||||||
|
|
||||||
|
Das sind Entra App Registration Werte, nicht normale Benutzer-Credentials.
|
||||||
|
|
||||||
|
## 7. Config Import/Export
|
||||||
|
|
||||||
|
Es gibt JSON-Import/Export der Konfiguration mit Checkbox:
|
||||||
|
|
||||||
|
- mit Secrets
|
||||||
|
- ohne Secrets
|
||||||
|
|
||||||
|
Enthalten sind u. a.:
|
||||||
|
|
||||||
|
- SharePoint Config
|
||||||
|
- ExportSettings
|
||||||
|
- HanaServers
|
||||||
|
- Sites
|
||||||
|
- Transformation Rules
|
||||||
|
- SAP-Quellen
|
||||||
|
- SAP-Joins
|
||||||
|
- SAP-Mappings
|
||||||
|
|
||||||
|
## 8. Logging und Live-Status
|
||||||
|
|
||||||
|
Neue technische Logs über `AppEventLogs`.
|
||||||
|
|
||||||
|
Sichtbar:
|
||||||
|
|
||||||
|
- auf `/logs`
|
||||||
|
- im Dashboard als `Live-Status`
|
||||||
|
|
||||||
|
Geloggt werden u. a.:
|
||||||
|
|
||||||
|
- HANA-Query Start
|
||||||
|
- SAP Refresh
|
||||||
|
- SAP Reads
|
||||||
|
- Transformationen
|
||||||
|
- Excel-Erstellung
|
||||||
|
- zentrale Tabellenspeicherung
|
||||||
|
- Export erfolgreich / fehlgeschlagen
|
||||||
|
|
||||||
|
## 9. Excel öffnen
|
||||||
|
|
||||||
|
Im Dashboard gibt es neben `Export` den Button:
|
||||||
|
|
||||||
|
- `Excel öffnen`
|
||||||
|
|
||||||
|
Dieser nutzt `ExportLogs.FilePath`.
|
||||||
|
|
||||||
|
Voraussetzungen:
|
||||||
|
|
||||||
|
- letzter Export erfolgreich
|
||||||
|
- `FilePath` gespeichert
|
||||||
|
- Datei existiert lokal
|
||||||
|
|
||||||
|
## 10. Management Cockpit
|
||||||
|
|
||||||
|
Es gibt einen neuen Menüpunkt:
|
||||||
|
|
||||||
|
- `Management Cockpit`
|
||||||
|
|
||||||
|
Funktion:
|
||||||
|
|
||||||
|
- Auswahl vorhandener Excel-Dateien
|
||||||
|
- Analyse einer exportierten Standort-Datei
|
||||||
|
- Kennzahlen für Geschäftsinhaber / Management
|
||||||
|
|
||||||
|
Aktuell enthalten:
|
||||||
|
|
||||||
|
- Umsatz
|
||||||
|
- geschätzte Kosten
|
||||||
|
- geschätzte Marge
|
||||||
|
- Rechnungsanzahl
|
||||||
|
- Kundenanzahl
|
||||||
|
- Top Kunden
|
||||||
|
- Top Produktgruppen
|
||||||
|
- Top Sales Owner
|
||||||
|
- Datenqualitätshinweise
|
||||||
|
- automatische Management-Aussagen
|
||||||
|
|
||||||
|
## Wichtige Dateien
|
||||||
|
|
||||||
|
### Modelle
|
||||||
|
|
||||||
|
- `Models/Site.cs`
|
||||||
|
- `Models/ExportSettings.cs`
|
||||||
|
- `Models/ExportLog.cs`
|
||||||
|
- `Models/CentralSalesRecord.cs`
|
||||||
|
- `Models/SapSourceDefinition.cs`
|
||||||
|
- `Models/SapJoinDefinition.cs`
|
||||||
|
- `Models/SapFieldMapping.cs`
|
||||||
|
- `Models/ManagementCockpitModels.cs`
|
||||||
|
- `Models/ConfigTransferPackage.cs`
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
- `Services/SiteExportService.cs`
|
||||||
|
- `Services/ConsolidatedExportService.cs`
|
||||||
|
- `Services/CentralSalesRecordService.cs`
|
||||||
|
- `Services/SapGatewayService.cs`
|
||||||
|
- `Services/SapCompositionService.cs`
|
||||||
|
- `Services/ConfigTransferService.cs`
|
||||||
|
- `Services/AppEventLogService.cs`
|
||||||
|
- `Services/ManagementCockpitService.cs`
|
||||||
|
- `Services/DatabaseInitializationService.cs`
|
||||||
|
- `Services/ExportOrchestrationService.cs`
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
- `Components/Pages/Standorte.razor`
|
||||||
|
- `Components/Pages/Settings.razor`
|
||||||
|
- `Components/Pages/Dashboard.razor`
|
||||||
|
- `Components/Pages/Logs.razor`
|
||||||
|
- `Components/Pages/ManagementCockpit.razor`
|
||||||
|
- `Components/Layout/NavMenu.razor`
|
||||||
|
|
||||||
|
## Datenbank / Migrationen
|
||||||
|
|
||||||
|
Viele Änderungen laufen über `DatabaseInitializationService`.
|
||||||
|
|
||||||
|
Wichtige neue oder erweiterte Tabellen/Felder:
|
||||||
|
|
||||||
|
- `Sites`
|
||||||
|
- `UsernameOverride`
|
||||||
|
- `PasswordOverride`
|
||||||
|
- `SapServiceUrl`
|
||||||
|
- `SapEntitySet`
|
||||||
|
- `SapEntitySetsCache`
|
||||||
|
- `SapEntitySetsRefreshedAtUtc`
|
||||||
|
- `LocalExportFolderOverride`
|
||||||
|
- `ExportSettings`
|
||||||
|
- zentrale SAP/BI1/SAGE Credentials
|
||||||
|
- `LocalSiteExportFolder`
|
||||||
|
- `LocalConsolidatedExportFolder`
|
||||||
|
- `DebugLoggingEnabled`
|
||||||
|
- `ExportLogs`
|
||||||
|
- `FilePath`
|
||||||
|
- neue Tabellen:
|
||||||
|
- `AppEventLogs`
|
||||||
|
- `CentralSalesRecords`
|
||||||
|
- SAP-Konfigtabellen
|
||||||
|
|
||||||
|
## Aktuell offenes Hauptproblem
|
||||||
|
|
||||||
|
### Zentrale Speicherung hängt noch
|
||||||
|
|
||||||
|
Die große Problemstelle war die zentrale SQLite-Speicherung.
|
||||||
|
|
||||||
|
Bereits probiert:
|
||||||
|
|
||||||
|
- EF `RemoveRange + SaveChanges`
|
||||||
|
- EF Batch-Speichern
|
||||||
|
- Dashboard-Polling reduziert
|
||||||
|
- SQLite WAL + busy timeout
|
||||||
|
- direkte SQLite-Inserts in einer großen Transaktion
|
||||||
|
- jetzt: kleine abgeschlossene Transaktionen pro Batch
|
||||||
|
|
||||||
|
Aktueller Stand:
|
||||||
|
|
||||||
|
- zentrale Excel ist jetzt sehr schnell
|
||||||
|
- das Hängen wurde stark eingegrenzt
|
||||||
|
- zuletzt wurde der Schreibpfad so umgebaut, dass:
|
||||||
|
- Löschen in eigener kurzer Transaktion läuft
|
||||||
|
- Inserts batchweise mit Commit pro Batch laufen
|
||||||
|
|
||||||
|
Datei:
|
||||||
|
|
||||||
|
- `Services/CentralSalesRecordService.cs`
|
||||||
|
|
||||||
|
Die nächste Session sollte genau dort weiter debuggen, falls es noch hängt.
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- Das Problem ist nicht SAP
|
||||||
|
- nicht SharePoint
|
||||||
|
- nicht mehr der große EF-Insert
|
||||||
|
- sondern sehr wahrscheinlich SQLite-Commit/Lock-Verhalten rund um die zentrale Tabelle
|
||||||
|
|
||||||
|
## Letzte bekannte Beobachtung
|
||||||
|
|
||||||
|
Der User meldete zuletzt:
|
||||||
|
|
||||||
|
- vorher Hänger bei `Zentrale Tabelle: Abschluss speichern...`
|
||||||
|
- danach wurde auf Commit pro Batch umgestellt
|
||||||
|
- neue Session soll testen, ob es jetzt bei
|
||||||
|
- `Batch x/y speichern...`
|
||||||
|
- `Batch x/y abschliessen...`
|
||||||
|
- oder gar nicht mehr hängt
|
||||||
|
|
||||||
|
## Build-Status
|
||||||
|
|
||||||
|
Letzter Build:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build TrafagSalesExporter.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- erfolgreich
|
||||||
|
- bekannte Warnungen bleiben:
|
||||||
|
- SAP HANA Architekturwarnung `MSB3270`
|
||||||
|
- MudBlazor Analyzer `Dense`
|
||||||
|
|
||||||
|
## Hinweise für nächste Session
|
||||||
|
|
||||||
|
1. Zuerst aktuellen Export testen
|
||||||
|
2. Genaue letzte Live-Status-Meldung notieren
|
||||||
|
3. `Services/CentralSalesRecordService.cs` prüfen
|
||||||
|
4. Falls nötig:
|
||||||
|
- SQLite pragmas weiter anpassen
|
||||||
|
- zentrale Tabelle temporär ganz abschaltbar machen
|
||||||
|
- oder Schreiben über separate DB / Queue entkoppeln
|
||||||
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Next Steps
|
||||||
|
|
||||||
|
Stand: 2026-04-15
|
||||||
|
|
||||||
|
## 1. Erstes Ziel
|
||||||
|
|
||||||
|
Prüfen, ob die aktuelle Version beim Standort-Export noch in der zentralen SQLite-Speicherung hängen bleibt.
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- App neu starten
|
||||||
|
- denselben Standort erneut exportieren
|
||||||
|
- letzte sichtbare `Live-Status`-Meldung exakt notieren
|
||||||
|
|
||||||
|
Interessant sind vor allem diese Fälle:
|
||||||
|
|
||||||
|
- `Zentrale Tabelle: Batch x/y speichern...`
|
||||||
|
- `Zentrale Tabelle: Batch x/y abschliessen...`
|
||||||
|
- `Zentrale Tabelle aktualisiert`
|
||||||
|
- `Export erfolgreich`
|
||||||
|
|
||||||
|
## 2. Hauptverdächtiger
|
||||||
|
|
||||||
|
Datei:
|
||||||
|
|
||||||
|
- `Services/CentralSalesRecordService.cs`
|
||||||
|
|
||||||
|
Aktueller Stand:
|
||||||
|
|
||||||
|
- alte Sätze werden in eigener Transaktion gelöscht
|
||||||
|
- Inserts laufen in Batches von 25
|
||||||
|
- jeder Batch wird separat committed
|
||||||
|
|
||||||
|
Wenn es noch hängt, dort zuerst ansetzen.
|
||||||
|
|
||||||
|
## 3. Falls es weiter hängt
|
||||||
|
|
||||||
|
In dieser Reihenfolge prüfen:
|
||||||
|
|
||||||
|
1. Batchgröße weiter reduzieren
|
||||||
|
- z. B. `10` statt `25`
|
||||||
|
2. Direkt vor und direkt nach `transaction.CommitAsync()` zusätzlich technische Logs setzen
|
||||||
|
3. Prüfen, ob parallel noch andere SQLite-Zugriffe laufen
|
||||||
|
4. Optional zentrale Speicherung vorübergehend per Setting deaktivierbar machen
|
||||||
|
5. Falls nötig zentrale Speicherung in separate DB-Datei auslagern
|
||||||
|
|
||||||
|
## 4. Dashboard / UI prüfen
|
||||||
|
|
||||||
|
Zu testen:
|
||||||
|
|
||||||
|
- `Excel öffnen` wird nach neuem erfolgreichen Export aktiv
|
||||||
|
- `Export erfolgreich` zeigt `Pfad=...`
|
||||||
|
- Dashboard-Live-Status setzt sich nach Abschluss sauber zurück
|
||||||
|
|
||||||
|
Dateien:
|
||||||
|
|
||||||
|
- `Components/Pages/Dashboard.razor`
|
||||||
|
- `Services/SiteExportService.cs`
|
||||||
|
- `Models/ExportLog.cs`
|
||||||
|
|
||||||
|
## 5. SAP-Funktionalität kurz gegenprüfen
|
||||||
|
|
||||||
|
Zu testen:
|
||||||
|
|
||||||
|
- `Quellen refreshen`
|
||||||
|
- `Felder aus Quellen laden`
|
||||||
|
- `Auto-Match`
|
||||||
|
- SAP-Export eines Standorts
|
||||||
|
|
||||||
|
Dateien:
|
||||||
|
|
||||||
|
- `Components/Pages/Standorte.razor`
|
||||||
|
- `Services/SapGatewayService.cs`
|
||||||
|
- `Services/SapCompositionService.cs`
|
||||||
|
|
||||||
|
## 6. Management Cockpit prüfen
|
||||||
|
|
||||||
|
Zu testen:
|
||||||
|
|
||||||
|
- vorhandene Excel-Datei auswählbar
|
||||||
|
- Analyse läuft
|
||||||
|
- Kennzahlen plausibel
|
||||||
|
|
||||||
|
Dateien:
|
||||||
|
|
||||||
|
- `Components/Pages/ManagementCockpit.razor`
|
||||||
|
- `Services/ManagementCockpitService.cs`
|
||||||
|
|
||||||
|
## 7. Wenn Stabilität vor Funktion geht
|
||||||
|
|
||||||
|
Sinnvolle pragmatische Zwischenlösung:
|
||||||
|
|
||||||
|
- zentrale SQLite-Speicherung per Setting abschaltbar machen
|
||||||
|
- Export lokal und zentral Excel weiter erlauben
|
||||||
|
- zentrale DB erst wieder aktivieren, wenn der Commit-Pfad stabil ist
|
||||||
|
|
||||||
|
## 8. Referenzdatei
|
||||||
|
|
||||||
|
Für den vollständigen Kontext zuerst lesen:
|
||||||
|
|
||||||
|
- `HANDOFF_2026-04-15.md`
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ builder.Services.AddRazorComponents()
|
|||||||
builder.Services.AddMudServices();
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
||||||
options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=10"));
|
options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=60"));
|
||||||
|
|
||||||
builder.Services.AddSingleton<IHanaQueryService, HanaQueryService>();
|
builder.Services.AddSingleton<IHanaQueryService, HanaQueryService>();
|
||||||
builder.Services.AddSingleton<IExcelExportService, ExcelExportService>();
|
builder.Services.AddSingleton<IExcelExportService, ExcelExportService>();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public class CentralSalesRecordService : ICentralSalesRecordService
|
|||||||
|
|
||||||
updateStatus?.Invoke("Zentrale Tabelle: neue Saetze vorbereiten...");
|
updateStatus?.Invoke("Zentrale Tabelle: neue Saetze vorbereiten...");
|
||||||
await InsertRecordsInCommittedBatchesAsync(connection, site, recordList, updateStatus);
|
await InsertRecordsInCommittedBatchesAsync(connection, site, recordList, updateStatus);
|
||||||
|
updateStatus?.Invoke("Zentrale Tabelle aktualisiert");
|
||||||
|
|
||||||
await _appEventLogService.WriteAsync(
|
await _appEventLogService.WriteAsync(
|
||||||
"Export",
|
"Export",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
|||||||
private static void EnsureSchema(AppDbContext db)
|
private static void EnsureSchema(AppDbContext db)
|
||||||
{
|
{
|
||||||
EnsureSitesTableSupportsOptionalHanaServer(db);
|
EnsureSitesTableSupportsOptionalHanaServer(db);
|
||||||
|
RepairBrokenSiteForeignKeys(db);
|
||||||
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
||||||
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0");
|
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0");
|
||||||
@@ -175,6 +176,221 @@ FROM Sites_old;";
|
|||||||
enableFk.ExecuteNonQuery();
|
enableFk.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void RepairBrokenSiteForeignKeys(AppDbContext db)
|
||||||
|
{
|
||||||
|
var conn = db.Database.GetDbConnection();
|
||||||
|
if (conn.State != ConnectionState.Open)
|
||||||
|
conn.Open();
|
||||||
|
|
||||||
|
var tablesToRepair = new[]
|
||||||
|
{
|
||||||
|
("ExportLogs", GetExportLogsCreateSql()),
|
||||||
|
("AppEventLogs", GetAppEventLogsCreateSql()),
|
||||||
|
("CentralSalesRecords", GetCentralSalesRecordsCreateSql()),
|
||||||
|
("SapSourceDefinitions", GetSapSourceDefinitionsCreateSql()),
|
||||||
|
("SapJoinDefinitions", GetSapJoinDefinitionsCreateSql()),
|
||||||
|
("SapFieldMappings", GetSapFieldMappingsCreateSql())
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (tableName, createSql) in tablesToRepair)
|
||||||
|
{
|
||||||
|
if (TableReferencesSitesOld(conn, tableName))
|
||||||
|
RebuildTable(conn, tableName, createSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TableReferencesSitesOld(System.Data.Common.DbConnection connection, string tableName)
|
||||||
|
{
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;";
|
||||||
|
|
||||||
|
var parameter = command.CreateParameter();
|
||||||
|
parameter.ParameterName = "$tableName";
|
||||||
|
parameter.Value = tableName;
|
||||||
|
command.Parameters.Add(parameter);
|
||||||
|
|
||||||
|
var sql = command.ExecuteScalar()?.ToString() ?? string.Empty;
|
||||||
|
return sql.Contains("Sites_old", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildTable(System.Data.Common.DbConnection connection, string tableName, string createSql)
|
||||||
|
{
|
||||||
|
using var disableFk = connection.CreateCommand();
|
||||||
|
disableFk.CommandText = "PRAGMA foreign_keys = OFF;";
|
||||||
|
disableFk.ExecuteNonQuery();
|
||||||
|
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
|
||||||
|
var tempTableName = $"{tableName}_repair_old";
|
||||||
|
|
||||||
|
using (var rename = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
rename.Transaction = transaction;
|
||||||
|
rename.CommandText = $"ALTER TABLE {tableName} RENAME TO {tempTableName};";
|
||||||
|
rename.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var create = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
create.Transaction = transaction;
|
||||||
|
create.CommandText = createSql;
|
||||||
|
create.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns = GetSharedColumns(connection, transaction, tableName, tempTableName);
|
||||||
|
if (columns.Count > 0)
|
||||||
|
{
|
||||||
|
var columnList = string.Join(", ", columns);
|
||||||
|
|
||||||
|
using var copy = connection.CreateCommand();
|
||||||
|
copy.Transaction = transaction;
|
||||||
|
copy.CommandText = $"INSERT INTO {tableName} ({columnList}) SELECT {columnList} FROM {tempTableName};";
|
||||||
|
copy.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var drop = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
drop.Transaction = transaction;
|
||||||
|
drop.CommandText = $"DROP TABLE {tempTableName};";
|
||||||
|
drop.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
|
||||||
|
using var enableFk = connection.CreateCommand();
|
||||||
|
enableFk.CommandText = "PRAGMA foreign_keys = ON;";
|
||||||
|
enableFk.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> GetSharedColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction transaction, string newTableName, string oldTableName)
|
||||||
|
{
|
||||||
|
var newColumns = GetTableColumns(connection, transaction, newTableName);
|
||||||
|
var oldColumns = GetTableColumns(connection, transaction, oldTableName);
|
||||||
|
|
||||||
|
return newColumns.Where(oldColumns.Contains).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<string> GetTableColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction transaction, string tableName)
|
||||||
|
{
|
||||||
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.Transaction = transaction;
|
||||||
|
command.CommandText = $"PRAGMA table_info({tableName})";
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var name = reader["name"]?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
columns.Add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetExportLogsCreateSql() => @"
|
||||||
|
CREATE TABLE ExportLogs (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
Timestamp TEXT NOT NULL,
|
||||||
|
SiteId INTEGER NOT NULL,
|
||||||
|
Land TEXT NOT NULL,
|
||||||
|
TSC TEXT NOT NULL,
|
||||||
|
Status TEXT NOT NULL,
|
||||||
|
RowCount INTEGER NOT NULL,
|
||||||
|
ErrorMessage TEXT NULL,
|
||||||
|
FileName TEXT NOT NULL DEFAULT '',
|
||||||
|
FilePath TEXT NOT NULL DEFAULT '',
|
||||||
|
DurationSeconds REAL NOT NULL,
|
||||||
|
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||||
|
);";
|
||||||
|
|
||||||
|
private static string GetAppEventLogsCreateSql() => @"
|
||||||
|
CREATE TABLE AppEventLogs (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
Timestamp TEXT NOT NULL,
|
||||||
|
Level TEXT NOT NULL,
|
||||||
|
Category TEXT NOT NULL,
|
||||||
|
SiteId INTEGER NULL,
|
||||||
|
Land TEXT NOT NULL,
|
||||||
|
Message TEXT NOT NULL,
|
||||||
|
Details TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||||
|
);";
|
||||||
|
|
||||||
|
private static string GetCentralSalesRecordsCreateSql() => @"
|
||||||
|
CREATE TABLE CentralSalesRecords (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
StoredAtUtc TEXT NOT NULL,
|
||||||
|
SiteId INTEGER NOT NULL,
|
||||||
|
SourceSystem TEXT NOT NULL,
|
||||||
|
ExtractionDate TEXT NOT NULL,
|
||||||
|
Tsc TEXT NOT NULL,
|
||||||
|
InvoiceNumber TEXT NOT NULL,
|
||||||
|
PositionOnInvoice INTEGER NOT NULL,
|
||||||
|
Material TEXT NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
ProductGroup TEXT NOT NULL,
|
||||||
|
Quantity TEXT NOT NULL,
|
||||||
|
SupplierNumber TEXT NOT NULL,
|
||||||
|
SupplierName TEXT NOT NULL,
|
||||||
|
SupplierCountry TEXT NOT NULL,
|
||||||
|
CustomerNumber TEXT NOT NULL,
|
||||||
|
CustomerName TEXT NOT NULL,
|
||||||
|
CustomerCountry TEXT NOT NULL,
|
||||||
|
CustomerIndustry TEXT NOT NULL,
|
||||||
|
StandardCost TEXT NOT NULL,
|
||||||
|
StandardCostCurrency TEXT NOT NULL,
|
||||||
|
PurchaseOrderNumber TEXT NOT NULL,
|
||||||
|
SalesPriceValue TEXT NOT NULL,
|
||||||
|
SalesCurrency TEXT NOT NULL,
|
||||||
|
Incoterms2020 TEXT NOT NULL,
|
||||||
|
SalesResponsibleEmployee TEXT NOT NULL,
|
||||||
|
InvoiceDate TEXT NULL,
|
||||||
|
OrderDate TEXT NULL,
|
||||||
|
Land TEXT NOT NULL,
|
||||||
|
DocumentType TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||||
|
);";
|
||||||
|
|
||||||
|
private static string GetSapSourceDefinitionsCreateSql() => @"
|
||||||
|
CREATE TABLE SapSourceDefinitions (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
SiteId INTEGER NOT NULL,
|
||||||
|
Alias TEXT NOT NULL,
|
||||||
|
EntitySet TEXT NOT NULL,
|
||||||
|
IsPrimary INTEGER NOT NULL DEFAULT 0,
|
||||||
|
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||||
|
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||||
|
);";
|
||||||
|
|
||||||
|
private static string GetSapJoinDefinitionsCreateSql() => @"
|
||||||
|
CREATE TABLE SapJoinDefinitions (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
SiteId INTEGER NOT NULL,
|
||||||
|
LeftAlias TEXT NOT NULL,
|
||||||
|
RightAlias TEXT NOT NULL,
|
||||||
|
LeftKeys TEXT NOT NULL,
|
||||||
|
RightKeys TEXT NOT NULL,
|
||||||
|
JoinType TEXT NOT NULL DEFAULT 'Left',
|
||||||
|
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||||
|
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||||
|
);";
|
||||||
|
|
||||||
|
private static string GetSapFieldMappingsCreateSql() => @"
|
||||||
|
CREATE TABLE SapFieldMappings (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
SiteId INTEGER NOT NULL,
|
||||||
|
TargetField TEXT NOT NULL,
|
||||||
|
SourceExpression TEXT NOT NULL,
|
||||||
|
IsRequired INTEGER NOT NULL DEFAULT 0,
|
||||||
|
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||||
|
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||||
|
);";
|
||||||
|
|
||||||
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
||||||
{
|
{
|
||||||
var conn = db.Database.GetDbConnection();
|
var conn = db.Database.GetDbConnection();
|
||||||
|
|||||||
Reference in New Issue
Block a user