QR-Scanner: Kamera-Fallbacks und Foto-Upload; weitere Aenderungen

This commit is contained in:
spaethf committed 2026-09-29 18:08:01 +02:00
1 parent 7a0406eaa2
commit e2aef67099
13 files changed
+965 -58

No files matched your search

@@ -3,6 +3,7 @@
@inject ICustomerService CustomerService
@inject ITireStackService TireStackService
@inject ILexwareSyncService LexwareSyncService
@inject ICustomerMergeService CustomerMergeService
@inject ISiteSettingsService SiteSettingsService
@inject UserSessionService UserSession
@inject IJSRuntime JS
@@ -187,6 +188,12 @@ else
<button class="btn btn-sm btn-outline" @onclick="() => EditCustomer(customer)">
Bearbeiten
</button>
@if (lexwareEnabled && CustomerMergeService.IsLocalCustomer(customer))
{
<button class="btn btn-sm btn-outline" @onclick="() => OpenMerge(customer)" title="Lokalen Kunden mit einem Lexware-Kunden zusammenführen">
Mit Lexware-Kunde zusammenführen
</button>
}
<a href="/tire-checkin" class="btn btn-sm btn-primary">
Reifenstapel einlagern
</a>
@@ -329,6 +336,88 @@ else
</div>
</div>
}
@* Merge local customer into Lexware customer *@
@if (showMergeModal && mergeSource != null)
{
<div class="modal-backdrop" @onclick="CloseMerge"></div>
<div class="app-modal" @onclick="CloseMerge" role="dialog" aria-modal="true" aria-labelledby="merge-title">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="merge-title">Kunden zusammenführen</h5>
<button type="button" class="btn-close" @onclick="CloseMerge" aria-label="Dialog schließen"></button>
</div>
<div class="modal-body">
<div class="merge-source">
<span class="label">Lokaler Kunde</span>
<strong>@mergeSource.DisplayName</strong>
<span class="merge-number">@mergeSource.CustomerNumber</span>
</div>
<label class="form-label" for="merge-search">Lexware-Kunde auswählen</label>
<input id="merge-search" type="text" class="form-control" placeholder="Name, Firma, Kundennummer, Telefon…"
@bind="mergeSearch" @bind:event="oninput" @bind:after="FilterMergeCandidates" />
<ul class="merge-candidates" role="listbox">
@if (mergeCandidates.Count == 0)
{
<li class="merge-empty">Keine passenden Lexware-Kunden gefunden.</li>
}
@foreach (var c in mergeCandidates)
{
<li @key="c.Id" class="merge-candidate @(mergeTarget?.Id == c.Id ? "selected" : "")"
role="option" aria-selected="@(mergeTarget?.Id == c.Id)"
@onclick="() => SelectMergeTarget(c)">
<div class="merge-candidate-main">
<strong>@c.DisplayName</strong>
<span class="merge-number">@c.CustomerNumber</span>
@if (mergeSuggestionIds.Contains(c.Id))
{
<span class="merge-suggest-badge">Vorschlag</span>
}
</div>
<div class="merge-candidate-sub">
@if (!string.IsNullOrEmpty(c.Phone)) { <span>@c.Phone</span> }
@if (!string.IsNullOrEmpty(c.Email)) { <span>@c.Email</span> }
</div>
</li>
}
</ul>
@if (mergeTarget != null)
{
<div class="merge-summary">
<p>Folgende Daten werden auf <strong>@mergeTarget.DisplayName</strong> (@mergeTarget.CustomerNumber) übertragen:</p>
<ul>
<li>@mergeVehicleCount Fahrzeug(e)</li>
<li>@mergeStackCount Reifenstapel (inkl. Historie)</li>
<li>Verknüpfte Aufgaben und Notizen</li>
</ul>
<p class="merge-warning">Der lokale Kunde <strong>@mergeSource.CustomerNumber</strong> wird anschließend endgültig gelöscht.</p>
<label class="merge-confirm">
<input type="checkbox" @bind="mergeConfirmed" />
Ich habe verstanden, dass dieser Vorgang nicht rückgängig gemacht werden kann.
</label>
</div>
}
@if (!string.IsNullOrEmpty(mergeError))
{
<div class="alert alert-danger">@mergeError</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" @onclick="CloseMerge" disabled="@isMerging">Abbrechen</button>
<button type="button" class="btn btn-danger" @onclick="ExecuteMerge"
disabled="@(mergeTarget == null || !mergeConfirmed || isMerging)">
@(isMerging ? "Wird zusammengeführt…" : "Zusammenführen")
</button>
</div>
</div>
</div>
</div>
}
}
@code {
@@ -362,6 +451,152 @@ else
private bool CanSave =>
!string.IsNullOrWhiteSpace(editCustomer.LastName);
// Merge state
private bool showMergeModal;
private Customer? mergeSource;
private Customer? mergeTarget;
private string mergeSearch = "";
private List<Customer> mergeCandidates = new();
private HashSet<Guid> 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<Customer> 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)