using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.RegularExpressions; namespace Lagerbestand.Models; /// /// Represents a physical storage location in the warehouse. /// Storage locations are identified by a 4-digit code in ABCD format: /// - A: Gebäude (1=Werkstatt mit 3 Reihen, 2=Lager mit 5 Reihen) /// - B: Regalreihe (Werkstatt: 1-3, Lager: 1-5) /// - C: Seite/Ebene (1=links oben, 2=links unten, 3=rechts oben, 4=rechts unten) /// - D: Platz längs (1-9) /// Example: 2235 = Lager, Reihe 2, rechts oben, Platz 5 /// public class StorageLocation { /// /// Regex pattern for valid location codes: ABCD format /// A = 1-2 (Gebäude), B = 1-5 (Reihe), C = 1-4 (Seite/Ebene), D = 1-9 (Platz) /// public const string LocationCodePattern = @"^[12][1-5][1-4][1-9]$"; /// /// Minimum row number /// public const int MinRow = 1; /// /// Maximum row number (Lager, Gebäude 2) /// public const int MaxRow = 5; /// /// Maximum row number for Werkstatt (Gebäude 1) /// public const int MaxRowWerkstatt = 3; /// /// Maximum row number for Lager (Gebäude 2) /// public const int MaxRowLager = 5; /// /// Minimum slot number (längs) /// public const int MinSlot = 1; /// /// Maximum slot number (längs) /// public const int MaxSlot = 9; /// /// Number of positions per lengthwise slot (links/rechts × oben/unten) /// public const int PositionsPerSlot = 4; [Key] public Guid Id { get; set; } = Guid.NewGuid(); /// /// Unique 4-digit location code in ABCD format (e.g., "2235"). /// This is the primary human-readable identifier. /// [Required] [MaxLength(4)] [RegularExpression(LocationCodePattern, ErrorMessage = "Ungültiges Code-Format. Erwartet: ABCD (z.B. 2235)")] public string Code { get; set; } = string.Empty; /// /// Storage building (Werkstatt=1, Lager=2). /// Corresponds to position A in the location code. /// [Required] public AreaType Area { get; set; } = AreaType.Lager; /// /// Shelf row number within the building (Werkstatt: 1-3, Lager: 1-5). /// Corresponds to position B in the location code. /// [Required] [Range(MinRow, MaxRow, ErrorMessage = "Reihe muss zwischen 1 und 5 liegen.")] public int Row { get; set; } = 1; /// /// Position within the shelf slot: side (links/rechts) and level (oben/unten). /// Corresponds to position C in the location code. /// [Required] public ShelfPosition Position { get; set; } = ShelfPosition.LinksOben; /// /// Lengthwise slot number within the row (1-9). /// Corresponds to position D in the location code. /// [Required] [Range(MinSlot, MaxSlot, ErrorMessage = "Platz muss zwischen 1 und 9 liegen.")] public int SlotNumber { get; set; } = 1; /// /// Whether this location is currently occupied by a tire stack. /// public bool IsOccupied { get; set; } /// /// The ID of the tire stack currently stored at this location (if occupied). /// public Guid? TireStackId { get; set; } /// /// Navigation property to the tire stack at this location. /// [ForeignKey(nameof(TireStackId))] public TireStack? OccupiedTireStack { get; set; } /// /// When the current tire stack was placed at this location. /// public DateTime? OccupiedSince { get; set; } /// /// Optional notes about this location. /// [MaxLength(500)] public string? Notes { get; set; } /// /// Whether this location is available for new storage (not blocked). /// public bool IsAvailable { get; set; } = true; /// /// Whether this location is active in the system. /// public bool IsActive { get; set; } = true; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } /// /// Navigation property for tire stacks stored at this location (legacy support). /// public ICollection TireStacks { get; set; } = new List(); /// /// Display name showing the human-readable location. /// [NotMapped] public string DisplayName => $"{Area.GetDisplayName()}, Reihe {Row}, {Position.GetDisplayName()}, Platz {SlotNumber}"; /// /// Short display for compact views. /// [NotMapped] public string ShortDisplay => $"{Area.GetDisplayName()} R{Row} {Position.GetShortName()} P{SlotNumber}"; /// /// Full path description of the location. /// [NotMapped] public string FullPath => $"{Area.GetBuildingLabel()} / Reihe {Row} / {Position.GetDisplayName()} / Platz {SlotNumber}"; /// /// Current occupancy count (number of stacks stored). /// [NotMapped] public int CurrentOccupancy => IsOccupied ? 1 : 0; /// /// Capacity is always 1 for the new system (one stack per location). /// [NotMapped] public int Capacity => 1; /// /// Generates the location code from the component parts. /// public void GenerateCode() { Code = $"{(int)Area}{Row}{(int)Position}{SlotNumber}"; } /// /// Parses the location code and populates the Area, Row, Position, and SlotNumber properties. /// /// True if parsing was successful public bool ParseCode() { if (string.IsNullOrWhiteSpace(Code) || Code.Length != 4) return false; if (!Regex.IsMatch(Code, LocationCodePattern)) return false; if (!AreaTypeExtensions.TryFromCode(Code[0], out var area)) return false; if (!int.TryParse(Code[1].ToString(), out var row) || row < MinRow || row > area.GetMaxRow()) return false; if (!ShelfPositionExtensions.TryFromCode(Code[2], out var position)) return false; if (!int.TryParse(Code[3].ToString(), out var slot) || slot < MinSlot || slot > MaxSlot) return false; Area = area; Row = row; Position = position; SlotNumber = slot; return true; } /// /// Validates that the current Code matches the Area, Row, Position, and SlotNumber values. /// public bool ValidateCodeConsistency() { var expectedCode = $"{(int)Area}{Row}{(int)Position}{SlotNumber}"; return Code == expectedCode; } /// /// Assigns a tire stack to this location. /// /// The ID of the tire stack to assign /// If location is already occupied public void AssignTireStack(Guid tireStackId) { if (IsOccupied) throw new InvalidOperationException($"Lagerplatz {Code} ist bereits belegt."); TireStackId = tireStackId; IsOccupied = true; OccupiedSince = DateTime.UtcNow; UpdatedAt = DateTime.UtcNow; } /// /// Clears the tire stack from this location. /// public void ClearLocation() { TireStackId = null; OccupiedTireStack = null; IsOccupied = false; OccupiedSince = null; UpdatedAt = DateTime.UtcNow; } }