1313 lines
56 KiB
Plaintext
1313 lines
56 KiB
Plaintext
@page "/storage-locations"
|
||
@using Lagerbestand.Components.Shared
|
||
@inject IStorageLocationService StorageLocationService
|
||
@inject ILocationCodeService LocationCodeService
|
||
@inject ITireStackService TireStackService
|
||
@inject UserSessionService UserSession
|
||
@inject IStorageRowPdfService RowPdfService
|
||
@inject IJSRuntime JS
|
||
|
||
<PageTitle>Lagerplätze - Reifen Späth Lagerverwaltung</PageTitle>
|
||
|
||
<SeoMetaTags
|
||
Title="Lagerplätze - Reifen Späth Lagerverwaltung"
|
||
Description="Verwaltung der Lagerplätze in der Reifen Späth Lagerverwaltung."
|
||
Robots="noindex, nofollow" />
|
||
|
||
@if (!UserSession.IsAdmin)
|
||
{
|
||
<section class="access-denied" role="alert" aria-labelledby="access-denied-title">
|
||
<div class="access-denied-icon" aria-hidden="true">🔒</div>
|
||
<h1 id="access-denied-title">Zugriff verweigert</h1>
|
||
<p>Diese Seite ist nur für Administratoren zugänglich.</p>
|
||
<a href="/tire-storage" class="btn btn-primary">Zur Lagerübersicht</a>
|
||
</section>
|
||
}
|
||
else
|
||
{
|
||
<main class="locations-page" role="main" @onkeydown="HandleKeyDown">
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<h1>Lagerplätze</h1>
|
||
<span class="location-count">@filteredLocations.Count von @allLocations.Count</span>
|
||
</div>
|
||
<div class="header-actions">
|
||
<div class="view-toggle">
|
||
<button class="view-btn @(viewMode == "grid" ? "active" : "")" @onclick="SetGridView" title="Rasteransicht">
|
||
<span class="icon">⊞</span>
|
||
</button>
|
||
<button class="view-btn @(viewMode == "table" ? "active" : "")" @onclick="SetTableView" title="Tabellenansicht">
|
||
<span class="icon">☰</span>
|
||
</button>
|
||
</div>
|
||
<button class="btn btn-primary" @onclick="ShowAddLocation">
|
||
+ Neuer Platz
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<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 && l.IsAvailable)</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="toolbar-controls">
|
||
<div class="search-box">
|
||
<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>
|
||
|
||
@* Loading State *@
|
||
@if (isLoading)
|
||
{
|
||
<div class="loading-state">
|
||
<div class="spinner"></div>
|
||
<span>Lade Lagerplätze...</span>
|
||
</div>
|
||
}
|
||
else if (filteredLocations.Count == 0)
|
||
{
|
||
<div class="empty-state">
|
||
<div class="empty-icon">📦</div>
|
||
<h3>Keine Lagerplätze</h3>
|
||
<p>@(string.IsNullOrEmpty(searchTerm) ? "Erstellen Sie Lagerplätze für Ihre Reifenlagerung." : "Keine Ergebnisse für Ihre Suche.")</p>
|
||
@if (string.IsNullOrEmpty(searchTerm))
|
||
{
|
||
<button class="btn btn-primary" @onclick="ShowAddLocation">Ersten Platz anlegen</button>
|
||
}
|
||
</div>
|
||
}
|
||
else if (viewMode == "grid")
|
||
{
|
||
@* Grid View - grouped by area and row *@
|
||
@foreach (var areaGroup in filteredLocations.GroupBy(l => l.Area).OrderBy(g => (int)g.Key))
|
||
{
|
||
<div class="area-section @(areaGroup.Key == AreaType.Lager ? "lager" : "werkstatt")">
|
||
<h2 class="area-title">
|
||
@(areaGroup.Key == AreaType.Lager ? "🏭" : "🔧") @areaGroup.Key.GetDisplayName()
|
||
</h2>
|
||
|
||
@foreach (var rowGroup in areaGroup.GroupBy(l => l.Row).OrderBy(g => g.Key))
|
||
{
|
||
var rowKey = $"{areaGroup.Key}-{rowGroup.Key}";
|
||
var isCollapsed = collapsedRows.Contains(rowKey);
|
||
var occupiedCount = rowGroup.Count(l => l.IsOccupied);
|
||
var totalCount = rowGroup.Count();
|
||
<div class="row-section @(isCollapsed ? "collapsed" : "")">
|
||
<div class="row-header-wrap">
|
||
<button type="button" class="row-header" @onclick="() => ToggleRow(rowKey)">
|
||
<span class="row-chevron">@(isCollapsed ? "▶" : "▼")</span>
|
||
<h3 class="row-title">Reihe @rowGroup.Key</h3>
|
||
<span class="row-summary">@occupiedCount / @totalCount belegt</span>
|
||
</button>
|
||
<button type="button" class="btn btn-outline btn-sm row-print-btn"
|
||
title="Reihe als PDF drucken"
|
||
disabled="@(printingRowKey != null)"
|
||
@onclick="() => PrintRow(areaGroup.Key, rowGroup.Key)"
|
||
@onclick:stopPropagation>
|
||
@(printingRowKey == rowKey ? "⏳" : "🖨") PDF
|
||
</button>
|
||
</div>
|
||
@if (printErrorRowKey == rowKey && printError != null)
|
||
{
|
||
<div class="row-print-error" role="alert">@printError</div>
|
||
}
|
||
@if (!isCollapsed)
|
||
{
|
||
@foreach (var posGroup in rowGroup.GroupBy(l => l.Position).OrderBy(g => (int)g.Key))
|
||
{
|
||
<h4 class="position-title">@posGroup.Key.GetDisplayName()</h4>
|
||
<div class="location-grid">
|
||
@foreach (var location in posGroup.OrderBy(l => l.SlotNumber))
|
||
{
|
||
var storedTireStack = location.OccupiedTireStack;
|
||
<div class="location-card @(location.IsOccupied ? "occupied" : "available") @(!location.IsAvailable ? "blocked" : "")"
|
||
@onclick="() => ShowLocationDetails(location)">
|
||
<div class="card-header">
|
||
<span class="location-code">@location.Code</span>
|
||
<span class="slot-number">@location.SlotNumber</span>
|
||
</div>
|
||
@if (location.IsOccupied && storedTireStack != null)
|
||
{
|
||
<div class="card-content">
|
||
<span class="customer-name" title="@(storedTireStack.Customer?.FullName ?? storedTireStack.CatalogItem?.Name ?? "Lager")">
|
||
@(storedTireStack.Customer?.FullName ?? storedTireStack.CatalogItem?.Name ?? "Lager")
|
||
</span>
|
||
@if (storedTireStack.Vehicle != null)
|
||
{
|
||
<span class="vehicle-line" title="@storedTireStack.Vehicle.ShortName">
|
||
<span class="vehicle-model">🚗 @storedTireStack.Vehicle.ShortName</span>
|
||
</span>
|
||
}
|
||
<span class="tire-info">
|
||
<span class="season-icon">@(storedTireStack.Season == TireSeason.Sommer ? "☀️" : storedTireStack.Season == TireSeason.Winter ? "❄️" : "🔄")</span>
|
||
@if (!string.IsNullOrEmpty(storedTireStack.TireSize))
|
||
{
|
||
<span class="tire-size-compact">@storedTireStack.TireSize</span>
|
||
}
|
||
</span>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="card-content empty">
|
||
<span class="empty-text">Frei</span>
|
||
</div>
|
||
}
|
||
@if (!location.IsAvailable)
|
||
{
|
||
<div class="blocked-badge">Gesperrt</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
@* Table View *@
|
||
<div class="location-table-wrapper">
|
||
<table class="location-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="col-status"></th>
|
||
<th class="col-code">Code</th>
|
||
<th class="col-area">Gebäude</th>
|
||
<th class="col-row">Reihe</th>
|
||
<th class="col-position">Seite/Ebene</th>
|
||
<th class="col-slot">Platz</th>
|
||
<th class="col-customer">Kunde/Inhalt</th>
|
||
<th class="col-tires">Reifen</th>
|
||
<th class="col-season">Saison</th>
|
||
<th class="col-stored">Eingelagert</th>
|
||
<th class="col-actions"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var location in filteredLocations)
|
||
{
|
||
var storedTireStack = location.OccupiedTireStack;
|
||
<tr class="@(location.IsOccupied ? "occupied" : "available") @(!location.IsAvailable ? "blocked" : "")"
|
||
@onclick="() => ShowLocationDetails(location)">
|
||
<td class="col-status">
|
||
<span class="status-dot @(location.IsOccupied ? "occupied" : location.IsAvailable ? "available" : "blocked")"></span>
|
||
</td>
|
||
<td class="col-code">
|
||
<strong class="code-mono">@location.Code</strong>
|
||
</td>
|
||
<td class="col-area">
|
||
<span class="area-badge @(location.Area == AreaType.Lager ? "lager" : "werkstatt")">
|
||
@(location.Area == AreaType.Lager ? "🏭" : "🔧") @location.Area.GetDisplayName()
|
||
</span>
|
||
</td>
|
||
<td class="col-row">@location.Row</td>
|
||
<td class="col-position">@location.Position.GetShortName()</td>
|
||
<td class="col-slot">@location.SlotNumber</td>
|
||
<td class="col-customer">
|
||
@if (storedTireStack != null)
|
||
{
|
||
@if (storedTireStack.Customer != null)
|
||
{
|
||
<span class="customer">@storedTireStack.Customer.FullName</span>
|
||
@if (storedTireStack.Vehicle != null)
|
||
{
|
||
<span class="vehicle-info">🚗 @storedTireStack.Vehicle.ShortName</span>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<span class="inventory">📦 @(storedTireStack.CatalogItem?.Name ?? "Lager")</span>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<span class="empty-dash">-</span>
|
||
}
|
||
</td>
|
||
<td class="col-tires">
|
||
@if (storedTireStack != null)
|
||
{
|
||
<span class="tire-count">1 (@TireStack.TiresPerStack)</span>
|
||
@if (!string.IsNullOrEmpty(storedTireStack.TireSize))
|
||
{
|
||
<span class="tire-size">@storedTireStack.TireSize</span>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<span class="empty-dash">-</span>
|
||
}
|
||
</td>
|
||
<td class="col-season">
|
||
@if (storedTireStack != null)
|
||
{
|
||
<span class="season @storedTireStack.Season.ToString().ToLower()">
|
||
@(storedTireStack.Season == TireSeason.Sommer ? "☀️" : storedTireStack.Season == TireSeason.Winter ? "❄️" : "🔄")
|
||
</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="empty-dash">-</span>
|
||
}
|
||
</td>
|
||
<td class="col-stored">
|
||
@if (location.OccupiedSince.HasValue)
|
||
{
|
||
<span class="date">@location.OccupiedSince.Value.ToLocalTime().ToString("dd.MM.yy")</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="empty-dash">-</span>
|
||
}
|
||
</td>
|
||
<td class="col-actions">
|
||
<button class="btn-icon" @onclick="() => ShowLocationDetails(location)" @onclick:stopPropagation="true" title="Details">
|
||
⋯
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
}
|
||
|
||
@* Batch Create Section *@
|
||
<div class="batch-section @(showBatchCreate ? "expanded" : "")">
|
||
<button class="batch-toggle" @onclick="ToggleBatchCreate">
|
||
<span>Schnell-Erstellung</span>
|
||
<span class="toggle-icon">@(showBatchCreate ? "−" : "+")</span>
|
||
</button>
|
||
|
||
@if (showBatchCreate)
|
||
{
|
||
<div class="batch-content">
|
||
<p>Erstellen Sie mehrere Lagerplätze auf einmal – pro Platz werden alle 4 Positionen (links/rechts × oben/unten) angelegt.</p>
|
||
|
||
<div class="batch-form">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Gebäude</label>
|
||
<select class="form-control" @bind="batchArea" @bind:after="OnBatchAreaChanged">
|
||
<option value="@AreaType.Werkstatt">Gebäude 1 – Werkstatt</option>
|
||
<option value="@AreaType.Lager">Gebäude 2 – Lager</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Reihe</label>
|
||
<select class="form-control" @bind="batchRow">
|
||
@for (int r = StorageLocation.MinRow; r <= batchArea.GetMaxRow(); r++)
|
||
{
|
||
<option value="@r">@r</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Plätze (von-bis)</label>
|
||
<div class="range-input">
|
||
<input type="number" class="form-control" @bind="batchSlotStart" min="@StorageLocation.MinSlot" max="@StorageLocation.MaxSlot" />
|
||
<span>-</span>
|
||
<input type="number" class="form-control" @bind="batchSlotEnd" min="@StorageLocation.MinSlot" max="@StorageLocation.MaxSlot" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="batch-preview">
|
||
<span class="preview-label">Vorschau:</span>
|
||
<span class="preview-codes">
|
||
@LocationCodeService.GenerateLocationCode(batchArea, batchRow, ShelfPosition.LinksOben, batchSlotStart) ... @LocationCodeService.GenerateLocationCode(batchArea, batchRow, ShelfPosition.RechtsUnten, batchSlotEnd)
|
||
</span>
|
||
</div>
|
||
|
||
<div class="batch-summary-row">
|
||
<div class="summary-info">
|
||
<span class="summary-count">@batchCount Plätze</span>
|
||
<span class="summary-detail">= @(batchCount * TireStack.TiresPerStack) Reifen Kapazität</span>
|
||
</div>
|
||
<button class="btn btn-primary" @onclick="CreateBatchLocations" disabled="@(!CanCreateBatch || isCreatingBatch)">
|
||
@if (isCreatingBatch)
|
||
{
|
||
<span class="spinner"></span>
|
||
}
|
||
Erstellen
|
||
</button>
|
||
</div>
|
||
|
||
@if (!string.IsNullOrEmpty(batchError))
|
||
{
|
||
<div class="alert @(batchError.Contains("erstellt") ? "alert-success" : "alert-danger")">@batchError</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
@* Location Details Modal *@
|
||
@if (selectedLocation != null)
|
||
{
|
||
<div class="modal-backdrop" @onclick="CloseDetails"></div>
|
||
<div class="app-modal" @onclick="CloseDetails" role="dialog" aria-modal="true" aria-labelledby="details-modal-title">
|
||
<div class="modal-dialog" @onclick:stopPropagation="true">
|
||
<div class="modal-content details-modal">
|
||
<div class="modal-header">
|
||
<div class="modal-title-group">
|
||
<div class="code-badge">
|
||
<span class="code-icon">📍</span>
|
||
<span class="code-text">@selectedLocation.Code</span>
|
||
</div>
|
||
<span id="details-modal-title" class="location-description">@selectedLocation.DisplayName</span>
|
||
</div>
|
||
<button class="btn-close" @onclick="CloseDetails" aria-label="Schließen">✕</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<div class="status-banner @(selectedLocation.IsOccupied ? "occupied" : selectedLocation.IsAvailable ? "available" : "blocked")">
|
||
<span class="status-icon">
|
||
@(selectedLocation.IsOccupied ? "📦" : selectedLocation.IsAvailable ? "✓" : "🚫")
|
||
</span>
|
||
<span class="status-text">
|
||
@(selectedLocation.IsOccupied ? "Belegt" : selectedLocation.IsAvailable ? "Frei & Verfügbar" : "Gesperrt")
|
||
</span>
|
||
</div>
|
||
|
||
<div class="detail-grid">
|
||
<div class="detail-item">
|
||
<span class="label">Gebäude</span>
|
||
<span class="value">
|
||
<span class="area-icon">@(selectedLocation.Area == AreaType.Lager ? "🏭" : "🔧")</span>
|
||
@selectedLocation.Area.GetDisplayName()
|
||
</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="label">Reihe</span>
|
||
<span class="value">@selectedLocation.Row</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="label">Seite/Ebene</span>
|
||
<span class="value">@selectedLocation.Position.GetDisplayName()</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="label">Platz</span>
|
||
<span class="value">@selectedLocation.SlotNumber</span>
|
||
</div>
|
||
@if (selectedLocation.OccupiedSince.HasValue)
|
||
{
|
||
<div class="detail-item">
|
||
<span class="label">Belegt seit</span>
|
||
<span class="value">@selectedLocation.OccupiedSince.Value.ToLocalTime().ToString("dd.MM.yyyy")</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
@if (selectedLocation.IsOccupied && selectedLocation.OccupiedTireStack != null)
|
||
{
|
||
var ts = selectedLocation.OccupiedTireStack;
|
||
<div class="stored-tires">
|
||
<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">
|
||
@(ts.Customer?.FullName ?? ts.CatalogItem?.Name ?? "Lagerbestand")
|
||
</span>
|
||
<span class="stored-ticket">@ts.TicketNumber</span>
|
||
</div>
|
||
<div class="stored-details">
|
||
<span class="detail-chip">1 Stapel (@TireStack.TiresPerStack Reifen)</span>
|
||
@if (!string.IsNullOrEmpty(ts.TireSize))
|
||
{
|
||
<span class="detail-chip">@ts.TireSize</span>
|
||
}
|
||
<span class="detail-chip season-chip @ts.Season.ToString().ToLower()">
|
||
@(ts.Season == TireSeason.Sommer ? "☀️" : ts.Season == TireSeason.Winter ? "❄️" : "🔄") @ts.Season
|
||
</span>
|
||
@if (ts.WithRims)
|
||
{
|
||
<span class="detail-chip">mit Felgen</span>
|
||
}
|
||
</div>
|
||
@if (!string.IsNullOrEmpty(ts.TireBrand))
|
||
{
|
||
<div class="stored-brand">@ts.TireBrand @ts.TireModel</div>
|
||
}
|
||
<div class="stored-date">
|
||
Eingelagert am @ts.StoredAt.ToLocalTime().ToString("dd.MM.yyyy") • @ts.DaysStored Tage
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrEmpty(selectedLocation.Notes))
|
||
{
|
||
<div class="notes-section">
|
||
<span class="label">📝 Notizen</span>
|
||
<p>@selectedLocation.Notes</p>
|
||
</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrEmpty(detailsError))
|
||
{
|
||
<div class="alert alert-danger">@detailsError</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
@if (!selectedLocation.IsOccupied)
|
||
{
|
||
<button class="btn btn-danger" @onclick="() => ConfirmDeleteLocation(selectedLocation)" disabled="@isDeleting">
|
||
@if (isDeleting)
|
||
{
|
||
<span class="spinner small"></span>
|
||
}
|
||
else
|
||
{
|
||
<span>🗑️</span>
|
||
}
|
||
Löschen
|
||
</button>
|
||
}
|
||
<div class="footer-spacer"></div>
|
||
<button class="btn btn-outline" @onclick="() => ToggleAvailability(selectedLocation)">
|
||
@(selectedLocation.IsAvailable ? "🚫 Sperren" : "✓ Freigeben")
|
||
</button>
|
||
<button class="btn btn-primary" @onclick="EditLocation">
|
||
✏️ Bearbeiten
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@* Delete Confirmation Modal *@
|
||
@if (showDeleteConfirm && locationToDelete != null)
|
||
{
|
||
<div class="modal-backdrop" @onclick="CancelDelete"></div>
|
||
<div class="app-modal" @onclick="CancelDelete" role="alertdialog" aria-modal="true" aria-labelledby="delete-modal-title">
|
||
<div class="modal-dialog modal-dialog-sm" @onclick:stopPropagation="true">
|
||
<div class="modal-content confirm-modal">
|
||
<div class="modal-header">
|
||
<h3 id="delete-modal-title">Lagerplatz löschen?</h3>
|
||
<button class="btn-close" @onclick="CancelDelete" aria-label="Schließen">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="confirm-icon">⚠️</div>
|
||
<p class="confirm-text">
|
||
Möchten Sie den Lagerplatz <strong>@locationToDelete.Code</strong> wirklich löschen?
|
||
</p>
|
||
<p class="confirm-subtext">
|
||
@locationToDelete.DisplayName
|
||
</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-outline" @onclick="CancelDelete">Abbrechen</button>
|
||
<button class="btn btn-danger" @onclick="DeleteLocation" disabled="@isDeleting">
|
||
@if (isDeleting)
|
||
{
|
||
<span class="spinner small"></span>
|
||
}
|
||
Endgültig löschen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@* Add/Edit Location Modal - Improved UI/UX *@
|
||
@if (showModal)
|
||
{
|
||
<div class="modal-backdrop" @onclick="HandleModalBackdropClick"></div>
|
||
<div class="app-modal" @onclick="HandleModalBackdropClick" role="dialog" aria-modal="true" aria-labelledby="edit-modal-title">
|
||
<div class="modal-dialog" @onclick:stopPropagation="true">
|
||
<div class="modal-content edit-modal">
|
||
<div class="modal-header">
|
||
<div class="modal-title-wrapper">
|
||
<h3 id="edit-modal-title">@(isEditing ? "Lagerplatz bearbeiten" : "Neuen Stellplatz anlegen")</h3>
|
||
@if (!isEditing)
|
||
{
|
||
<span class="modal-subtitle">Füllen Sie alle Pflichtfelder aus, um einen neuen Stellplatz zu erstellen.</span>
|
||
}
|
||
</div>
|
||
<button class="btn-close" @onclick="CloseModal" aria-label="Schließen" title="Schließen (Esc)">✕</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
@* Success Message *@
|
||
@if (showSuccessMessage)
|
||
{
|
||
<div class="alert alert-success success-message" role="status">
|
||
<span class="success-icon">✓</span>
|
||
<span>@successMessage</span>
|
||
</div>
|
||
}
|
||
|
||
@* Location Code Input Component *@
|
||
<LocationCodeInput
|
||
@ref="locationCodeInputRef"
|
||
@bind-SelectedArea="editArea"
|
||
@bind-SelectedRow="editRow"
|
||
@bind-SelectedPosition="editPosition"
|
||
@bind-SelectedSlot="editSlot"
|
||
@bind-LocationCode="editCode"
|
||
ShowAvailability="@(!isEditing)"
|
||
OnValidationChanged="OnFormValidationChanged"
|
||
OnAvailabilityChanged="OnAvailabilityChanged" />
|
||
|
||
@* Notes Field - Optional *@
|
||
<div class="form-group notes-group">
|
||
<label for="location-notes">
|
||
Notizen
|
||
<span class="optional-indicator">(optional)</span>
|
||
</label>
|
||
<textarea id="location-notes"
|
||
class="form-control"
|
||
rows="2"
|
||
@bind="editNotes"
|
||
placeholder="Zusätzliche Informationen zum Lagerplatz..."
|
||
aria-describedby="notes-hint"></textarea>
|
||
<span id="notes-hint" class="field-hint">z.B. Besonderheiten, Einschränkungen</span>
|
||
</div>
|
||
|
||
@* Error Message *@
|
||
@if (!string.IsNullOrEmpty(errorMessage))
|
||
{
|
||
<div class="alert alert-danger" role="alert">
|
||
<span class="error-icon">⚠</span>
|
||
<span>@errorMessage</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button class="btn btn-outline" @onclick="CloseModal" type="button">
|
||
Abbrechen
|
||
</button>
|
||
<button class="btn btn-primary"
|
||
@onclick="SaveLocation"
|
||
disabled="@(!CanSave || isSaving)"
|
||
type="button">
|
||
@if (isSaving)
|
||
{
|
||
<span class="spinner small"></span>
|
||
<span>@(isEditing ? "Speichern..." : "Anlegen...")</span>
|
||
}
|
||
else
|
||
{
|
||
<span>@(isEditing ? "Speichern" : "Stellplatz anlegen")</span>
|
||
}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
</main>
|
||
}
|
||
|
||
@code {
|
||
// Status filter constants
|
||
private const string StatusAvailable = "available";
|
||
private const string StatusOccupied = "occupied";
|
||
|
||
private List<StorageLocation> allLocations = new();
|
||
private List<StorageLocation> filteredLocations = new();
|
||
private AreaType? selectedArea;
|
||
private string? selectedRow;
|
||
private string? statusFilter;
|
||
private string searchTerm = "";
|
||
private string viewMode = "grid";
|
||
private readonly HashSet<string> collapsedRows = new();
|
||
private bool isLoading = true;
|
||
private bool showBatchCreate = false;
|
||
|
||
// Details modal
|
||
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;
|
||
private bool isDeleting;
|
||
|
||
// Add/Edit modal
|
||
private bool showModal;
|
||
private bool isEditing;
|
||
private Guid editId;
|
||
private AreaType editArea = AreaType.Lager;
|
||
private int editRow = 1;
|
||
private ShelfPosition editPosition = ShelfPosition.LinksOben;
|
||
private int editSlot = 1;
|
||
private string editCode = "";
|
||
private string? editNotes;
|
||
private string? errorMessage;
|
||
private bool isSaving;
|
||
private bool isFormValid;
|
||
private bool isLocationAvailable;
|
||
private bool showSuccessMessage;
|
||
private string? successMessage;
|
||
private LocationCodeInput? locationCodeInputRef;
|
||
|
||
private void ToggleRow(string rowKey)
|
||
{
|
||
if (!collapsedRows.Add(rowKey))
|
||
{
|
||
collapsedRows.Remove(rowKey);
|
||
}
|
||
}
|
||
|
||
private string? printingRowKey;
|
||
private string? printErrorRowKey;
|
||
private string? printError;
|
||
|
||
private async Task PrintRow(AreaType area, int row)
|
||
{
|
||
var rowKey = $"{area}-{row}";
|
||
if (printingRowKey != null) return;
|
||
|
||
printingRowKey = rowKey;
|
||
printError = null;
|
||
printErrorRowKey = null;
|
||
|
||
try
|
||
{
|
||
var rowLocations = allLocations
|
||
.Where(l => l.Area == area && l.Row == row)
|
||
.ToList();
|
||
|
||
var pdf = RowPdfService.GenerateRowPdf(area, row, rowLocations);
|
||
var fileName = $"Lagerplan_{area.GetDisplayName()}_Reihe{row}_{DateTime.Now:yyyyMMdd-HHmm}.pdf";
|
||
await JS.InvokeVoidAsync("appInterop.downloadFile", fileName, Convert.ToBase64String(pdf), "application/pdf");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
printErrorRowKey = rowKey;
|
||
printError = $"PDF konnte nicht erstellt werden: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
printingRowKey = null;
|
||
}
|
||
}
|
||
|
||
// Batch creation
|
||
private AreaType batchArea = AreaType.Lager;
|
||
private int batchRow = 1;
|
||
private int batchSlotStart = 1;
|
||
private int batchSlotEnd = 10;
|
||
private bool isCreatingBatch;
|
||
private string? batchError;
|
||
|
||
private bool CanSave => !string.IsNullOrWhiteSpace(editCode) &&
|
||
LocationCodeService.ValidateLocationCode(editCode).IsValid &&
|
||
(isEditing || isLocationAvailable);
|
||
|
||
private bool CanCreateBatch =>
|
||
batchSlotEnd >= batchSlotStart &&
|
||
batchSlotStart >= StorageLocation.MinSlot &&
|
||
batchSlotEnd <= StorageLocation.MaxSlot;
|
||
|
||
private int batchCount => CanCreateBatch ? (batchSlotEnd - batchSlotStart + 1) * ShelfPositionExtensions.All.Length : 0;
|
||
|
||
private void OnBatchAreaChanged()
|
||
{
|
||
if (batchRow > batchArea.GetMaxRow())
|
||
batchRow = batchArea.GetMaxRow();
|
||
}
|
||
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
if (UserSession.IsAdmin)
|
||
{
|
||
await LoadLocations();
|
||
}
|
||
}
|
||
|
||
private void HandleKeyDown(KeyboardEventArgs e)
|
||
{
|
||
// Close modals on Escape key
|
||
if (e.Key == "Escape")
|
||
{
|
||
if (showModal)
|
||
{
|
||
CloseModal();
|
||
}
|
||
else if (showDeleteConfirm)
|
||
{
|
||
CancelDelete();
|
||
}
|
||
else if (selectedLocation != null)
|
||
{
|
||
CloseDetails();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void HandleModalBackdropClick()
|
||
{
|
||
// Only close if form is empty (not dirty)
|
||
if (!IsFormDirty())
|
||
{
|
||
CloseModal();
|
||
}
|
||
}
|
||
|
||
private bool IsFormDirty()
|
||
{
|
||
if (isEditing) return true; // Always confirm when editing
|
||
return !string.IsNullOrEmpty(editNotes) || editSlot != 1;
|
||
}
|
||
|
||
private void OnFormValidationChanged(bool isValid)
|
||
{
|
||
isFormValid = isValid;
|
||
StateHasChanged();
|
||
}
|
||
|
||
private void OnAvailabilityChanged(bool available)
|
||
{
|
||
isLocationAvailable = available;
|
||
StateHasChanged();
|
||
}
|
||
|
||
private async Task LoadLocations()
|
||
{
|
||
isLoading = true;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
allLocations = await StorageLocationService.GetAllAsync();
|
||
ApplyFilter();
|
||
}
|
||
finally
|
||
{
|
||
isLoading = false;
|
||
}
|
||
}
|
||
|
||
private void SetGridView() => viewMode = "grid";
|
||
private void SetTableView() => viewMode = "table";
|
||
private void ToggleBatchCreate() => showBatchCreate = !showBatchCreate;
|
||
|
||
private void FilterByArea(AreaType? area)
|
||
{
|
||
selectedArea = area;
|
||
ApplyFilter();
|
||
}
|
||
|
||
private void FilterByStatus(string? status)
|
||
{
|
||
statusFilter = status;
|
||
ApplyFilter();
|
||
}
|
||
|
||
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()
|
||
{
|
||
var query = allLocations.AsEnumerable();
|
||
|
||
if (selectedArea.HasValue)
|
||
{
|
||
query = query.Where(l => l.Area == selectedArea.Value);
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(selectedRow) && int.TryParse(selectedRow, out var row))
|
||
{
|
||
query = query.Where(l => l.Row == row);
|
||
}
|
||
|
||
if (statusFilter == StatusAvailable)
|
||
{
|
||
query = query.Where(l => !l.IsOccupied && l.IsAvailable);
|
||
}
|
||
else if (statusFilter == StatusOccupied)
|
||
{
|
||
query = query.Where(l => l.IsOccupied);
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||
{
|
||
var term = searchTerm.ToLower().Trim();
|
||
query = query.Where(l =>
|
||
l.Code.ToLower().Contains(term) ||
|
||
l.Area.GetDisplayName().ToLower().Contains(term) ||
|
||
(l.OccupiedTireStack?.Customer?.FullName?.ToLower().Contains(term) ?? false) ||
|
||
(l.OccupiedTireStack?.Vehicle?.Make?.ToLower().Contains(term) ?? false) ||
|
||
(l.OccupiedTireStack?.Vehicle?.Model?.ToLower().Contains(term) ?? false)
|
||
);
|
||
}
|
||
|
||
filteredLocations = query
|
||
.OrderBy(l => l.Area)
|
||
.ThenBy(l => l.Row)
|
||
.ThenBy(l => l.Position)
|
||
.ThenBy(l => l.SlotNumber)
|
||
.ToList();
|
||
}
|
||
|
||
private async Task ShowLocationDetails(StorageLocation location)
|
||
{
|
||
detailsError = null;
|
||
isEditingTireStack = false;
|
||
editTireStack = null;
|
||
selectedLocation = await StorageLocationService.GetByIdAsync(location.Id);
|
||
}
|
||
|
||
private void CloseDetails()
|
||
{
|
||
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()
|
||
{
|
||
// Find next available location
|
||
var next = await LocationCodeService.GetNextAvailableSlotAsync(AreaType.Lager, 1);
|
||
|
||
editId = Guid.Empty;
|
||
editArea = next?.Area ?? AreaType.Lager;
|
||
editRow = next?.Row ?? 1;
|
||
editPosition = next?.Position ?? ShelfPosition.LinksOben;
|
||
editSlot = next?.SlotNumber ?? 1;
|
||
editCode = LocationCodeService.GenerateLocationCode(editArea, editRow, editPosition, editSlot);
|
||
editNotes = null;
|
||
isEditing = false;
|
||
errorMessage = null;
|
||
isSaving = false;
|
||
showSuccessMessage = false;
|
||
isLocationAvailable = true;
|
||
showModal = true;
|
||
}
|
||
|
||
private void EditLocation()
|
||
{
|
||
if (selectedLocation == null) return;
|
||
|
||
editId = selectedLocation.Id;
|
||
editArea = selectedLocation.Area;
|
||
editRow = selectedLocation.Row;
|
||
editPosition = selectedLocation.Position;
|
||
editSlot = selectedLocation.SlotNumber;
|
||
editCode = selectedLocation.Code;
|
||
editNotes = selectedLocation.Notes;
|
||
isEditing = true;
|
||
errorMessage = null;
|
||
isSaving = false;
|
||
showSuccessMessage = false;
|
||
selectedLocation = null;
|
||
showModal = true;
|
||
}
|
||
|
||
private void CloseModal()
|
||
{
|
||
showModal = false;
|
||
errorMessage = null;
|
||
showSuccessMessage = false;
|
||
isSaving = false;
|
||
}
|
||
|
||
private async Task SaveLocation()
|
||
{
|
||
if (!CanSave || isSaving) return;
|
||
|
||
// Validate form
|
||
if (locationCodeInputRef != null && !locationCodeInputRef.ValidateAll())
|
||
{
|
||
return;
|
||
}
|
||
|
||
isSaving = true;
|
||
errorMessage = null;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
if (isEditing)
|
||
{
|
||
var existing = allLocations.First(l => l.Id == editId);
|
||
existing.Area = editArea;
|
||
existing.Row = editRow;
|
||
existing.Position = editPosition;
|
||
existing.SlotNumber = editSlot;
|
||
existing.Notes = editNotes;
|
||
existing.GenerateCode();
|
||
|
||
await StorageLocationService.UpdateAsync(existing);
|
||
|
||
// Show success and close
|
||
ApplyFilter();
|
||
CloseModal();
|
||
}
|
||
else
|
||
{
|
||
var location = new StorageLocation
|
||
{
|
||
Area = editArea,
|
||
Row = editRow,
|
||
Position = editPosition,
|
||
SlotNumber = editSlot,
|
||
Notes = editNotes
|
||
};
|
||
location.GenerateCode();
|
||
|
||
var created = await StorageLocationService.CreateAsync(location);
|
||
allLocations.Add(created);
|
||
|
||
// Show success message
|
||
successMessage = $"Stellplatz {created.Code} erfolgreich angelegt";
|
||
showSuccessMessage = true;
|
||
StateHasChanged();
|
||
|
||
// Auto-close after delay
|
||
await Task.Delay(1500);
|
||
ApplyFilter();
|
||
CloseModal();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = ex.Message.Contains("existiert")
|
||
? "Dieser Stellplatz ist bereits belegt"
|
||
: $"Fehler beim Anlegen des Stellplatzes: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
isSaving = false;
|
||
}
|
||
}
|
||
|
||
private async Task ToggleAvailability(StorageLocation location)
|
||
{
|
||
location.IsAvailable = !location.IsAvailable;
|
||
location.UpdatedAt = DateTime.UtcNow;
|
||
await StorageLocationService.UpdateAsync(location);
|
||
selectedLocation = null;
|
||
await LoadLocations();
|
||
}
|
||
|
||
private void ConfirmDeleteLocation(StorageLocation location)
|
||
{
|
||
locationToDelete = location;
|
||
showDeleteConfirm = true;
|
||
selectedLocation = null;
|
||
}
|
||
|
||
private void CancelDelete()
|
||
{
|
||
showDeleteConfirm = false;
|
||
locationToDelete = null;
|
||
}
|
||
|
||
private async Task DeleteLocation()
|
||
{
|
||
if (locationToDelete == null) return;
|
||
|
||
isDeleting = true;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
var success = await StorageLocationService.DeleteAsync(locationToDelete.Id);
|
||
if (success)
|
||
{
|
||
allLocations.RemoveAll(l => l.Id == locationToDelete.Id);
|
||
ApplyFilter();
|
||
CancelDelete();
|
||
}
|
||
else
|
||
{
|
||
detailsError = "Lagerplatz konnte nicht gelöscht werden.";
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
detailsError = $"Fehler beim Löschen: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
isDeleting = false;
|
||
}
|
||
}
|
||
|
||
private async Task CreateBatchLocations()
|
||
{
|
||
if (!CanCreateBatch) return;
|
||
|
||
isCreatingBatch = true;
|
||
batchError = null;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
var created = await StorageLocationService.CreateBatchAsync(
|
||
batchArea, batchRow, batchSlotStart, batchSlotEnd);
|
||
|
||
await LoadLocations();
|
||
|
||
var skipped = batchCount - created.Count;
|
||
|
||
if (created.Count > 0)
|
||
{
|
||
batchError = $"✓ {created.Count} Plätze erstellt" + (skipped > 0 ? $", {skipped} übersprungen" : "");
|
||
}
|
||
else if (skipped > 0)
|
||
{
|
||
batchError = $"Alle {skipped} Plätze existieren bereits";
|
||
}
|
||
|
||
// Reset to next row
|
||
batchSlotStart = 1;
|
||
batchSlotEnd = 10;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
batchError = $"Fehler: {ex.Message}";
|
||
}
|
||
finally
|
||
{
|
||
isCreatingBatch = false;
|
||
}
|
||
}
|
||
}
|