diff --git a/backend/API/Controllers/FilamentsController.cs b/backend/API/Controllers/FilamentsController.cs
index 5c62fec..5bb951e 100644
--- a/backend/API/Controllers/FilamentsController.cs
+++ b/backend/API/Controllers/FilamentsController.cs
@@ -46,9 +46,11 @@ public class FilamentsController : ControllerBase
_logger.LogDebug(
"Getting filaments: pageNumber={PageNumber}, pageSize={PageSize}, " +
"materialBaseId={MaterialBaseId}, materialFinishId={MaterialFinishId}, " +
- "materialModifierId={MaterialModifierId}, brand={Brand}, isActive={IsActive}",
+ "materialModifierId={MaterialModifierId}, brand={Brand}, isActive={IsActive}, " +
+ "includeArchived={IncludeArchived}, storageLocation={StorageLocation}",
query.PageNumber, query.PageSize, query.MaterialBaseId,
- query.MaterialFinishId, query.MaterialModifierId, query.Brand, query.IsActive);
+ query.MaterialFinishId, query.MaterialModifierId, query.Brand, query.IsActive,
+ query.IncludeArchived, query.StorageLocation);
// Clamp pagination values
var pageNumber = Math.Max(1, query.PageNumber);
@@ -77,6 +79,15 @@ public class FilamentsController : ControllerBase
if (query.IsActive.HasValue)
spoolQuery = spoolQuery.Where(s => s.IsActive == query.IsActive.Value);
+ // Exclude archived spools by default; include when explicitly requested
+ if (query.IncludeArchived != true)
+ spoolQuery = spoolQuery.Where(s => !s.IsArchived);
+
+ if (!string.IsNullOrWhiteSpace(query.StorageLocation))
+ spoolQuery = spoolQuery.Where(s =>
+ s.StorageLocation != null &&
+ s.StorageLocation.ToLower().Contains(query.StorageLocation.ToLower()));
+
var totalCount = await spoolQuery.CountAsync();
var items = await spoolQuery
@@ -185,7 +196,9 @@ public class FilamentsController : ControllerBase
SpoolSerial = request.SpoolSerial,
PurchasePrice = request.PurchasePrice,
PurchaseDate = request.PurchaseDate,
- IsActive = request.IsActive
+ IsActive = request.IsActive,
+ IsArchived = request.IsArchived,
+ StorageLocation = request.StorageLocation
};
_dbContext.Spools.Add(entity);
@@ -267,6 +280,8 @@ public class FilamentsController : ControllerBase
entity.PurchasePrice = request.PurchasePrice;
entity.PurchaseDate = request.PurchaseDate;
entity.IsActive = request.IsActive;
+ entity.IsArchived = request.IsArchived;
+ entity.StorageLocation = request.StorageLocation;
await _dbContext.SaveChangesAsync();
@@ -307,6 +322,8 @@ public class FilamentsController : ControllerBase
PurchasePrice = s.PurchasePrice,
PurchaseDate = s.PurchaseDate,
IsActive = s.IsActive,
+ IsArchived = s.IsArchived,
+ StorageLocation = s.StorageLocation,
CreatedAt = s.CreatedAt,
UpdatedAt = s.UpdatedAt,
QrCodeUrl = $"/api/qr/spool/{s.Id}"
diff --git a/backend/API/DTOs/Filaments/FilamentDtos.cs b/backend/API/DTOs/Filaments/FilamentDtos.cs
index 3b1b91b..75f9247 100644
--- a/backend/API/DTOs/Filaments/FilamentDtos.cs
+++ b/backend/API/DTOs/Filaments/FilamentDtos.cs
@@ -59,6 +59,12 @@ public class FilamentResponse
/// Whether the spool is currently active and available.
public bool IsActive { get; set; }
+ /// Whether the spool has been archived (removed from active inventory).
+ public bool IsArchived { get; set; }
+
+ /// Physical storage location (e.g., "Shelf A", "Drawer 3"). Null if unset.
+ public string? StorageLocation { get; set; }
+
/// Timestamp when this record was created (UTC).
public DateTime CreatedAt { get; set; }
@@ -133,6 +139,15 @@ public class CreateFilamentRequest
/// Whether the spool is active. Defaults to true.
public bool IsActive { get; set; } = true;
+
+ /// Whether the spool is archived. Defaults to false.
+ ///
+ public bool IsArchived { get; set; } = false;
+
+ /// Physical storage location (e.g., "Shelf A", "Drawer 3"). Optional.
+ ///
+ [StringLength(200, ErrorMessage = "StorageLocation must not exceed 200 characters.")]
+ public string? StorageLocation { get; set; }
}
///
@@ -196,4 +211,11 @@ public class UpdateFilamentRequest
/// Whether the spool is active.
public bool IsActive { get; set; } = true;
+
+ /// Whether the spool is archived. Defaults to false.
+ public bool IsArchived { get; set; } = false;
+
+ /// Physical storage location (e.g., "Shelf A", "Drawer 3"). Optional.
+ [StringLength(200, ErrorMessage = "StorageLocation must not exceed 200 characters.")]
+ public string? StorageLocation { get; set; }
}
\ No newline at end of file
diff --git a/backend/API/DTOs/Filaments/FilamentQueryDtos.cs b/backend/API/DTOs/Filaments/FilamentQueryDtos.cs
index 98c9bd6..43936e7 100644
--- a/backend/API/DTOs/Filaments/FilamentQueryDtos.cs
+++ b/backend/API/DTOs/Filaments/FilamentQueryDtos.cs
@@ -30,4 +30,11 @@ public class FilamentQueryParameters
/// Optional filter by active status. True = active only, False = inactive only.
public bool? IsActive { get; set; }
+
+ /// Whether to include archived spools in results. Defaults to false (excludes archived).
+ ///
+ public bool? IncludeArchived { get; set; }
+
+ /// Optional filter by storage location (case-insensitive partial match).
+ public string? StorageLocation { get; set; }
}
\ No newline at end of file
diff --git a/backend/API/Validators/FilamentValidators.cs b/backend/API/Validators/FilamentValidators.cs
index 8fe0f18..9578b95 100644
--- a/backend/API/Validators/FilamentValidators.cs
+++ b/backend/API/Validators/FilamentValidators.cs
@@ -52,6 +52,12 @@ public class CreateFilamentRequestValidator : AbstractValidator x.PurchasePrice!.Value)
.GreaterThanOrEqualTo(0).WithMessage("Purchase price must be non-negative.");
});
+
+ When(x => x.StorageLocation != null, () =>
+ {
+ RuleFor(x => x.StorageLocation!)
+ .MaximumLength(200).WithMessage("StorageLocation must not exceed 200 characters.");
+ });
}
}
@@ -104,5 +110,11 @@ public class UpdateFilamentRequestValidator : AbstractValidator x.PurchasePrice!.Value)
.GreaterThanOrEqualTo(0).WithMessage("Purchase price must be non-negative.");
});
+
+ When(x => x.StorageLocation != null, () =>
+ {
+ RuleFor(x => x.StorageLocation!)
+ .MaximumLength(200).WithMessage("StorageLocation must not exceed 200 characters.");
+ });
}
}
\ No newline at end of file
diff --git a/backend/Domain/Entities/Spool.cs b/backend/Domain/Entities/Spool.cs
index 5084a7b..fd17d26 100644
--- a/backend/Domain/Entities/Spool.cs
+++ b/backend/Domain/Entities/Spool.cs
@@ -93,6 +93,20 @@ public class Spool : AuditableEntity
///
public bool IsActive { get; set; } = true;
+ ///
+ /// Whether the spool has been archived (removed from active inventory).
+ /// Archived spools are retained for historical records but hidden from
+ /// default inventory views. Distinguishes long-term archival from
+ /// temporary inactivity (e.g., spool swapped out of AMS).
+ ///
+ public bool IsArchived { get; set; } = false;
+
+ ///
+ /// Physical storage location of the spool (e.g., "Shelf A", "Drawer 3", "AMS Tray 2").
+ /// Optional — not every spool has a designated storage location.
+ ///
+ public string? StorageLocation { get; set; }
+
///
/// Navigation collection of AMS slots where this spool is loaded.
///
diff --git a/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs b/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs
index a426906..19eff6d 100644
--- a/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs
+++ b/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs
@@ -68,6 +68,15 @@ public class SpoolConfiguration : BaseEntityConfiguration
.HasDefaultValue(true)
.IsRequired();
+ builder.Property(e => e.IsArchived)
+ .HasColumnName("is_archived")
+ .HasDefaultValue(false)
+ .IsRequired();
+
+ builder.Property(e => e.StorageLocation)
+ .HasColumnName("storage_location")
+ .HasMaxLength(200);
+
// Unique index on spool_serial — critical for barcode/QR scanning
builder.HasIndex(e => e.SpoolSerial)
.IsUnique()
@@ -89,6 +98,14 @@ public class SpoolConfiguration : BaseEntityConfiguration
builder.HasIndex(e => e.IsActive)
.HasDatabaseName("ix_spools_is_active");
+ // Index on is_archived for inventory filtering (exclude archived from default views)
+ builder.HasIndex(e => e.IsArchived)
+ .HasDatabaseName("ix_spools_is_archived");
+
+ // Composite index on is_active + is_archived for common inventory queries
+ builder.HasIndex(e => new { e.IsActive, e.IsArchived })
+ .HasDatabaseName("ix_spools_active_archived");
+
// Relationships
builder.HasOne(e => e.MaterialBase)
.WithMany(e => e.Spools)