Files
ReifenSpaeth/Lagerbestand/Components/Pages/StorageLocations.razor
T

1091 lines
44 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/storage-locations"
@using Lagerbestand.Components.Shared
@inject IStorageLocationService StorageLocationService
@inject ILocationCodeService LocationCodeService
@inject ITireStackService TireStackService
@inject UserSessionService UserSession
<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="stats-bar">
<div class="stat">
<span class="stat-value">@allLocations.Count</span>
<span class="stat-label">Gesamt</span>
</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="search-box">
<input type="text" class="form-control" placeholder="Code suchen (z.B. 2135)..."
@bind="searchTerm" @bind:event="oninput" @onkeyup="ApplyFilter" />
</div>
</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" : "")">
<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>
@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">
@(storedTireStack.Customer?.FullName ?? storedTireStack.CatalogItem?.Name ?? "Lager")
</span>
<span class="tire-info">
@(storedTireStack.Season == TireSeason.Sommer ? "☀️" : storedTireStack.Season == TireSeason.Winter ? "❄️" : "🔄")
</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">
<h4>📦 Eingelagerter Reifenstapel</h4>
<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;
// 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);
}
}
// 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(StatusAvailable);
private void FilterByStatusOccupied() => FilterByStatus(StatusOccupied);
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;
selectedLocation = await StorageLocationService.GetByIdAsync(location.Id);
}
private void CloseDetails()
{
selectedLocation = null;
detailsError = null;
}
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;
}
}
}