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
@@ -0,0 +1,161 @@
|
||||
using Lagerbestand.Data;
|
||||
using Lagerbestand.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Lagerbestand.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Result of merging a local customer into a Lexware customer.
|
||||
/// </summary>
|
||||
public class CustomerMergeResult
|
||||
{
|
||||
public Guid TargetCustomerId { get; set; }
|
||||
public int VehiclesMoved { get; set; }
|
||||
public int TireStacksMoved { get; set; }
|
||||
public int TasksMoved { get; set; }
|
||||
}
|
||||
|
||||
public interface ICustomerMergeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Local customers are created by this app and use the "K-" number prefix.
|
||||
/// Lexware customers carry the numeric Lexware customer number.
|
||||
/// </summary>
|
||||
bool IsLocalCustomer(Customer customer);
|
||||
|
||||
/// <summary>
|
||||
/// Moves all vehicles, tire stacks and task links from a locally created customer
|
||||
/// to an existing Lexware customer and permanently deletes the local customer.
|
||||
/// </summary>
|
||||
Task<CustomerMergeResult> MergeAsync(Guid sourceId, Guid targetId);
|
||||
}
|
||||
|
||||
public class CustomerMergeService : ICustomerMergeService
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly ILogger<CustomerMergeService> _logger;
|
||||
|
||||
public CustomerMergeService(AppDbContext context, ILogger<CustomerMergeService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static bool IsLocalCustomerNumber(string? customerNumber) =>
|
||||
string.IsNullOrWhiteSpace(customerNumber) ||
|
||||
customerNumber.StartsWith("K-", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public bool IsLocalCustomer(Customer customer) => IsLocalCustomerNumber(customer.CustomerNumber);
|
||||
|
||||
public async Task<CustomerMergeResult> MergeAsync(Guid sourceId, Guid targetId)
|
||||
{
|
||||
if (sourceId == targetId)
|
||||
throw new InvalidOperationException("Quell- und Zielkunde dürfen nicht identisch sein.");
|
||||
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
var source = await _context.Customers.FirstOrDefaultAsync(c => c.Id == sourceId)
|
||||
?? throw new InvalidOperationException("Lokaler Kunde wurde nicht gefunden.");
|
||||
var target = await _context.Customers.FirstOrDefaultAsync(c => c.Id == targetId)
|
||||
?? throw new InvalidOperationException("Lexware-Kunde wurde nicht gefunden.");
|
||||
|
||||
if (!IsLocalCustomerNumber(source.CustomerNumber))
|
||||
throw new InvalidOperationException("Nur lokal angelegte Kunden (K-…) können zusammengeführt werden.");
|
||||
if (IsLocalCustomerNumber(target.CustomerNumber))
|
||||
throw new InvalidOperationException("Der Zielkunde muss ein Lexware-Kunde sein.");
|
||||
|
||||
var result = new CustomerMergeResult { TargetCustomerId = targetId };
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Vehicles
|
||||
var targetHasPrimary = await _context.Vehicles
|
||||
.AnyAsync(v => v.CustomerId == targetId && v.IsActive && v.IsPrimary);
|
||||
var vehicles = await _context.Vehicles
|
||||
.Where(v => v.CustomerId == sourceId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var v in vehicles)
|
||||
{
|
||||
v.CustomerId = targetId;
|
||||
if (targetHasPrimary && v.IsPrimary)
|
||||
v.IsPrimary = false;
|
||||
v.UpdatedAt = now;
|
||||
}
|
||||
result.VehiclesMoved = vehicles.Count;
|
||||
|
||||
// Tire stacks (including history / picked-up ones)
|
||||
var stacks = await _context.TireStacks
|
||||
.Where(ts => ts.CustomerId == sourceId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var ts in stacks)
|
||||
{
|
||||
ts.CustomerId = targetId;
|
||||
ts.UpdatedAt = now;
|
||||
}
|
||||
result.TireStacksMoved = stacks.Count;
|
||||
|
||||
// Kanban task links (composite key TaskId + CustomerId -> recreate)
|
||||
var taskLinks = await _context.KanbanTaskCustomers
|
||||
.Where(tc => tc.CustomerId == sourceId)
|
||||
.ToListAsync();
|
||||
|
||||
if (taskLinks.Count > 0)
|
||||
{
|
||||
var taskIds = taskLinks.Select(tc => tc.TaskId).ToList();
|
||||
var existingTargetTaskIds = (await _context.KanbanTaskCustomers
|
||||
.Where(tc => tc.CustomerId == targetId && taskIds.Contains(tc.TaskId))
|
||||
.Select(tc => tc.TaskId)
|
||||
.ToListAsync()).ToHashSet();
|
||||
|
||||
foreach (var link in taskLinks)
|
||||
{
|
||||
if (!existingTargetTaskIds.Contains(link.TaskId))
|
||||
{
|
||||
_context.KanbanTaskCustomers.Add(new KanbanTaskCustomer
|
||||
{
|
||||
TaskId = link.TaskId,
|
||||
CustomerId = targetId,
|
||||
VehicleId = link.VehicleId
|
||||
});
|
||||
result.TasksMoved++;
|
||||
}
|
||||
_context.KanbanTaskCustomers.Remove(link);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill empty contact data on target from source
|
||||
var targetChanged = false;
|
||||
if (string.IsNullOrWhiteSpace(target.Phone) && !string.IsNullOrWhiteSpace(source.Phone)) { target.Phone = source.Phone; targetChanged = true; }
|
||||
if (string.IsNullOrWhiteSpace(target.Email) && !string.IsNullOrWhiteSpace(source.Email)) { target.Email = source.Email; targetChanged = true; }
|
||||
if (!string.IsNullOrWhiteSpace(source.Notes))
|
||||
{
|
||||
var mergedNotes = string.IsNullOrWhiteSpace(target.Notes)
|
||||
? source.Notes
|
||||
: $"{target.Notes}\n[Zusammengeführt aus {source.CustomerNumber}] {source.Notes}";
|
||||
if (mergedNotes.Length > 500) mergedNotes = mergedNotes[..500];
|
||||
target.Notes = mergedNotes;
|
||||
targetChanged = true;
|
||||
}
|
||||
if (!target.IsActive) { target.IsActive = true; targetChanged = true; }
|
||||
if (targetChanged) target.UpdatedAt = now;
|
||||
|
||||
// Persist FK changes first, then hard-delete the source customer
|
||||
await _context.SaveChangesAsync();
|
||||
_context.Customers.Remove(source);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Customer {SourceNumber} merged into {TargetNumber}: {Vehicles} vehicles, {Stacks} tire stacks, {Tasks} task links",
|
||||
source.CustomerNumber, target.CustomerNumber, result.VehiclesMoved, result.TireStacksMoved, result.TasksMoved);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user