@page "/customers" @implements IAsyncDisposable @inject ICustomerService CustomerService @inject ITireStackService TireStackService @inject ILexwareSyncService LexwareSyncService @inject ICustomerMergeService CustomerMergeService @inject ISiteSettingsService SiteSettingsService @inject UserSessionService UserSession @inject IJSRuntime JS @using Lagerbestand.Components.Shared Kundenverwaltung - Reifen Späth Lagerverwaltung @if (!UserSession.IsAdmin) { } else {
@if (!string.IsNullOrEmpty(syncMessage)) {
@syncMessage
} @* Search *@ @* Customer List *@ @if (isLoading) {
Lade Kunden...
} else if (filteredCustomers.Count == 0) {

Keine Kunden

@(string.IsNullOrEmpty(searchTerm) ? "Noch keine Kunden angelegt." : "Keine Ergebnisse für Ihre Suche.")

} else {
@foreach (var customer in filteredCustomers) {
@customer.FullName @if (!string.IsNullOrEmpty(customer.Company)) { @customer.Company } @customer.CustomerNumber
@{ var activeVehicles = customer.Vehicles?.Where(v => v.IsActive).ToList() ?? new List(); } @if (activeVehicles.Any()) { @string.Join(", ", activeVehicles.Select(v => v.ShortName)) } @(customer.TireStacks?.Count(ts => ts.Status == TireStackStatus.Stored) ?? 0) Stapel
@if (expandedId == customer.Id) {
@if (!string.IsNullOrEmpty(customer.Phone)) {
Telefon
@customer.Phone
} @if (!string.IsNullOrEmpty(customer.Email)) { } @if (customer.PrimaryVehicle is { } primaryVehicle) {
Fahrzeug
@primaryVehicle.DisplayName
}
Kunde seit
@if (!string.IsNullOrEmpty(customer.Notes)) {
Notizen @customer.Notes
} @* Customer's Tire Stacks *@ @if (customerTireStacks.TryGetValue(customer.Id, out var tireStacks) && tireStacks.Count > 0) {

Eingelagerte Reifenstapel

    @foreach (var ts in tireStacks.Where(t => t.Status == TireStackStatus.Stored)) {
  • @(ts.Season == TireSeason.Sommer ? "☀️" : ts.Season == TireSeason.Winter ? "❄️" : "🔄") @ts.TireSize (@TireStack.TiresPerStack Reifen) @(ts.StorageLocation?.Code ?? "-")
  • }
}
}
}
}
@* Add/Edit Customer Modal *@ @if (showModal) { } @* Merge local customer into Lexware customer *@ @if (showMergeModal && mergeSource != null) { } } @code { private List allCustomers = new(); private List filteredCustomers = new(); private Dictionary> customerTireStacks = new(); private string searchTerm = ""; private Guid? expandedId; private bool isLoading = true; // Modal state private bool showModal; private bool isEditing; private Customer editCustomer = new(); private string? errorMessage; // Vehicle editing state private List editVehicles = new(); private Vehicle editVehicle = new(); private bool showVehicleForm; private bool isEditingVehicle; private List vehiclesToDelete = new(); // Lexware sync state private bool lexwareEnabled; private bool isSyncing; private string? syncMessage; private string syncMessageType = "success"; private bool CanSave => !string.IsNullOrWhiteSpace(editCustomer.LastName); // Merge state private bool showMergeModal; private Customer? mergeSource; private Customer? mergeTarget; private string mergeSearch = ""; private List mergeCandidates = new(); private HashSet mergeSuggestionIds = new(); private int mergeVehicleCount; private int mergeStackCount; private bool mergeConfirmed; private bool isMerging; private string? mergeError; private async Task OpenMerge(Customer customer) { mergeSource = customer; mergeTarget = null; mergeSearch = ""; mergeConfirmed = false; mergeError = null; mergeVehicleCount = customer.Vehicles.Count(v => v.IsActive); if (!customerTireStacks.TryGetValue(customer.Id, out var stacks)) { stacks = await TireStackService.GetByCustomerAsync(customer.Id); customerTireStacks[customer.Id] = stacks; } mergeStackCount = stacks.Count; var lastName = customer.LastName.Trim().ToLowerInvariant(); var phone = NormalizePhone(customer.Phone); var email = customer.Email?.Trim().ToLowerInvariant(); mergeSuggestionIds = allCustomers .Where(c => c.Id != customer.Id && !CustomerMergeService.IsLocalCustomer(c)) .Where(c => (lastName.Length > 0 && c.LastName.Trim().ToLowerInvariant() == lastName) || (phone.Length > 0 && NormalizePhone(c.Phone) == phone) || (!string.IsNullOrEmpty(email) && c.Email?.Trim().ToLowerInvariant() == email)) .Select(c => c.Id) .ToHashSet(); FilterMergeCandidates(); showMergeModal = true; } private void FilterMergeCandidates() { if (mergeSource == null) return; var term = mergeSearch.Trim().ToLowerInvariant(); IEnumerable query = allCustomers .Where(c => c.Id != mergeSource.Id && !CustomerMergeService.IsLocalCustomer(c)); if (term.Length > 0) { query = query.Where(c => c.FullName.ToLowerInvariant().Contains(term) || (c.Company?.ToLowerInvariant().Contains(term) ?? false) || c.CustomerNumber.ToLowerInvariant().Contains(term) || (c.Phone?.Contains(term) ?? false) || (c.Email?.ToLowerInvariant().Contains(term) ?? false)); } mergeCandidates = query .OrderByDescending(c => mergeSuggestionIds.Contains(c.Id)) .ThenBy(c => c.LastName) .ThenBy(c => c.FirstName) .Take(term.Length > 0 ? 50 : 20) .ToList(); if (mergeTarget != null && mergeCandidates.All(c => c.Id != mergeTarget.Id)) { mergeTarget = null; mergeConfirmed = false; } } private void SelectMergeTarget(Customer customer) { mergeTarget = customer; mergeConfirmed = false; mergeError = null; } private void CloseMerge() { if (isMerging) return; showMergeModal = false; mergeSource = null; mergeTarget = null; mergeCandidates = new(); mergeSuggestionIds = new(); mergeError = null; } private async Task ExecuteMerge() { if (mergeSource == null || mergeTarget == null || !mergeConfirmed) return; isMerging = true; mergeError = null; try { var result = await CustomerMergeService.MergeAsync(mergeSource.Id, mergeTarget.Id); customerTireStacks.Remove(mergeSource.Id); customerTireStacks.Remove(mergeTarget.Id); syncMessage = $"Kunde {mergeSource.CustomerNumber} wurde mit {mergeTarget.DisplayName} ({mergeTarget.CustomerNumber}) zusammengeführt: " + $"{result.VehiclesMoved} Fahrzeug(e), {result.TireStacksMoved} Reifenstapel, {result.TasksMoved} Aufgabe(n) übertragen."; syncMessageType = "success"; var targetId = result.TargetCustomerId; isMerging = false; CloseMerge(); await LoadCustomers(); SearchCustomers(); expandedId = targetId; await ToggleCustomerLoadOnly(targetId); } catch (Exception ex) { mergeError = $"Zusammenführen fehlgeschlagen: {ex.Message}"; } finally { isMerging = false; } } private async Task ToggleCustomerLoadOnly(Guid id) { if (!customerTireStacks.ContainsKey(id)) { customerTireStacks[id] = await TireStackService.GetByCustomerAsync(id); } } private static string NormalizePhone(string? phone) => string.IsNullOrWhiteSpace(phone) ? "" : new string(phone.Where(char.IsDigit).ToArray()); protected override async Task OnInitializedAsync() { if (UserSession.IsAdmin) { await LoadCustomers(); await CheckLexwareEnabled(); } } private DotNetObjectReference? _visibilityRef; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && UserSession.IsAdmin) { _visibilityRef = DotNetObjectReference.Create(this); await VisibilityRefresh.RegisterAsync(JS, _visibilityRef, nameof(OnVisibilityReturn), 120); } } [JSInvokable] public async Task OnVisibilityReturn(int hiddenSeconds) { if (!UserSession.IsAdmin) return; await LoadCustomers(); await InvokeAsync(StateHasChanged); } public async ValueTask DisposeAsync() { await VisibilityRefresh.UnregisterAsync(JS); _visibilityRef?.Dispose(); } private async Task CheckLexwareEnabled() { try { var settings = await SiteSettingsService.GetLexwareApiSettingsAsync(); lexwareEnabled = settings.IsEnabled && settings.IsConfigured; } catch { } } private async Task SyncFromLexware() { isSyncing = true; syncMessage = null; StateHasChanged(); try { var result = await LexwareSyncService.SyncCustomersAsync(); syncMessage = result.Message; syncMessageType = result.Success ? "success" : "error"; if (result.Success) { await LoadCustomers(); } } catch (Exception ex) { syncMessage = $"Fehler: {ex.Message}"; syncMessageType = "error"; } finally { isSyncing = false; } } private async Task LoadCustomers() { isLoading = true; StateHasChanged(); try { allCustomers = await CustomerService.GetAllAsync(); filteredCustomers = allCustomers; } finally { isLoading = false; } } private void SearchCustomers() { if (string.IsNullOrWhiteSpace(searchTerm)) { filteredCustomers = allCustomers; return; } var term = searchTerm.ToLower(); filteredCustomers = allCustomers .Where(c => c.FullName.ToLower().Contains(term) || (c.Company?.ToLower().Contains(term) ?? false) || c.Vehicles.Any(v => v.Make.ToLower().Contains(term) || (v.Model?.ToLower().Contains(term) ?? false)) || c.CustomerNumber.ToLower().Contains(term) || (c.Phone?.Contains(term) ?? false)) .ToList(); } private async Task ToggleCustomer(Guid id) { if (expandedId == id) { expandedId = null; return; } expandedId = id; // Load tire stacks for this customer if not already loaded if (!customerTireStacks.ContainsKey(id)) { var tireStacks = await TireStackService.GetByCustomerAsync(id); customerTireStacks[id] = tireStacks; } } private void ShowAddCustomer() { editCustomer = new Customer(); editVehicles = new List(); vehiclesToDelete = new List(); isEditing = false; errorMessage = null; showVehicleForm = false; showModal = true; } private async Task EditCustomer(Customer customer) { editCustomer = new Customer { Id = customer.Id, CustomerNumber = customer.CustomerNumber, FirstName = customer.FirstName, LastName = customer.LastName, Company = customer.Company, Phone = customer.Phone, Email = customer.Email, Notes = customer.Notes }; // Load vehicles for editing var vehicles = await CustomerService.GetVehiclesAsync(customer.Id); editVehicles = vehicles.Select(v => new Vehicle { Id = v.Id, CustomerId = v.CustomerId, Make = v.Make, Model = v.Model, TireSize = v.TireSize, IsPrimary = v.IsPrimary, IsActive = v.IsActive, Notes = v.Notes }).ToList(); vehiclesToDelete = new List(); isEditing = true; errorMessage = null; showVehicleForm = false; showModal = true; } private void CloseModal() { showModal = false; editCustomer = new Customer(); editVehicles = new List(); vehiclesToDelete = new List(); showVehicleForm = false; errorMessage = null; } // Vehicle management methods private void AddEditVehicle() { editVehicle = new Vehicle(); isEditingVehicle = false; showVehicleForm = true; } private void EditVehicle(Vehicle vehicle) { editVehicle = new Vehicle { Id = vehicle.Id, CustomerId = vehicle.CustomerId, Make = vehicle.Make, Model = vehicle.Model, TireSize = vehicle.TireSize, IsPrimary = vehicle.IsPrimary, IsActive = vehicle.IsActive, Notes = vehicle.Notes }; isEditingVehicle = true; showVehicleForm = true; } private void CancelVehicleForm() { editVehicle = new Vehicle(); showVehicleForm = false; isEditingVehicle = false; } private void SaveVehicle() { if (string.IsNullOrWhiteSpace(editVehicle.Make) || string.IsNullOrWhiteSpace(editVehicle.TireSize)) return; if (isEditingVehicle) { var existing = editVehicles.FirstOrDefault(v => v.Id == editVehicle.Id); if (existing != null) { existing.Make = editVehicle.Make; existing.Model = editVehicle.Model; existing.TireSize = editVehicle.TireSize; existing.Notes = editVehicle.Notes; } } else { var newVehicle = new Vehicle { Id = Guid.NewGuid(), Make = editVehicle.Make, Model = editVehicle.Model, TireSize = editVehicle.TireSize, IsPrimary = editVehicles.Count == 0, // First vehicle is primary IsActive = true }; editVehicles.Add(newVehicle); } showVehicleForm = false; editVehicle = new Vehicle(); isEditingVehicle = false; } private void RemoveVehicle(Vehicle vehicle) { editVehicles.Remove(vehicle); // Track for deletion if it's an existing vehicle if (vehicle.CustomerId != Guid.Empty) { vehiclesToDelete.Add(vehicle.Id); } // Ensure there's still a primary if vehicles remain if (editVehicles.Any() && !editVehicles.Any(v => v.IsPrimary)) { editVehicles.First().IsPrimary = true; } } private void SetPrimaryVehicle(Vehicle vehicle) { foreach (var v in editVehicles) { v.IsPrimary = v.Id == vehicle.Id; } } private async Task SaveCustomer() { if (!CanSave) return; try { if (isEditing) { var existing = allCustomers.First(c => c.Id == editCustomer.Id); existing.FirstName = editCustomer.FirstName; existing.LastName = editCustomer.LastName; existing.Company = editCustomer.Company; existing.Phone = editCustomer.Phone; existing.Email = editCustomer.Email; existing.Notes = editCustomer.Notes; await CustomerService.UpdateAsync(existing); // Delete removed vehicles foreach (var vehicleId in vehiclesToDelete) { await CustomerService.DeleteVehicleAsync(vehicleId); } // Update or add vehicles foreach (var vehicle in editVehicles) { if (vehicle.CustomerId == Guid.Empty) { // New vehicle vehicle.CustomerId = existing.Id; await CustomerService.AddVehicleAsync(existing.Id, vehicle); } else { // Existing vehicle await CustomerService.UpdateVehicleAsync(vehicle); } } // Handle primary vehicle var primary = editVehicles.FirstOrDefault(v => v.IsPrimary); if (primary != null) { await CustomerService.SetPrimaryVehicleAsync(existing.Id, primary.Id); } // Reload to get updated vehicles await LoadCustomers(); } else { // Create new customer if (editVehicles.Any()) { var firstVehicle = editVehicles.First(); var created = await CustomerService.CreateWithVehicleAsync(editCustomer, firstVehicle); // Add remaining vehicles for (int i = 1; i < editVehicles.Count; i++) { await CustomerService.AddVehicleAsync(created.Id, editVehicles[i]); } allCustomers.Insert(0, created); } else { var created = await CustomerService.CreateAsync(editCustomer); allCustomers.Insert(0, created); } } SearchCustomers(); CloseModal(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } }