Release 1.1.0: QR-Reportimport, Regal-Auswahlmodal, Einlagern/Auslagern-Verknuepfungen aus Reifenlager, Stabilitaetsfixes
This commit is contained in:
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()
|
||||
|
||||
Reference in new issue
Block a user