Release 1.1.0: QR-Reportimport, Regal-Auswahlmodal, Einlagern/Auslagern-Verknuepfungen aus Reifenlager, Stabilitaetsfixes
This commit is contained in:
1 parent
cba9414e92
commit
3404fb9875
25 files changed
+5106
-744
No files matched your search
@@ -23,10 +23,25 @@ public interface ITireStackService
|
||||
Task<TireStack> CreateAndStoreAsync(TireStack tireStack, Guid storageLocationId, Guid? storedByUserId = null);
|
||||
Task<TireStack> PickupAsync(Guid tireStackId, Guid? pickedUpByUserId = null);
|
||||
Task<List<TireStack>> PickupBatchAsync(IEnumerable<Guid> tireStackIds, Guid? pickedUpByUserId = null);
|
||||
Task<SwapResult> SwapAsync(Guid outgoingTireStackId, TireStack incomingTireStack, Guid? targetLocationId = null, Guid? userId = null);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
Task<string> GenerateTicketNumberAsync(bool isInventory = false);
|
||||
Task<StorageStatistics> GetStatisticsAsync();
|
||||
Task<List<TireStack>> GetExpiringPickupsAsync(int daysAhead = 14);
|
||||
Task<TireInspection> AddInspectionAsync(TireInspection inspection);
|
||||
Task<List<TireInspection>> GetInspectionsAsync(Guid tireStackId);
|
||||
Task<TireInspection?> GetLatestInspectionAsync(Guid tireStackId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a seasonal tire swap (one stack out, another stack in)
|
||||
/// </summary>
|
||||
public class SwapResult
|
||||
{
|
||||
public required TireStack Outgoing { get; init; }
|
||||
public required TireStack Incoming { get; init; }
|
||||
public string? FreedLocationCode { get; init; }
|
||||
public string? NewLocationCode { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -303,13 +318,13 @@ public class TireStackService : ITireStackService
|
||||
var tireStack = await _context.TireStacks
|
||||
.Include(ts => ts.Customer)
|
||||
.Include(ts => ts.StorageLocation)
|
||||
.FirstOrDefaultAsync(ts => ts.Id == tireStackId);
|
||||
.FirstOrDefaultAsync(ts => ts.Id == tireStackId && ts.IsActive);
|
||||
|
||||
if (tireStack == null)
|
||||
throw new InvalidOperationException("Reifenstapel nicht gefunden.");
|
||||
|
||||
if (tireStack.Status != TireStackStatus.Stored && tireStack.Status != TireStackStatus.Reserved)
|
||||
throw new InvalidOperationException("Reifenstapel ist nicht eingelagert.");
|
||||
throw new InvalidOperationException($"Reifenstapel {tireStack.TicketNumber} ist nicht eingelagert.");
|
||||
|
||||
// Clear the storage location (both sides)
|
||||
if (tireStack.StorageLocation != null)
|
||||
@@ -402,6 +417,111 @@ public class TireStackService : ITireStackService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SwapResult> SwapAsync(Guid outgoingTireStackId, TireStack incomingTireStack, Guid? targetLocationId = null, Guid? userId = null)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
try
|
||||
{
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var outgoing = await _context.TireStacks
|
||||
.Include(ts => ts.Customer)
|
||||
.Include(ts => ts.Vehicle)
|
||||
.Include(ts => ts.StorageLocation)
|
||||
.FirstOrDefaultAsync(ts => ts.Id == outgoingTireStackId && ts.IsActive);
|
||||
|
||||
if (outgoing == null)
|
||||
throw new InvalidOperationException("Herauszugebender Reifenstapel nicht gefunden.");
|
||||
if (outgoing.Status != TireStackStatus.Stored && outgoing.Status != TireStackStatus.Reserved)
|
||||
throw new InvalidOperationException("Herauszugebender Reifenstapel ist nicht eingelagert.");
|
||||
if (outgoing.CustomerId == null)
|
||||
throw new InvalidOperationException("Reifenwechsel ist nur für Kundenreifen möglich.");
|
||||
|
||||
var freedLocationId = outgoing.StorageLocationId;
|
||||
var freedLocationCode = outgoing.StorageLocation?.Code;
|
||||
|
||||
// 1. Release outgoing stack: mark as mounted on vehicle, free the location
|
||||
if (outgoing.StorageLocation != null)
|
||||
outgoing.StorageLocation.ClearLocation();
|
||||
|
||||
outgoing.Status = TireStackStatus.OnVehicle;
|
||||
outgoing.PickedUpAt = DateTime.UtcNow;
|
||||
outgoing.PickedUpByUserId = userId;
|
||||
outgoing.StorageLocationId = null;
|
||||
outgoing.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// 2. Resolve incoming stack: existing (from history) or new
|
||||
TireStack incoming;
|
||||
var existing = await _context.TireStacks
|
||||
.FirstOrDefaultAsync(ts => ts.Id == incomingTireStack.Id && ts.IsActive);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
if (existing.Status != TireStackStatus.OnVehicle && existing.Status != TireStackStatus.PickedUp)
|
||||
throw new InvalidOperationException($"Reifenstapel {existing.TicketNumber} kann nicht wieder eingelagert werden (Status: {existing.StatusText}).");
|
||||
if (existing.CustomerId != outgoing.CustomerId)
|
||||
throw new InvalidOperationException("Der einzulagernde Reifenstapel gehört einem anderen Kunden.");
|
||||
|
||||
existing.PickedUpAt = null;
|
||||
existing.PickedUpByUserId = null;
|
||||
existing.ExpectedPickupDate = incomingTireStack.ExpectedPickupDate;
|
||||
if (!string.IsNullOrWhiteSpace(incomingTireStack.Notes))
|
||||
existing.Notes = incomingTireStack.Notes;
|
||||
existing.Condition = incomingTireStack.Condition;
|
||||
if (incomingTireStack.VehicleId != null)
|
||||
existing.VehicleId = incomingTireStack.VehicleId;
|
||||
incoming = existing;
|
||||
}
|
||||
else
|
||||
{
|
||||
incomingTireStack.CustomerId = outgoing.CustomerId;
|
||||
incomingTireStack.VehicleId ??= outgoing.VehicleId;
|
||||
incomingTireStack.CatalogItemId = null;
|
||||
incomingTireStack.ItemCount = TireStack.TiresPerStack;
|
||||
incomingTireStack.PickedUpAt = null;
|
||||
incomingTireStack.PickedUpByUserId = null;
|
||||
incoming = await CreateAsync(incomingTireStack);
|
||||
}
|
||||
|
||||
// 3. Store incoming stack on target (default: the location just freed)
|
||||
var locationId = targetLocationId ?? freedLocationId
|
||||
?? throw new InvalidOperationException("Kein Lagerplatz für die Einlagerung angegeben.");
|
||||
|
||||
incoming = await StoreAsync(incoming, locationId, userId);
|
||||
if (incoming.StorageLocation == null)
|
||||
incoming.StorageLocation = await _context.StorageLocations.FindAsync(locationId);
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInformation("Tire swap: {Outgoing} -> vehicle, {Incoming} -> location {Location}",
|
||||
outgoing.TicketNumber, incoming.TicketNumber, incoming.StorageLocation?.Code);
|
||||
|
||||
return new SwapResult
|
||||
{
|
||||
Outgoing = outgoing,
|
||||
Incoming = incoming,
|
||||
FreedLocationCode = freedLocationCode,
|
||||
NewLocationCode = incoming.StorageLocation?.Code
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_context.ChangeTracker.Clear();
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
throw new InvalidOperationException("Der Lagerplatz wurde zwischenzeitlich geändert. Bitte erneut versuchen.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var tireStack = await _context.TireStacks.FindAsync(id);
|
||||
@@ -501,4 +621,34 @@ public class TireStackService : ITireStackService
|
||||
.OrderBy(ts => ts.ExpectedPickupDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TireInspection> AddInspectionAsync(TireInspection inspection)
|
||||
{
|
||||
var exists = await _context.TireStacks.AnyAsync(ts => ts.Id == inspection.TireStackId);
|
||||
if (!exists)
|
||||
throw new InvalidOperationException("Reifenstapel nicht gefunden.");
|
||||
|
||||
if (inspection.Id == Guid.Empty) inspection.Id = Guid.NewGuid();
|
||||
inspection.CreatedAt = DateTime.UtcNow;
|
||||
|
||||
_context.TireInspections.Add(inspection);
|
||||
await _context.SaveChangesAsync();
|
||||
return inspection;
|
||||
}
|
||||
|
||||
public async Task<List<TireInspection>> GetInspectionsAsync(Guid tireStackId)
|
||||
{
|
||||
return await _context.TireInspections
|
||||
.Where(i => i.TireStackId == tireStackId)
|
||||
.OrderByDescending(i => i.MeasuredAt ?? i.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TireInspection?> GetLatestInspectionAsync(Guid tireStackId)
|
||||
{
|
||||
return await _context.TireInspections
|
||||
.Where(i => i.TireStackId == tireStackId)
|
||||
.OrderByDescending(i => i.MeasuredAt ?? i.CreatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user