Compare commits
3 Commits
12f10a44c4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 83f377cfa0 | |||
| 9003396ca8 | |||
| 80b74d1ded |
+30
-11
@@ -23,7 +23,14 @@
|
|||||||
|
|
||||||
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Wir merken uns die Daten global für den Vollbild-Wechsel
|
||||||
|
window._sankeyData = null;
|
||||||
|
window._sankeyElementId = null;
|
||||||
|
|
||||||
window.drawSankeyChart = function (elementId, dataArray) {
|
window.drawSankeyChart = function (elementId, dataArray) {
|
||||||
|
window._sankeyData = dataArray;
|
||||||
|
window._sankeyElementId = elementId;
|
||||||
|
|
||||||
google.charts.load('current', {'packages':['sankey']});
|
google.charts.load('current', {'packages':['sankey']});
|
||||||
google.charts.setOnLoadCallback(function() {
|
google.charts.setOnLoadCallback(function() {
|
||||||
var data = new google.visualization.DataTable();
|
var data = new google.visualization.DataTable();
|
||||||
@@ -33,22 +40,14 @@
|
|||||||
data.addRows(dataArray);
|
data.addRows(dataArray);
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
backgroundColor: 'transparent', // Wichtig für den Dark-Mode
|
backgroundColor: 'transparent',
|
||||||
sankey: {
|
sankey: {
|
||||||
node: {
|
node: {
|
||||||
nodePadding: 20,
|
nodePadding: 20,
|
||||||
// Ein paar moderne, kräftige Farben
|
|
||||||
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'],
|
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'],
|
||||||
label: {
|
label: { color: '#ffffff', fontName: 'Urbanist', fontSize: 14, bold: true }
|
||||||
color: '#ffffff', // Weiße Schrift, damit sie auf dem dunklen Hintergrund lesbar bleibt
|
|
||||||
fontName: 'Urbanist', // Wir greifen deinen Font auf!
|
|
||||||
fontSize: 14,
|
|
||||||
bold: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
link: {
|
link: { colorMode: 'gradient' }
|
||||||
colorMode: 'gradient' // Sorgt für den fließenden, modernen Look
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,6 +55,26 @@
|
|||||||
chart.draw(data, options);
|
chart.draw(data, options);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NEU: Wenn sich das Fenster (oder Vollbild) ändert, zeichne neu!
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
if (window._sankeyData && window._sankeyElementId && document.getElementById(window._sankeyElementId)) {
|
||||||
|
clearTimeout(window._resizeTimer);
|
||||||
|
window._resizeTimer = setTimeout(function() {
|
||||||
|
window.drawSankeyChart(window._sankeyElementId, window._sankeyData);
|
||||||
|
}, 100); // Minimaler Delay für weiche Übergänge
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NEU: Die Fullscreen-Umschaltung
|
||||||
|
window.toggleFullscreen = function (elementId) {
|
||||||
|
var elem = document.getElementById(elementId);
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
elem.requestFullscreen().catch(err => console.error(err));
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,20 @@
|
|||||||
@inject WorkspaceService DbService
|
@inject WorkspaceService DbService
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Greift NUR, wenn der Container im Vollbildmodus ist */
|
||||||
|
#sankey-fullscreen-container:fullscreen {
|
||||||
|
background-color: var(--mud-palette-background);
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Das Diagramm im Vollbild massiv vergrößern */
|
||||||
|
#sankey-fullscreen-container:fullscreen #sankey-chart {
|
||||||
|
height: calc(100vh - 120px) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8 mb-8">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8 mb-8">
|
||||||
<MudText Typo="Typo.h4" Class="mb-6">Daten-Auswertung</MudText>
|
<MudText Typo="Typo.h4" Class="mb-6">Daten-Auswertung</MudText>
|
||||||
|
|
||||||
@@ -62,7 +76,17 @@
|
|||||||
|
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudPaper Class="pa-6 mt-4" Elevation="1">
|
<MudPaper Class="pa-6 mt-4" Elevation="1">
|
||||||
<MudText Typo="Typo.h6" Class="mb-4">Geldfluss (Sankey-Diagramm)</MudText>
|
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<MudText Typo="Typo.h6">Geldfluss (Sankey-Diagramm)</MudText>
|
||||||
|
<MudTooltip Text="Vollbild umschalten">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Fullscreen"
|
||||||
|
OnClick="ToggleFullscreen"
|
||||||
|
Color="Color.Default" />
|
||||||
|
</MudTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sankey-fullscreen-container">
|
||||||
|
|
||||||
<MudSelect T="string"
|
<MudSelect T="string"
|
||||||
Label="Sichtweise (Klick-Reihenfolge bestimmt den Fluss)"
|
Label="Sichtweise (Klick-Reihenfolge bestimmt den Fluss)"
|
||||||
@@ -78,6 +102,8 @@
|
|||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|
||||||
<div id="sankey-chart" style="width: 100%; height: 400px;"></div>
|
<div id="sankey-chart" style="width: 100%; height: 400px;"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
@@ -200,13 +226,45 @@
|
|||||||
|
|
||||||
if (!_fakten.Any() || _selectedWorkspace == null || !_selectedChartDimensions.Any())
|
if (!_fakten.Any() || _selectedWorkspace == null || !_selectedChartDimensions.Any())
|
||||||
{
|
{
|
||||||
_shouldRenderChart = true; // NEU: Nur noch Flag setzen
|
_shouldRenderChart = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedDims = _selectedChartDimensions.ToList();
|
var selectedDims = _selectedChartDimensions.ToList();
|
||||||
var workspaceLabel = GetUniqueLabel(_selectedWorkspace.Name, _selectedWorkspace.Name);
|
|
||||||
|
|
||||||
|
// --- 1. DURCHGANG: Gesamtsummen pro eindeutigem Pfad berechnen ---
|
||||||
|
var pathAmounts = new Dictionary<string, decimal>();
|
||||||
|
decimal totalWorkspaceAmount = 0;
|
||||||
|
|
||||||
|
foreach (var fakt in _fakten)
|
||||||
|
{
|
||||||
|
totalWorkspaceAmount += fakt.Amount;
|
||||||
|
var currentFullPath = _selectedWorkspace.Name;
|
||||||
|
|
||||||
|
foreach (var dimName in selectedDims)
|
||||||
|
{
|
||||||
|
var val = GetDimensionValue(fakt, dimName);
|
||||||
|
var levels = string.IsNullOrWhiteSpace(val)
|
||||||
|
? new[] { $"Ohne {dimName}" }
|
||||||
|
: val.Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
||||||
|
|
||||||
|
foreach (var level in levels)
|
||||||
|
{
|
||||||
|
var targetFullPath = $"{currentFullPath} / {level}";
|
||||||
|
|
||||||
|
if (!pathAmounts.ContainsKey(targetFullPath))
|
||||||
|
pathAmounts[targetFullPath] = 0;
|
||||||
|
|
||||||
|
pathAmounts[targetFullPath] += fakt.Amount;
|
||||||
|
currentFullPath = targetFullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hauptknoten (Workspace) mit Gesamtsumme versehen
|
||||||
|
var workspaceLabel = GetUniqueLabel(_selectedWorkspace.Name, $"{_selectedWorkspace.Name} ({totalWorkspaceAmount.ToString("N2")} €)");
|
||||||
|
|
||||||
|
// --- 2. DURCHGANG: Diagramm-Daten mit den summierten Labels bauen ---
|
||||||
foreach (var fakt in _fakten)
|
foreach (var fakt in _fakten)
|
||||||
{
|
{
|
||||||
var currentFullPath = _selectedWorkspace.Name;
|
var currentFullPath = _selectedWorkspace.Name;
|
||||||
@@ -215,7 +273,6 @@
|
|||||||
foreach (var dimName in selectedDims)
|
foreach (var dimName in selectedDims)
|
||||||
{
|
{
|
||||||
var val = GetDimensionValue(fakt, dimName);
|
var val = GetDimensionValue(fakt, dimName);
|
||||||
|
|
||||||
var levels = string.IsNullOrWhiteSpace(val)
|
var levels = string.IsNullOrWhiteSpace(val)
|
||||||
? new[] { $"Ohne {dimName}" }
|
? new[] { $"Ohne {dimName}" }
|
||||||
: val.Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
: val.Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
||||||
@@ -223,7 +280,13 @@
|
|||||||
foreach (var level in levels)
|
foreach (var level in levels)
|
||||||
{
|
{
|
||||||
var targetFullPath = $"{currentFullPath} / {level}";
|
var targetFullPath = $"{currentFullPath} / {level}";
|
||||||
var targetLabel = GetUniqueLabel(targetFullPath, level);
|
|
||||||
|
// Wir holen den berechneten Gesamtbetrag für diesen spezifischen Knoten
|
||||||
|
var nodeAmount = pathAmounts[targetFullPath];
|
||||||
|
var shortNameWithAmount = $"{level} ({nodeAmount.ToString("N2")} €)";
|
||||||
|
|
||||||
|
// Unser Zero-Width-Space Hack sorgt weiterhin für die Trennung im Hintergrund
|
||||||
|
var targetLabel = GetUniqueLabel(targetFullPath, shortNameWithAmount);
|
||||||
|
|
||||||
AddSankeyLink(currentSourceLabel, targetLabel, fakt.Amount);
|
AddSankeyLink(currentSourceLabel, targetLabel, fakt.Amount);
|
||||||
|
|
||||||
@@ -233,7 +296,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldRenderChart = true; // NEU: Nur noch Flag setzen
|
_shouldRenderChart = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NEU: Die Magie, die unsichtbare Leerzeichen für eindeutige IDs anhängt ---
|
// --- NEU: Die Magie, die unsichtbare Leerzeichen für eindeutige IDs anhängt ---
|
||||||
@@ -281,4 +344,10 @@
|
|||||||
// Ein winziger Delay stellt sicher, dass Blazor das UI fertig gerendert hat
|
// Ein winziger Delay stellt sicher, dass Blazor das UI fertig gerendert hat
|
||||||
await JSRuntime.InvokeVoidAsync("drawSankeyChart", "sankey-chart", _sankeyData);
|
await JSRuntime.InvokeVoidAsync("drawSankeyChart", "sankey-chart", _sankeyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ToggleFullscreen()
|
||||||
|
{
|
||||||
|
// Wir übergeben die ID unseres neuen Containers
|
||||||
|
await JSRuntime.InvokeVoidAsync("toggleFullscreen", "sankey-fullscreen-container");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,9 @@
|
|||||||
</MudText>
|
</MudText>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
<CardHeaderActions>
|
<CardHeaderActions>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" Color="Color.Default" />
|
<MudIconButton Icon="@Icons.Material.Filled.Settings"
|
||||||
|
Color="Color.Default"
|
||||||
|
Href="@($"/workspace/{ws.Id}")" />
|
||||||
</CardHeaderActions>
|
</CardHeaderActions>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
@page "/workspace/{*WorkspaceId}"
|
||||||
|
@using ZahlenAnalyse.Web.Models
|
||||||
|
@using ZahlenAnalyse.Web.Services
|
||||||
|
@inject WorkspaceService DbService
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
@inject NavigationManager NavManager
|
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8 mb-8">
|
||||||
|
@if (_workspace == null)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Indeterminate="true" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h3" Class="mb-6">@_workspace.Name</MudText>
|
||||||
|
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudPaper Class="pa-6" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-4">Daten importieren</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="mb-4">Lade eine Excel-Datei hoch, um Fakten in diesen Workspace zu importieren.</MudText>
|
||||||
|
|
||||||
|
<MudFileUpload T="IBrowserFile" FilesChanged="@(e => UploadFile(e))" Accept=".xlsx">
|
||||||
|
<CustomContent>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.CloudUpload"
|
||||||
|
OnClick="@context.OpenFilePickerAsync">
|
||||||
|
Excel hochladen
|
||||||
|
</MudButton>
|
||||||
|
</CustomContent>
|
||||||
|
</MudFileUpload>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudPaper Class="pa-6" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-4">Dimensionen</MudText>
|
||||||
|
<MudStack Row="true" Wrap="Wrap.Wrap">
|
||||||
|
@foreach (var dim in _workspace.Dimensions)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Info">@dim.Name</MudChip>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudPaper Class="pa-6" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-4">Letzte Importe / Einträge</MudText>
|
||||||
|
<MudTable Items="_recentFakten" Hover="true" Breakpoint="Breakpoint.Sm">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Datum</MudTh>
|
||||||
|
<MudTh>Betrag</MudTh>
|
||||||
|
<MudTh>Details</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Date.ToShortDateString()</MudTd>
|
||||||
|
<MudTd>@context.Amount.ToString("N2") €</MudTd>
|
||||||
|
<MudTd>@string.Join(" | ", context.Dimensions.Values)</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
}
|
||||||
|
</MudContainer>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string WorkspaceId { get; set; } = string.Empty;
|
||||||
|
private Workspace? _workspace;
|
||||||
|
private List<AnalysisFakt> _recentFakten = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
_workspace = await DbService.GetWorkspaceAsync(WorkspaceId);
|
||||||
|
if (_workspace != null)
|
||||||
|
{
|
||||||
|
_recentFakten = await DbService.GetFaktenForWorkspaceAsync(WorkspaceId);
|
||||||
|
_recentFakten = _recentFakten.Take(5).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UploadFile(Microsoft.AspNetCore.Components.Forms.IBrowserFile e)
|
||||||
|
{
|
||||||
|
var file = e;
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. Datenstrom vom Browser öffnen
|
||||||
|
using var browserStream = file.OpenReadStream(maxAllowedSize: 1024 * 1024 * 10); // 10MB
|
||||||
|
|
||||||
|
// 2. Den Stream komplett in den "Eimer" (Arbeitsspeicher) laden
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await browserStream.CopyToAsync(memoryStream);
|
||||||
|
|
||||||
|
// 3. WICHTIG: Den Lese-Zeiger im Eimer wieder nach ganz oben setzen!
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
|
||||||
|
// 4. Den fertigen MemoryStream an den Service übergeben
|
||||||
|
await DbService.ImportFromExcelAsync(WorkspaceId, memoryStream);
|
||||||
|
|
||||||
|
Snackbar.Add("Import erfolgreich!", Severity.Success);
|
||||||
|
|
||||||
|
// Die Tabelle mit den neuen Daten aktualisieren
|
||||||
|
_recentFakten = (await DbService.GetFaktenForWorkspaceAsync(WorkspaceId)).Take(5).ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Fehler beim Import: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Raven.Client.Documents;
|
using Raven.Client.Documents;
|
||||||
using ZahlenAnalyse.Web.Models;
|
using ZahlenAnalyse.Web.Models;
|
||||||
|
using ExcelDataReader;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
namespace ZahlenAnalyse.Web.Services;
|
namespace ZahlenAnalyse.Web.Services;
|
||||||
|
|
||||||
@@ -118,4 +120,43 @@ public class WorkspaceService
|
|||||||
.OrderByDescending(f => f.Date)
|
.OrderByDescending(f => f.Date)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ImportFromExcelAsync(string workspaceId, Stream fileStream)
|
||||||
|
{
|
||||||
|
// Encoding für deutsche Excel-Dateien
|
||||||
|
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
|
using var reader = ExcelReaderFactory.CreateReader(fileStream);
|
||||||
|
var result = reader.AsDataSet(new ExcelDataSetConfiguration() {
|
||||||
|
ConfigureDataTable = (_) => new ExcelDataTableConfiguration() { UseHeaderRow = true }
|
||||||
|
});
|
||||||
|
|
||||||
|
var dataTable = result.Tables[0];
|
||||||
|
|
||||||
|
using var session = _store.OpenAsyncSession();
|
||||||
|
|
||||||
|
foreach (DataRow row in dataTable.Rows)
|
||||||
|
{
|
||||||
|
var fakt = new AnalysisFakt
|
||||||
|
{
|
||||||
|
WorkspaceId = workspaceId,
|
||||||
|
Date = Convert.ToDateTime(row["Datum"]),
|
||||||
|
Amount = Convert.ToDecimal(row["Betrag"]),
|
||||||
|
// Hier mappen wir dynamisch die Dimensionen
|
||||||
|
Dimensions = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wir gehen davon aus, dass deine Excel-Spaltennamen
|
||||||
|
// den Dimensions-Namen entsprechen
|
||||||
|
foreach (var col in dataTable.Columns.Cast<DataColumn>().Skip(2)) // Datum/Betrag überspringen
|
||||||
|
{
|
||||||
|
fakt.Dimensions[col.ColumnName] = row[col.ColumnName].ToString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnrichWithAuditDataAsync(fakt);
|
||||||
|
await session.StoreAsync(fakt);
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DotNetEnv" Version="3.2.0" />
|
<PackageReference Include="DotNetEnv" Version="3.2.0" />
|
||||||
|
<PackageReference Include="ExcelDataReader" Version="3.8.0" />
|
||||||
|
<PackageReference Include="ExcelDataReader.DataSet" Version="3.8.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.8" />
|
||||||
<PackageReference Include="MudBlazor" Version="9.5.0" />
|
<PackageReference Include="MudBlazor" Version="9.5.0" />
|
||||||
<PackageReference Include="RavenDB.Client" Version="7.2.2" />
|
<PackageReference Include="RavenDB.Client" Version="7.2.2" />
|
||||||
|
|||||||
Reference in New Issue
Block a user