Files
ReifenSpaeth/Lagerbestand/Services/LocationCodeService.cs
T

276 lines
10 KiB
C#

using System.Text.RegularExpressions;
using Lagerbestand.Data;
using Lagerbestand.Exceptions;
using Lagerbestand.Models;
using Microsoft.EntityFrameworkCore;
namespace Lagerbestand.Services;
/// <summary>
/// Result of location code validation.
/// </summary>
public record LocationCodeValidationResult(
bool IsValid,
string? ErrorMessage = null,
AreaType? Area = null,
int? Row = null,
ShelfPosition? Position = null,
int? SlotNumber = null
);
/// <summary>
/// Parsed components of a location code.
/// </summary>
public record ParsedLocationCode(AreaType Area, int Row, ShelfPosition Position, int SlotNumber)
{
/// <summary>
/// Generates the 4-digit code string.
/// </summary>
public string ToCode() => $"{(int)Area}{Row}{(int)Position}{SlotNumber}";
}
/// <summary>
/// Service for generating, parsing, and validating storage location codes.
/// Location codes follow the ABCD format where:
/// - A: Gebäude (1=Werkstatt, 2=Lager)
/// - B: Reihe (Werkstatt: 1-3, Lager: 1-5)
/// - C: Seite/Ebene (1=links oben, 2=links unten, 3=rechts oben, 4=rechts unten)
/// - D: Platz (1-9)
/// </summary>
public interface ILocationCodeService
{
/// <summary>
/// Generates a location code from component parts.
/// </summary>
/// <param name="area">Storage building</param>
/// <param name="row">Row number (Werkstatt: 1-3, Lager: 1-5)</param>
/// <param name="position">Shelf position (side/level)</param>
/// <param name="slot">Slot number (1-9)</param>
/// <returns>4-digit location code string</returns>
string GenerateLocationCode(AreaType area, int row, ShelfPosition position, int slot);
/// <summary>
/// Parses a location code string into its component parts.
/// </summary>
/// <param name="code">4-digit location code</param>
/// <returns>Parsed location code components</returns>
/// <exception cref="InvalidLocationCodeException">If code format is invalid</exception>
ParsedLocationCode ParseLocationCode(string code);
/// <summary>
/// Tries to parse a location code without throwing exceptions.
/// </summary>
bool TryParseLocationCode(string code, out ParsedLocationCode? result);
/// <summary>
/// Validates a location code format.
/// </summary>
/// <param name="code">Code to validate</param>
/// <returns>Validation result with details</returns>
LocationCodeValidationResult ValidateLocationCode(string code);
/// <summary>
/// Finds the next available (existing, unoccupied) location in a given area and row.
/// </summary>
/// <param name="area">Storage building</param>
/// <param name="row">Row number</param>
/// <returns>Parsed code of next free location, or null if row is full</returns>
Task<ParsedLocationCode?> GetNextAvailableSlotAsync(AreaType area, int row);
/// <summary>
/// Checks if a specific location code is available (exists and not occupied).
/// </summary>
Task<bool> IsLocationAvailableAsync(string code);
/// <summary>
/// Checks if a location code already exists in the database.
/// </summary>
Task<bool> CodeExistsAsync(string code);
/// <summary>
/// Gets all occupied slots in a specific area and row.
/// </summary>
Task<List<int>> GetOccupiedSlotsAsync(AreaType area, int row);
/// <summary>
/// Gets all existing slot numbers in a specific area and row.
/// </summary>
Task<List<int>> GetExistingSlotsAsync(AreaType area, int row);
/// <summary>
/// Formats a location code for display with human-readable text.
/// </summary>
string FormatForDisplay(string code);
}
/// <inheritdoc />
public class LocationCodeService : ILocationCodeService
{
private readonly AppDbContext _context;
private readonly ILogger<LocationCodeService> _logger;
private static readonly Regex CodeRegex = new(StorageLocation.LocationCodePattern, RegexOptions.Compiled);
public LocationCodeService(AppDbContext context, ILogger<LocationCodeService> logger)
{
_context = context;
_logger = logger;
}
/// <inheritdoc />
public string GenerateLocationCode(AreaType area, int row, ShelfPosition position, int slot)
{
var maxRow = area.GetMaxRow();
if (row < StorageLocation.MinRow || row > maxRow)
throw new ArgumentOutOfRangeException(nameof(row),
$"Reihe muss für {area.GetDisplayName()} zwischen {StorageLocation.MinRow} und {maxRow} liegen.");
if (slot < StorageLocation.MinSlot || slot > StorageLocation.MaxSlot)
throw new ArgumentOutOfRangeException(nameof(slot),
$"Platz muss zwischen {StorageLocation.MinSlot} und {StorageLocation.MaxSlot} liegen.");
return $"{(int)area}{row}{(int)position}{slot}";
}
/// <inheritdoc />
public ParsedLocationCode ParseLocationCode(string code)
{
var validation = ValidateLocationCode(code);
if (!validation.IsValid)
throw new InvalidLocationCodeException(code, validation.ErrorMessage ?? "Ungültiges Format");
return new ParsedLocationCode(
validation.Area!.Value,
validation.Row!.Value,
validation.Position!.Value,
validation.SlotNumber!.Value
);
}
/// <inheritdoc />
public bool TryParseLocationCode(string code, out ParsedLocationCode? result)
{
result = null;
var validation = ValidateLocationCode(code);
if (!validation.IsValid)
return false;
result = new ParsedLocationCode(
validation.Area!.Value,
validation.Row!.Value,
validation.Position!.Value,
validation.SlotNumber!.Value
);
return true;
}
/// <inheritdoc />
public LocationCodeValidationResult ValidateLocationCode(string code)
{
if (string.IsNullOrWhiteSpace(code))
return new LocationCodeValidationResult(false, "Lagerplatz-Code darf nicht leer sein.");
code = code.Trim();
if (code.Length != 4)
return new LocationCodeValidationResult(false,
$"Lagerplatz-Code muss genau 4 Zeichen lang sein (aktuell: {code.Length}).");
if (!CodeRegex.IsMatch(code))
return new LocationCodeValidationResult(false,
"Ungültiges Format. Erwartet: ABCD wobei A=Gebäude (1-2), B=Reihe, C=Seite/Ebene (1-4), D=Platz (1-9).");
// Parse area (position A)
if (!AreaTypeExtensions.TryFromCode(code[0], out var area))
return new LocationCodeValidationResult(false,
$"Ungültiges Gebäude '{code[0]}'. Erlaubt: 1 (Werkstatt), 2 (Lager).");
// Parse row (position B) - max row depends on building
if (!int.TryParse(code[1].ToString(), out var row))
return new LocationCodeValidationResult(false,
$"Ungültige Reihe '{code[1]}'.");
var maxRow = area.GetMaxRow();
if (row < StorageLocation.MinRow || row > maxRow)
return new LocationCodeValidationResult(false,
$"Reihe {row} außerhalb des gültigen Bereichs für {area.GetDisplayName()} ({StorageLocation.MinRow}-{maxRow}).");
// Parse position (position C)
if (!ShelfPositionExtensions.TryFromCode(code[2], out var position))
return new LocationCodeValidationResult(false,
$"Ungültige Seite/Ebene '{code[2]}'. Erlaubt: 1 (links oben), 2 (links unten), 3 (rechts oben), 4 (rechts unten).");
// Parse slot (position D)
if (!int.TryParse(code[3].ToString(), out var slot) || slot < StorageLocation.MinSlot || slot > StorageLocation.MaxSlot)
return new LocationCodeValidationResult(false,
$"Ungültiger Platz '{code[3]}'. Erlaubt: {StorageLocation.MinSlot}-{StorageLocation.MaxSlot}.");
return new LocationCodeValidationResult(true, null, area, row, position, slot);
}
/// <inheritdoc />
public async Task<ParsedLocationCode?> GetNextAvailableSlotAsync(AreaType area, int row)
{
var freeLocation = await _context.StorageLocations
.AsNoTracking()
.Where(l => l.Area == area && l.Row == row && l.IsActive && l.IsAvailable && !l.IsOccupied)
.OrderBy(l => l.SlotNumber)
.ThenBy(l => l.Position)
.FirstOrDefaultAsync();
if (freeLocation == null)
return null; // Row is full
return new ParsedLocationCode(freeLocation.Area, freeLocation.Row, freeLocation.Position, freeLocation.SlotNumber);
}
/// <inheritdoc />
public async Task<bool> IsLocationAvailableAsync(string code)
{
var validation = ValidateLocationCode(code);
if (!validation.IsValid)
return false;
var location = await _context.StorageLocations
.FirstOrDefaultAsync(l => l.Code == code && l.IsActive);
return location != null && !location.IsOccupied && location.IsAvailable;
}
/// <inheritdoc />
public async Task<bool> CodeExistsAsync(string code)
{
return await _context.StorageLocations
.AnyAsync(l => l.Code == code);
}
/// <inheritdoc />
public async Task<List<int>> GetOccupiedSlotsAsync(AreaType area, int row)
{
return await _context.StorageLocations
.Where(l => l.Area == area && l.Row == row && l.IsActive && l.IsOccupied)
.Select(l => l.SlotNumber)
.OrderBy(s => s)
.ToListAsync();
}
/// <inheritdoc />
public async Task<List<int>> GetExistingSlotsAsync(AreaType area, int row)
{
return await _context.StorageLocations
.Where(l => l.Area == area && l.Row == row && l.IsActive)
.Select(l => l.SlotNumber)
.OrderBy(s => s)
.ToListAsync();
}
/// <inheritdoc />
public string FormatForDisplay(string code)
{
if (!TryParseLocationCode(code, out var parsed) || parsed == null)
return code;
return $"{parsed.Area.GetDisplayName()}, Reihe {parsed.Row}, {parsed.Position.GetDisplayName()}, Platz {parsed.SlotNumber}";
}
}