QR-Scanner: Kamera-Fallbacks und Foto-Upload; weitere Aenderungen
This commit is contained in:
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)
|
||||
|
||||
Reference in new issue
Block a user