Initial commit: Lagerbestand Blazor Projekt

This commit is contained in:
spaethf committed 2026-09-21 21:28:23 +02:00
commit cba9414e92
105 files changed
+36431

No files matched your search

+252
View File
@@ -0,0 +1,252 @@
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;
}
}