using System.Text.RegularExpressions; using Lagerbestand.Data; using Lagerbestand.Exceptions; using Lagerbestand.Models; using Microsoft.EntityFrameworkCore; namespace Lagerbestand.Services; /// /// Result of location code validation. /// public record LocationCodeValidationResult( bool IsValid, string? ErrorMessage = null, AreaType? Area = null, int? Row = null, ShelfPosition? Position = null, int? SlotNumber = null ); /// /// Parsed components of a location code. /// public record ParsedLocationCode(AreaType Area, int Row, ShelfPosition Position, int SlotNumber) { /// /// Generates the 4-digit code string. /// public string ToCode() => $"{(int)Area}{Row}{(int)Position}{SlotNumber}"; } /// /// 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) /// public interface ILocationCodeService { /// /// Generates a location code from component parts. /// /// Storage building /// Row number (Werkstatt: 1-3, Lager: 1-5) /// Shelf position (side/level) /// Slot number (1-9) /// 4-digit location code string string GenerateLocationCode(AreaType area, int row, ShelfPosition position, int slot); /// /// Parses a location code string into its component parts. /// /// 4-digit location code /// Parsed location code components /// If code format is invalid ParsedLocationCode ParseLocationCode(string code); /// /// Tries to parse a location code without throwing exceptions. /// bool TryParseLocationCode(string code, out ParsedLocationCode? result); /// /// Validates a location code format. /// /// Code to validate /// Validation result with details LocationCodeValidationResult ValidateLocationCode(string code); /// /// Finds the next available (existing, unoccupied) location in a given area and row. /// /// Storage building /// Row number /// Parsed code of next free location, or null if row is full Task GetNextAvailableSlotAsync(AreaType area, int row); /// /// Checks if a specific location code is available (exists and not occupied). /// Task IsLocationAvailableAsync(string code); /// /// Checks if a location code already exists in the database. /// Task CodeExistsAsync(string code); /// /// Gets all occupied slots in a specific area and row. /// Task> GetOccupiedSlotsAsync(AreaType area, int row); /// /// Gets all existing slot numbers in a specific area and row. /// Task> GetExistingSlotsAsync(AreaType area, int row); /// /// Formats a location code for display with human-readable text. /// string FormatForDisplay(string code); } /// public class LocationCodeService : ILocationCodeService { private readonly AppDbContext _context; private readonly ILogger _logger; private static readonly Regex CodeRegex = new(StorageLocation.LocationCodePattern, RegexOptions.Compiled); public LocationCodeService(AppDbContext context, ILogger logger) { _context = context; _logger = logger; } /// 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}"; } /// 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 ); } /// 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; } /// 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); } /// public async Task 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); } /// public async Task 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; } /// public async Task CodeExistsAsync(string code) { return await _context.StorageLocations .AnyAsync(l => l.Code == code); } /// public async Task> 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(); } /// public async Task> 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(); } /// 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}"; } }