Files
ReifenSpaeth/Lagerbestand/Models/StorageLocation.cs
T

253 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.RegularExpressions;
namespace Lagerbestand.Models;
/// <summary>
/// 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
/// </summary>
public class StorageLocation
{
/// <summary>
/// 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)
/// </summary>
public const string LocationCodePattern = @"^[12][1-5][1-4][1-9]$";
/// <summary>
/// Minimum row number
/// </summary>
public const int MinRow = 1;
/// <summary>
/// Maximum row number (Lager, Gebäude 2)
/// </summary>
public const int MaxRow = 5;
/// <summary>
/// Maximum row number for Werkstatt (Gebäude 1)
/// </summary>
public const int MaxRowWerkstatt = 3;
/// <summary>
/// Maximum row number for Lager (Gebäude 2)
/// </summary>
public const int MaxRowLager = 5;
/// <summary>
/// Minimum slot number (längs)
/// </summary>
public const int MinSlot = 1;
/// <summary>
/// Maximum slot number (längs)
/// </summary>
public const int MaxSlot = 9;
/// <summary>
/// Number of positions per lengthwise slot (links/rechts × oben/unten)
/// </summary>
public const int PositionsPerSlot = 4;
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Unique 4-digit location code in ABCD format (e.g., "2235").
/// This is the primary human-readable identifier.
/// </summary>
[Required]
[MaxLength(4)]
[RegularExpression(LocationCodePattern, ErrorMessage = "Ungültiges Code-Format. Erwartet: ABCD (z.B. 2235)")]
public string Code { get; set; } = string.Empty;
/// <summary>
/// Storage building (Werkstatt=1, Lager=2).
/// Corresponds to position A in the location code.
/// </summary>
[Required]
public AreaType Area { get; set; } = AreaType.Lager;
/// <summary>
/// Shelf row number within the building (Werkstatt: 1-3, Lager: 1-5).
/// Corresponds to position B in the location code.
/// </summary>
[Required]
[Range(MinRow, MaxRow, ErrorMessage = "Reihe muss zwischen 1 und 5 liegen.")]
public int Row { get; set; } = 1;
/// <summary>
/// Position within the shelf slot: side (links/rechts) and level (oben/unten).
/// Corresponds to position C in the location code.
/// </summary>
[Required]
public ShelfPosition Position { get; set; } = ShelfPosition.LinksOben;
/// <summary>
/// Lengthwise slot number within the row (1-9).
/// Corresponds to position D in the location code.
/// </summary>
[Required]
[Range(MinSlot, MaxSlot, ErrorMessage = "Platz muss zwischen 1 und 9 liegen.")]
public int SlotNumber { get; set; } = 1;
/// <summary>
/// Whether this location is currently occupied by a tire stack.
/// </summary>
public bool IsOccupied { get; set; }
/// <summary>
/// The ID of the tire stack currently stored at this location (if occupied).
/// </summary>
public Guid? TireStackId { get; set; }
/// <summary>
/// Navigation property to the tire stack at this location.
/// </summary>
[ForeignKey(nameof(TireStackId))]
public TireStack? OccupiedTireStack { get; set; }
/// <summary>
/// When the current tire stack was placed at this location.
/// </summary>
public DateTime? OccupiedSince { get; set; }
/// <summary>
/// Optional notes about this location.
/// </summary>
[MaxLength(500)]
public string? Notes { get; set; }
/// <summary>
/// Whether this location is available for new storage (not blocked).
/// </summary>
public bool IsAvailable { get; set; } = true;
/// <summary>
/// Whether this location is active in the system.
/// </summary>
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// Navigation property for tire stacks stored at this location (legacy support).
/// </summary>
public ICollection<TireStack> TireStacks { get; set; } = new List<TireStack>();
/// <summary>
/// Display name showing the human-readable location.
/// </summary>
[NotMapped]
public string DisplayName => $"{Area.GetDisplayName()}, Reihe {Row}, {Position.GetDisplayName()}, Platz {SlotNumber}";
/// <summary>
/// Short display for compact views.
/// </summary>
[NotMapped]
public string ShortDisplay => $"{Area.GetDisplayName()} R{Row} {Position.GetShortName()} P{SlotNumber}";
/// <summary>
/// Full path description of the location.
/// </summary>
[NotMapped]
public string FullPath => $"{Area.GetBuildingLabel()} / Reihe {Row} / {Position.GetDisplayName()} / Platz {SlotNumber}";
/// <summary>
/// Current occupancy count (number of stacks stored).
/// </summary>
[NotMapped]
public int CurrentOccupancy => IsOccupied ? 1 : 0;
/// <summary>
/// Capacity is always 1 for the new system (one stack per location).
/// </summary>
[NotMapped]
public int Capacity => 1;
/// <summary>
/// Generates the location code from the component parts.
/// </summary>
public void GenerateCode()
{
Code = $"{(int)Area}{Row}{(int)Position}{SlotNumber}";
}
/// <summary>
/// Parses the location code and populates the Area, Row, Position, and SlotNumber properties.
/// </summary>
/// <returns>True if parsing was successful</returns>
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;
}
/// <summary>
/// Validates that the current Code matches the Area, Row, Position, and SlotNumber values.
/// </summary>
public bool ValidateCodeConsistency()
{
var expectedCode = $"{(int)Area}{Row}{(int)Position}{SlotNumber}";
return Code == expectedCode;
}
/// <summary>
/// Assigns a tire stack to this location.
/// </summary>
/// <param name="tireStackId">The ID of the tire stack to assign</param>
/// <exception cref="InvalidOperationException">If location is already occupied</exception>
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;
}
/// <summary>
/// Clears the tire stack from this location.
/// </summary>
public void ClearLocation()
{
TireStackId = null;
OccupiedTireStack = null;
IsOccupied = false;
OccupiedSince = null;
UpdatedAt = DateTime.UtcNow;
}
}