276 lines
10 KiB
C#
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}";
|
|
}
|
|
}
|