253 lines
7.8 KiB
C#
253 lines
7.8 KiB
C#
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;
|
||
}
|
||
}
|