Release 1.1.0: QR-Reportimport, Regal-Auswahlmodal, Einlagern/Auslagern-Verknuepfungen aus Reifenlager, Stabilitaetsfixes

This commit is contained in:
spaethf committed 2026-09-26 23:24:14 +02:00
1 parent cba9414e92
commit 3404fb9875
25 files changed
+5106 -744

No files matched your search

@@ -44,85 +44,54 @@ else
</div>
</div>
<div class="stats-bar">
<div class="stat">
<span class="stat-value">@allLocations.Count</span>
<span class="stat-label">Gesamt</span>
<div class="toolbar">
<div class="stat-chips">
<button class="stat-chip @(statusFilter == null && selectedArea == null ? "active" : "")"
@onclick="ResetFilters" title="Alle Filter zurücksetzen">
<span class="chip-value">@allLocations.Count</span> Gesamt
</button>
<button class="stat-chip available @(statusFilter == StatusAvailable ? "active" : "")"
@onclick="FilterByStatusAvailable" title="Nur freie Plätze anzeigen">
<span class="chip-value">@allLocations.Count(l => !l.IsOccupied)</span> Frei
</button>
<button class="stat-chip occupied @(statusFilter == StatusOccupied ? "active" : "")"
@onclick="FilterByStatusOccupied" title="Nur belegte Plätze anzeigen">
<span class="chip-value">@allLocations.Count(l => l.IsOccupied)</span> Belegt
</button>
<button class="stat-chip lager @(selectedArea == AreaType.Lager ? "active" : "")"
@onclick="() => FilterByArea(selectedArea == AreaType.Lager ? null : AreaType.Lager)" title="Nur Lager anzeigen">
<span class="chip-value">@allLocations.Count(l => l.Area == AreaType.Lager)</span> 🏭 Lager
</button>
<button class="stat-chip werkstatt @(selectedArea == AreaType.Werkstatt ? "active" : "")"
@onclick="() => FilterByArea(selectedArea == AreaType.Werkstatt ? null : AreaType.Werkstatt)" title="Nur Werkstatt anzeigen">
<span class="chip-value">@allLocations.Count(l => l.Area == AreaType.Werkstatt)</span> 🔧 Werkstatt
</button>
</div>
<div class="stat available">
<span class="stat-value">@allLocations.Count(l => !l.IsOccupied)</span>
<span class="stat-label">Frei</span>
</div>
<div class="stat occupied">
<span class="stat-value">@allLocations.Count(l => l.IsOccupied)</span>
<span class="stat-label">Belegt</span>
</div>
<div class="stat lager">
<span class="stat-value">@allLocations.Count(l => l.Area == AreaType.Lager)</span>
<span class="stat-label">🏭 Lager</span>
</div>
<div class="stat werkstatt">
<span class="stat-value">@allLocations.Count(l => l.Area == AreaType.Werkstatt)</span>
<span class="stat-label">🔧 Werkstatt</span>
</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<span class="filter-label">Gebäude</span>
<div class="area-filter">
<button class="filter-btn @(selectedArea == null ? "active" : "")"
@onclick="() => FilterByArea(null)">
Alle
</button>
<button class="filter-btn lager @(selectedArea == AreaType.Lager ? "active" : "")"
@onclick="() => FilterByArea(AreaType.Lager)">
🏭 Lager
</button>
<button class="filter-btn werkstatt @(selectedArea == AreaType.Werkstatt ? "active" : "")"
@onclick="() => FilterByArea(AreaType.Werkstatt)">
🔧 Werkstatt
</button>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Reihe</span>
<div class="row-filter">
<select class="form-control" @bind="selectedRow" @bind:after="ApplyFilter">
<option value="">Alle Reihen</option>
@for (int r = StorageLocation.MinRow; r <= StorageLocation.MaxRowLager; r++)
{
<option value="@r">Reihe @r</option>
}
</select>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Status</span>
<div class="status-filter">
<button class="filter-btn @(statusFilter == null ? "active" : "")"
@onclick="() => FilterByStatus(null)">
Alle
</button>
<button class="filter-btn available @(statusFilter == StatusAvailable ? "active" : "")"
@onclick="FilterByStatusAvailable">
Frei
</button>
<button class="filter-btn occupied @(statusFilter == StatusOccupied ? "active" : "")"
@onclick="FilterByStatusOccupied">
Belegt
</button>
</div>
</div>
<div class="filter-group search-group">
<span class="filter-label">Suche</span>
<div class="toolbar-controls">
<div class="search-box">
<input type="text" class="form-control" placeholder="Code suchen (z.B. 2135)..."
<input type="text" class="form-control" placeholder="🔍 Code suchen (z.B. 2135)..."
@bind="searchTerm" @bind:event="oninput" @onkeyup="ApplyFilter" />
</div>
<select class="form-control row-select" @bind="selectedRow" @bind:after="ApplyFilter" title="Nach Reihe filtern">
<option value="">Alle Reihen</option>
@for (int r = StorageLocation.MinRow; r <= StorageLocation.MaxRowLager; r++)
{
<option value="@r">Reihe @r</option>
}
</select>
@if (viewMode == "grid")
{
<button class="btn btn-outline btn-sm" @onclick="ToggleAllRows" title="Alle Reihen ein-/ausklappen">
@(allRowsCollapsed ? "▶ Alle aufklappen" : "▼ Alle zuklappen")
</button>
}
@if (HasActiveFilters)
{
<button class="btn btn-outline btn-sm clear-filters" @onclick="ResetFilters" title="Filter zurücksetzen">
✕ Filter löschen
</button>
}
</div>
</div>
@@ -456,7 +425,85 @@ else
{
var ts = selectedLocation.OccupiedTireStack;
<div class="stored-tires">
<h4>📦 Eingelagerter Reifenstapel</h4>
<div class="stored-tires-header">
<h4>📦 Eingelagerter Reifenstapel</h4>
@if (!isEditingTireStack)
{
<button class="btn btn-outline btn-sm" @onclick="StartEditTireStack">✏️ Bearbeiten</button>
}
</div>
@if (isEditingTireStack && editTireStack != null)
{
<div class="tirestack-edit-form">
<div class="form-group">
<label>Saison</label>
<div class="season-selector">
<button class="season-btn @(editTireStack.Season == TireSeason.Sommer ? "active summer" : "")"
@onclick="() => editTireStack.Season = TireSeason.Sommer">
☀️ Sommer
</button>
<button class="season-btn @(editTireStack.Season == TireSeason.Winter ? "active winter" : "")"
@onclick="() => editTireStack.Season = TireSeason.Winter">
❄️ Winter
</button>
<button class="season-btn @(editTireStack.Season == TireSeason.Allrad ? "active allseason" : "")"
@onclick="() => editTireStack.Season = TireSeason.Allrad">
🔄 Ganzjahr
</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Reifengröße</label>
<TireSizeInput @bind-Value="editTireStack.TireSize" />
</div>
<div class="form-group">
<label>Profiltiefe (mm)</label>
<input type="number" step="0.5" min="0" max="20" class="form-control" @bind="editTireStack.TreadDepth" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Marke</label>
<input type="text" class="form-control" @bind="editTireStack.TireBrand" placeholder="z.B. Michelin" />
</div>
<div class="form-group">
<label>Modell</label>
<input type="text" class="form-control" @bind="editTireStack.TireModel" placeholder="z.B. Pilot Sport 4" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Mit Felgen?</label>
<div class="toggle-group">
<button class="toggle-btn @(editTireStack.WithRims ? "active" : "")"
@onclick="() => editTireStack.WithRims = true">Ja</button>
<button class="toggle-btn @(!editTireStack.WithRims ? "active" : "")"
@onclick="() => editTireStack.WithRims = false">Nein</button>
</div>
</div>
@if (editTireStack.WithRims)
{
<div class="form-group">
<label>Felgengröße</label>
<input type="text" class="form-control" @bind="editTireStack.RimSize" placeholder="z.B. 17 Zoll" />
</div>
}
</div>
<div class="form-group">
<label>Notizen</label>
<textarea class="form-control" rows="2" @bind="editTireStack.Notes"></textarea>
</div>
<div class="form-actions">
<button class="btn btn-outline btn-sm" @onclick="CancelEditTireStack" disabled="@isSavingTireStack">Abbrechen</button>
<button class="btn btn-primary btn-sm" @onclick="SaveTireStack" disabled="@isSavingTireStack">
@(isSavingTireStack ? "Speichern..." : "💾 Speichern")
</button>
</div>
</div>
}
else
{
<div class="stored-item">
<div class="stored-header">
<span class="stored-customer">
@@ -486,6 +533,7 @@ else
Eingelagert am @ts.StoredAt.ToLocalTime().ToString("dd.MM.yyyy") • @ts.DaysStored Tage
</div>
</div>
}
</div>
}
@@ -677,6 +725,11 @@ else
private StorageLocation? selectedLocation;
private string? detailsError;
// Tire stack inline editing
private bool isEditingTireStack;
private bool isSavingTireStack;
private TireStack? editTireStack;
// Delete confirmation
private bool showDeleteConfirm;
private StorageLocation? locationToDelete;
@@ -820,8 +873,44 @@ else
ApplyFilter();
}
private void FilterByStatusAvailable() => FilterByStatus(StatusAvailable);
private void FilterByStatusOccupied() => FilterByStatus(StatusOccupied);
private void FilterByStatusAvailable() => FilterByStatus(statusFilter == StatusAvailable ? null : StatusAvailable);
private void FilterByStatusOccupied() => FilterByStatus(statusFilter == StatusOccupied ? null : StatusOccupied);
private bool HasActiveFilters =>
selectedArea != null || statusFilter != null ||
!string.IsNullOrEmpty(selectedRow) || !string.IsNullOrEmpty(searchTerm);
private void ResetFilters()
{
selectedArea = null;
statusFilter = null;
selectedRow = null;
searchTerm = "";
ApplyFilter();
}
private bool allRowsCollapsed =>
filteredLocations.Count > 0 &&
filteredLocations.GroupBy(l => $"{l.Area}-{l.Row}").All(g => collapsedRows.Contains(g.Key));
private void ToggleAllRows()
{
var rowKeys = filteredLocations
.GroupBy(l => $"{l.Area}-{l.Row}")
.Select(g => g.Key)
.ToList();
if (allRowsCollapsed)
{
foreach (var key in rowKeys)
collapsedRows.Remove(key);
}
else
{
foreach (var key in rowKeys)
collapsedRows.Add(key);
}
}
private void ApplyFilter()
{
@@ -869,6 +958,8 @@ else
private async Task ShowLocationDetails(StorageLocation location)
{
detailsError = null;
isEditingTireStack = false;
editTireStack = null;
selectedLocation = await StorageLocationService.GetByIdAsync(location.Id);
}
@@ -876,6 +967,78 @@ else
{
selectedLocation = null;
detailsError = null;
isEditingTireStack = false;
editTireStack = null;
}
private void StartEditTireStack()
{
var ts = selectedLocation?.OccupiedTireStack;
if (ts == null) return;
// Edit a copy so cancel does not mutate the displayed entity
editTireStack = new TireStack
{
Id = ts.Id,
Season = ts.Season,
TireSize = ts.TireSize,
TireBrand = ts.TireBrand,
TireModel = ts.TireModel,
TreadDepth = ts.TreadDepth,
WithRims = ts.WithRims,
RimSize = ts.RimSize,
Notes = ts.Notes
};
isEditingTireStack = true;
detailsError = null;
}
private void CancelEditTireStack()
{
isEditingTireStack = false;
editTireStack = null;
}
private async Task SaveTireStack()
{
if (editTireStack == null || selectedLocation == null) return;
isSavingTireStack = true;
detailsError = null;
try
{
var stack = await TireStackService.GetByIdAsync(editTireStack.Id);
if (stack == null)
{
detailsError = "Reifenstapel nicht gefunden.";
return;
}
stack.Season = editTireStack.Season;
stack.TireSize = editTireStack.TireSize;
stack.TireBrand = editTireStack.TireBrand;
stack.TireModel = editTireStack.TireModel;
stack.TreadDepth = editTireStack.TreadDepth;
stack.WithRims = editTireStack.WithRims;
stack.RimSize = editTireStack.WithRims ? editTireStack.RimSize : null;
stack.Notes = editTireStack.Notes;
await TireStackService.UpdateAsync(stack);
// Refresh modal + list so chips/season colors update everywhere
selectedLocation = await StorageLocationService.GetByIdAsync(selectedLocation.Id);
await LoadLocations();
isEditingTireStack = false;
editTireStack = null;
}
catch (Exception ex)
{
detailsError = $"Fehler beim Speichern: {ex.Message}";
}
finally
{
isSavingTireStack = false;
}
}
private async Task ShowAddLocation()