diff --git a/.idea/.idea.Smart-Factory-Management-System/.idea/encodings.xml b/.idea/.idea.Smart-Factory-Management-System/.idea/encodings.xml
index 7426cb0..bbfb857 100644
--- a/.idea/.idea.Smart-Factory-Management-System/.idea/encodings.xml
+++ b/.idea/.idea.Smart-Factory-Management-System/.idea/encodings.xml
@@ -1,6 +1,13 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Core/Employee.cs b/Core/Employee.cs
index fee8063..0ff8806 100644
--- a/Core/Employee.cs
+++ b/Core/Employee.cs
@@ -21,7 +21,7 @@ protected Employee(string name, string username, string passwordHash, bool isFir
IsFirstTimeLogin = isFirstTimeLogin;
}
- public int Id { get; init; }
+ public int Id { get; set; }
public string Name { get; private set; }
public string Username { get; private set; }
public string PasswordHash { get; private set; }
@@ -109,8 +109,6 @@ public override List GetAvailableMenuOptions()
[
"Quick Actions",
"Machine Management",
- "Reports",
- "Factory Information",
"Log Out / Exit Session"
];
}
@@ -150,7 +148,7 @@ public Accountant(string name, string username, string passwordHash, bool isFirs
Role = "Accountant";
}
- public override string QuickActionName => "N/A";
+ public override string QuickActionName => "Quick Actions";
public override string ShowActivity()
{
@@ -161,6 +159,7 @@ public override List GetAvailableMenuOptions()
{
return
[
+ "Quick Actions",
"Accounting",
"Reports",
"Log Out / Exit Session"
diff --git a/Core/Factory.cs b/Core/Factory.cs
index 3a48c20..b7f2458 100644
--- a/Core/Factory.cs
+++ b/Core/Factory.cs
@@ -1,3 +1,5 @@
+using Spectre.Console;
+
namespace Smart_Factory_Management_System;
public class Factory
@@ -23,11 +25,26 @@ public class Factory
public IReadOnlyList Employees => _employees;
public IReadOnlyList Machines => _machines;
public IReadOnlyList Inventory => _inventory;
- public int InventoryCapacity => _inventory.Capacity;
+ public int MaxBatches => 20;
+ public int MaxUnitsPerBatch => 100;
+ public int MaxCapacity => MaxBatches * MaxUnitsPerBatch;
+ public int MinStockThreshold => 200;
+
+ public int GetTotalUnsoldUnits()
+ {
+ return _batches.Where(b => !b.IsSold).Sum(b => b.Quantity);
+ }
public void LoadFromRepository(IEnumerable employees, IEnumerable machines,
- IEnumerable products)
+ IEnumerable products, IEnumerable orders, IEnumerable reportRequests)
{
+ // Reset counters to 0 BEFORE materializing the lists.
+ // JsonSerializer calls the real constructor for each deserialized object, which increments
+ // the static counter. Without this reset the counter would be inflated by the load itself,
+ // causing the next genuinely new entity to receive a wrong (too-high) ID.
+ Employee.InitializeIdCounter(0);
+ Machine.InitializeIdCounter(0);
+
var employeeList = employees.ToList();
var machineList = machines.ToList();
var productList = products.ToList();
@@ -36,6 +53,38 @@ public void LoadFromRepository(IEnumerable employees, IEnumerable !string.IsNullOrEmpty(p.BatchId)).GroupBy(p => p.BatchId);
+ foreach (var group in productsByBatch)
+ {
+ var firstProduct = group.First();
+ var totalQuantity = group.Sum(p => p.Quantity);
+ var totalCost = group.Sum(p => p.ProductionCost * p.Quantity);
+ var unitCost = totalQuantity > 0 ? totalCost / totalQuantity : firstProduct.ProductionCost;
+
+ var batch = new ProductionBatch(firstProduct.Name ?? "Loaded Product", totalQuantity, unitCost, group.Key);
+
+ var maxSellingPrice = group.Max(p => p.SellingPrice);
+ if (maxSellingPrice > 0) batch.SetUnitSellPrice(maxSellingPrice);
+
+ // If all products in the batch are marked as sold, the batch is sold
+ if (group.All(p => p.IsSold)) batch.MarkAsSold();
+
+ // Link matching product indexes to the batch
+ for (var i = 0; i < productList.Count; i++)
+ if (productList[i].BatchId == group.Key)
+ batch.AddInventoryIndex(i);
+
+ _batches.Add(batch);
+ }
+
+ // Now set counters to the highest persisted ID so the next new entity continues from there.
if (employeeList.Count > 0)
{
var maxEmployeeId = employeeList.Max(e => e.Id);
@@ -98,6 +147,28 @@ public void AddBatch(ProductionBatch batch)
_batches.Add(batch);
}
+ public static void ShowInventoryAlerts(IEnumerable inventory)
+ {
+ // Use the helper method defined in Product.cs
+ var lowStockItems = inventory.Where(p => p.IsLowStock()).ToList();
+
+ if (lowStockItems.Count == 0) return;
+ var table = new Table().Border(TableBorder.Rounded);
+ table.AddColumn("[red]Status[/]");
+ table.AddColumn("Product");
+ table.AddColumn("Current Stock");
+
+ foreach (var item in lowStockItems)
+ if (item.Name != null)
+ table.AddRow("⚠️", item.Name, $"[bold red]{item.Quantity}[/]");
+
+ AnsiConsole.Write(new Panel(table)
+ {
+ Header = new PanelHeader("[bold red] INVENTORY ALERT [/]"),
+ Border = BoxBorder.Double
+ });
+ }
+
public void RemoveProduct(Product product)
{
_inventory.Remove(product);
@@ -107,4 +178,9 @@ public void RemoveBatch(ProductionBatch batch)
{
_batches.Remove(batch);
}
+
+ public void RemoveEmployee(Employee employee)
+ {
+ _employees.Remove(employee);
+ }
}
\ No newline at end of file
diff --git a/Core/Machine.cs b/Core/Machine.cs
index 9584bb7..f19bc89 100644
--- a/Core/Machine.cs
+++ b/Core/Machine.cs
@@ -6,8 +6,7 @@ namespace Smart_Factory_Management_System;
public enum MachineStatus
{
Running,
- Stopped,
- Maintenance
+ Stopped
}
public enum MachineCondition
@@ -41,11 +40,11 @@ protected Machine(string name, string manufacturer, string serialNumber, List TotalProcessedCount++;
+ public void IncrementFailure() => TotalFailuresCount++;
+
+
+ //*******
public static void InitializeIdCounter(int maxId)
{
_idCounter = maxId;
@@ -136,52 +146,6 @@ public bool StartMachine()
return true;
}
- public void StopMachine()
- {
- AnsiConsole.Clear();
- AnsiConsole.Write(Align.Left(new Rule(string.Format($"[red]{Machines.ShutdownSequence}[/]", Name))));
- AnsiConsole.WriteLine();
-
- if (Status == MachineStatus.Stopped)
- {
- var alreadyStoppedPanel = new Panel(
- new Markup(
- string.Format(Machines.AlreadyStopped, Name))
- )
- {
- Border = BoxBorder.Rounded,
- Padding = new Padding(1, 1, 1, 1)
- };
- AnsiConsole.Write(alreadyStoppedPanel);
- return;
- }
-
- AnsiConsole.Status()
- .Spinner(Spinner.Known.Dots)
- .SpinnerStyle(Style.Parse("red bold"))
- .Start(Machines.SafeDecelerationSpinner, ctx =>
- {
- Thread.Sleep(500);
- ctx.Status(Machines.SpoolDownSpinner);
- Thread.Sleep(600);
- ctx.Status(Machines.IsolateRelaysSpinner);
- Thread.Sleep(500);
- });
-
- Status = MachineStatus.Stopped;
- //******
- var stopPanel = new Panel(
- new Markup(string.Format(Machines.ShutdownComplete, Name))
- )
- {
- Border = BoxBorder.Rounded,
- Padding = new Padding(1, 1, 1, 1),
- Header = new PanelHeader($"[red bold]{Machines.SystemOfflineHeader}[/]")
- };
-
- AnsiConsole.Write(stopPanel);
- }
-
protected void ApplyProductionWearAndTear()
{
if (Parts == null || Parts.Count == 0) return;
@@ -189,31 +153,104 @@ protected void ApplyProductionWearAndTear()
var randomIndex = Random.Next(0, Parts.Count);
var selectedPart = Parts[randomIndex];
+ // 20% chance overall for wear-and-tear:
+ // - If part is Excellent, it may degrade to Good (10%) or break directly to Critical (10%).
+ // - If part is Good, it degrades to Critical (20%).
+ // - Critical parts stay Critical.
if (Random.Next(0, 100) < 20)
{
var oldCondition = selectedPart.Condition;
- selectedPart.DegradeStep();
+ if (selectedPart.Condition == PartCondition.Excellent)
+ {
+ // Split between Good and Critical
+ if (Random.Next(0, 100) < 10) // 10% chance to go to Good
+ selectedPart.DegradeStep(); // Excellent -> Good
+ else
+ // Directly jump to Critical
+ selectedPart.BreakDown();
+ }
+ else if (selectedPart.Condition == PartCondition.Good)
+ {
+ // Good degrades to Critical
+ selectedPart.DegradeStep(); // Good -> Critical
+ }
+ // If already Critical, nothing changes
if (selectedPart.Condition != oldCondition)
{
- AnsiConsole.WriteLine();
if (selectedPart.Condition == PartCondition.Critical)
{
- AnsiConsole.Write(new Markup(
- string.Format(Machines.AlertCriticalPart, selectedPart.Name)));
+ if (!SilentMode)
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.Write(new Markup(
+ string.Format(Machines.AlertCriticalPart, selectedPart.Name)));
+ }
+
Status = MachineStatus.Stopped;
Condition = MachineCondition.Critical;
}
else
{
- AnsiConsole.Write(new Markup(
- string.Format(Machines.WarningDegradation, selectedPart.Name, selectedPart.Condition)));
+ if (!SilentMode)
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.Write(new Markup(
+ string.Format(Machines.WarningDegradation, selectedPart.Name, selectedPart.Condition)));
+ }
+
Condition = MachineCondition.Good;
}
}
}
}
+ public double CalculateEfficiency()
+ {
+ if (Parts == null || Parts.Count == 0) return 0;
+
+ // Mathematical formula: the average of part condition scores multiplied by the product of those scores.
+ // Excellent = 1.0, Good = 0.85 (drops by 15%), Critical = 0.0.
+ // If any part reaches Critical, the product terms will drive the entire efficiency score to 0.0 naturally.
+ var partFactors = Parts.Select(p => p.Condition switch
+ {
+ PartCondition.Excellent => 1.0,
+ PartCondition.Good => 0.85,
+ _ => 0.0
+ }).ToList();
+
+ var averageFactor = partFactors.Average();
+ var productFactor = partFactors.Aggregate(1.0, (acc, val) => acc * val);
+
+ return averageFactor * productFactor * 100.0;
+ }
+
+ public int GetEstimatedDaysUntilMaintenance()
+ {
+ if (Parts == null || Parts.Count == 0) return 30;
+
+ // Formula derived from ApplyProductionWearAndTear randomness:
+ // - Random selection of 1 of N parts (1/N chance).
+ // - 20% degradation probability (0.2).
+ // - Expected production cycles for a part to degrade by 1 step is (N / 0.2).
+ // - Excellent parts have 2 steps to critical, Good parts have 1 step, Critical parts have 0 steps.
+ // - Machine fails (and requires repair) as soon as the first part goes Critical.
+ // - Remaining Cycles Estimate = min(StepsRemaining) * (N / 0.2).
+ var n = Parts.Count;
+ var remainingSteps = Parts.Select(p => p.Condition switch
+ {
+ PartCondition.Excellent => 2,
+ PartCondition.Good => 1,
+ _ => 0
+ });
+
+ var minSteps = remainingSteps.Min();
+ if (minSteps == 0) return 0;
+
+ var expectedCycles = minSteps * n / 0.2;
+ return (int)Math.Round(expectedCycles);
+ }
+
public void InspectMachine()
{
AnsiConsole.Clear();
@@ -271,6 +308,30 @@ public void InspectMachine()
AnsiConsole.Write(componentTable);
AnsiConsole.WriteLine();
+
+ var daysLeft = GetEstimatedDaysUntilMaintenance();
+ var alertColor = daysLeft <= 5 ? "red" : daysLeft <= 10 ? "yellow" : "green";
+
+ var maintenancePanel = new Panel(new Markup(
+ $"[bold]Predictive Maintenance Estimate:[/] Machine [bold]{Name}[/] " +
+ $"requires maintenance in approximately [{alertColor}]{daysLeft} active production cycles (days)[/]."))
+ {
+ Border = BoxBorder.Rounded,
+ Header = new PanelHeader("[bold blue] Info: Predictive Maintenance [/]")
+ };
+
+ AnsiConsole.Write(maintenancePanel);
+ AnsiConsole.WriteLine();
+
+ double efficiency = CalculateEfficiency();
+ var effColor = efficiency > 80 ? "green" : (efficiency > 50 ? "yellow" : "red");
+
+ AnsiConsole.Write(new Panel(new Markup(
+ $"Operational Efficiency: [{effColor} bold]{efficiency:F1}%[/]"))
+ {
+ Header = new PanelHeader("[bold]Production Efficiency Dashboard[/]")
+ });
+ AnsiConsole.WriteLine();
}
public bool NeedsRepair()
@@ -284,12 +345,12 @@ public bool NeedsRepair()
return false;
}
- public bool RepairMachine()
+ public void RepairMachine()
{
if (!NeedsRepair())
{
AnsiConsole.MarkupLine(string.Format(Machines.DoesNotNeedRepairs, Name));
- return false;
+ return;
}
AnsiConsole.Clear();
@@ -297,12 +358,25 @@ public bool RepairMachine()
AnsiConsole.WriteLine();
var repairedParts = 0;
- foreach (var part in Parts ?? new List())
- if (part.Condition != PartCondition.Excellent)
+
+ AnsiConsole.Status()
+ .Spinner(Spinner.Known.Dots)
+ .SpinnerStyle(Style.Parse("yellow bold"))
+ .Start(string.Format(Machines.RepairSpinnerOpeningPanels, Name), ctx =>
{
- part.Repair(PartCondition.Excellent);
- repairedParts++;
- }
+ Thread.Sleep(800);
+ foreach (var part in Parts ?? new List())
+ if (part.Condition != PartCondition.Excellent)
+ {
+ ctx.Status(string.Format(Machines.RepairSpinnerServicingPart, part.Name));
+ Thread.Sleep(1000);
+ part.Repair(PartCondition.Excellent);
+ repairedParts++;
+ }
+
+ ctx.Status(Machines.RepairSpinnerDiagnostics);
+ Thread.Sleep(800);
+ });
Condition = GetMachineCondition();
Status = MachineStatus.Stopped;
@@ -315,7 +389,6 @@ public bool RepairMachine()
};
AnsiConsole.Write(panel);
- return true;
}
private MachineCondition GetMachineCondition()
@@ -344,23 +417,29 @@ public LitographyMachine(string name, string manufacturer, string serialNumber,
public override bool Produce(Product blueprint)
{
+ //Factory.ShowInventoryAlerts();
+
if (ActiveOrder == null || ActiveOrder.IsComplete)
{
Status = MachineStatus.Stopped;
- AnsiConsole.Write(new Markup(string.Format(Machines.LitographySuccess, blueprint.Name)));
+ if (!SilentMode) AnsiConsole.Write(new Markup(string.Format(Machines.LitographySuccess, blueprint.Name)));
return true;
}
if (blueprint.GetType() != SupportedProductType)
throw new InvalidOperationException($"This machine only produces {SupportedProductType.Name}!");
- AnsiConsole.Write(
- new Markup(string.Format(Machines.LitographyStart, blueprint.Name)));
+ if (!SilentMode)
+ {
+ AnsiConsole.Write(
+ new Markup(string.Format(Machines.LitographyStart, blueprint.Name)));
+
+ AnsiConsole.Status()
+ .Spinner(Spinner.Known.BouncingBar)
+ .SpinnerStyle(Style.Parse("cyan bold"))
+ .Start(Machines.LitographySpinner, _ => { Thread.Sleep(800); });
+ }
- AnsiConsole.Status()
- .Spinner(Spinner.Known.BouncingBar)
- .SpinnerStyle(Style.Parse("cyan bold"))
- .Start(Machines.LitographySpinner, _ => { Thread.Sleep(800); });
ApplyProductionWearAndTear();
return true;
}
@@ -379,21 +458,25 @@ public override bool Produce(Product product)
{
if (product is not Motherboard board)
{
- AnsiConsole.MarkupLine(string.Format(Machines.ProcessErrorUnsupported, product.GetType().Name));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(string.Format(Machines.ProcessErrorUnsupported, product.GetType().Name));
return false;
}
if (board.CurrentState != BoardState.BlankBoard)
{
- AnsiConsole.MarkupLine(
- string.Format(Machines.ProcessWarningUnexpectedState, board.Name, BoardState.BlankBoard,
- "Solder Paste Printing"));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(
+ string.Format(Machines.ProcessWarningUnexpectedState, board.Name, BoardState.BlankBoard,
+ "Solder Paste Printing"));
return false;
}
board.TransitionTo(BoardState.SolderPrinted);
- AnsiConsole.MarkupLine(
- string.Format(Machines.ProcessSuccessState, board.Name, BoardState.SolderPrinted, "Solder Paste Printing"));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(
+ string.Format(Machines.ProcessSuccessState, board.Name, BoardState.SolderPrinted,
+ "Solder Paste Printing"));
ApplyProductionWearAndTear();
return true;
}
@@ -411,22 +494,25 @@ public override bool Produce(Product product)
{
if (product is not Motherboard board)
{
- AnsiConsole.MarkupLine(string.Format(Machines.ProcessErrorUnsupported, product.GetType().Name));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(string.Format(Machines.ProcessErrorUnsupported, product.GetType().Name));
return false;
}
if (board.CurrentState != BoardState.SolderPrinted)
{
- AnsiConsole.MarkupLine(
- string.Format(Machines.ProcessWarningUnexpectedState, board.Name, BoardState.SolderPrinted,
- "Pick and Place Assembly"));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(
+ string.Format(Machines.ProcessWarningUnexpectedState, board.Name, BoardState.SolderPrinted,
+ "Pick and Place Assembly"));
return false;
}
board.TransitionTo(BoardState.ComponentsPlaced);
- AnsiConsole.MarkupLine(
- string.Format(Machines.ProcessSuccessState, board.Name, BoardState.ComponentsPlaced,
- "Pick and Place Assembly"));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(
+ string.Format(Machines.ProcessSuccessState, board.Name, BoardState.ComponentsPlaced,
+ "Pick and Place Assembly"));
ApplyProductionWearAndTear();
return true;
}
@@ -444,22 +530,28 @@ public override bool Produce(Product product)
{
if (product is not Motherboard board)
{
- AnsiConsole.MarkupLine(string.Format(Machines.ProcessErrorUnsupported, product.GetType().Name));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(string.Format(Machines.ProcessErrorUnsupported, product.GetType().Name));
return false;
}
if (board.CurrentState != BoardState.ComponentsPlaced)
{
- AnsiConsole.MarkupLine(
- string.Format(Machines.ProcessWarningUnexpectedState, board.Name, BoardState.ComponentsPlaced,
- "Reflow Baking"));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(
+ string.Format(Machines.ProcessWarningUnexpectedState, board.Name, BoardState.ComponentsPlaced,
+ "Reflow Baking"));
return false;
}
board.TransitionTo(BoardState.BakedAndSoldered);
- AnsiConsole.MarkupLine(
- string.Format(Machines.ProcessSuccessState, board.Name, BoardState.BakedAndSoldered, "Reflow Baking"));
+ if (!SilentMode)
+ AnsiConsole.MarkupLine(
+ string.Format(Machines.ProcessSuccessState, board.Name, BoardState.BakedAndSoldered, "Reflow Baking"));
ApplyProductionWearAndTear();
return true;
}
+
+
+ //***
}
\ No newline at end of file
diff --git a/Core/MachinePart.cs b/Core/MachinePart.cs
index 27817d4..f2a1e70 100644
--- a/Core/MachinePart.cs
+++ b/Core/MachinePart.cs
@@ -15,7 +15,7 @@ public enum PartCondition
[JsonDerivedType(typeof(AoiSystem), "aoiSystem")]
public abstract class MachinePart(string name, PartCondition? condition)
{
- public string? Name { get; private protected set; } = name;
+ public string? Name { get; } = name;
public PartCondition? Condition { get; private set; } = condition;
public void BreakDown()
diff --git a/Core/Product.cs b/Core/Product.cs
index e98bf16..1987072 100644
--- a/Core/Product.cs
+++ b/Core/Product.cs
@@ -6,9 +6,7 @@ namespace Smart_Factory_Management_System;
[JsonDerivedType(typeof(Motherboard), "motherboard")]
public abstract class Product
{
- private readonly double _productionCost;
- private int _quantity;
- private double _sellingPrice;
+ private double _productionCost;
protected Product(string name, double productionCost, double sellingPrice, int quantity)
{
@@ -18,7 +16,7 @@ protected Product(string name, double productionCost, double sellingPrice, int q
Quantity = quantity;
}
- public string? Name { get; init; }
+ public string? Name { get; }
public string? BatchId { get; set; }
public bool IsSold { get; set; }
@@ -26,18 +24,18 @@ protected Product(string name, double productionCost, double sellingPrice, int q
public double ProductionCost
{
get => _productionCost;
- private init => _productionCost = value >= 0 ? value : 0;
+ set => _productionCost = value >= 0 ? value : 0;
}
public double SellingPrice
{
- get => _sellingPrice;
- private set => _sellingPrice = value >= 0 ? value : 0;
+ get;
+ private set => field = value >= 0 ? value : 0;
}
public int Quantity
{
- get => _quantity;
+ get;
private set
{
if (value < 0)
@@ -45,10 +43,20 @@ private set
throw new ArgumentOutOfRangeException(nameof(value), "Quantity cannot be negative.");
}
- _quantity = value;
+ field = value;
}
}
+ //**
+ public int MinStockThreshold { get; set; } = 5; // Prag implicit
+
+
+ public bool IsLowStock()
+ {
+ return Quantity <= MinStockThreshold;
+ }
+
+ //***
public void MarkAsSold()
{
IsSold = true;
@@ -87,9 +95,8 @@ public class Microprocessor(
double clockSpeed)
: Product(name, productionCost, sellingPrice, quantity)
{
- public string? Architecture { get; init; }
- public int? Cores { get; init; } = cores;
- public double ClockSpeed { get; init; } = clockSpeed;
+ public int? Cores { get; } = cores;
+ public double ClockSpeed { get; } = clockSpeed;
public override string GetTechnicalSpecifications()
{
@@ -120,8 +127,8 @@ public class Motherboard(
{
public BoardState CurrentState { get; private set; } = BoardState.BlankBoard;
- public string? SocketStandard { get; init; } = socketStandard;
- public string? PhysicalForm { get; init; } = physicalForm;
+ public string? SocketStandard { get; } = socketStandard;
+ public string? PhysicalForm { get; } = physicalForm;
// Only the machines will call this
public void TransitionTo(BoardState nextState)
diff --git a/Core/ProductionBatch.cs b/Core/ProductionBatch.cs
index b824a6b..4a085bb 100644
--- a/Core/ProductionBatch.cs
+++ b/Core/ProductionBatch.cs
@@ -1,12 +1,12 @@
namespace Smart_Factory_Management_System;
-public class ProductionBatch(string productName, int quantity, double unitCost)
+public class ProductionBatch(string productName, int quantity, double unitCost, string? batchId = null)
{
private readonly List _inventoryIndexes = new();
- public string BatchId { get; } = Guid.NewGuid().ToString().Substring(0, 8);
- public string ProductName { get; init; } = productName;
- public int Quantity { get; init; } = quantity;
- public double UnitProductionCost { get; init; } = unitCost;
+ public string BatchId { get; } = batchId ?? Guid.NewGuid().ToString().Substring(0, 8);
+ public string ProductName { get; } = productName;
+ public int Quantity { get; set; } = quantity;
+ public double UnitProductionCost { get; set; } = unitCost;
public double? UnitSellPrice { get; private set; }
public bool IsPriced => UnitSellPrice is > 0;
public bool IsSold { get; private set; }
diff --git a/Core/ProductionOrder.cs b/Core/ProductionOrder.cs
index 8f316b1..c2a7298 100644
--- a/Core/ProductionOrder.cs
+++ b/Core/ProductionOrder.cs
@@ -1,12 +1,23 @@
namespace Smart_Factory_Management_System;
-public class ProductionOrder(string productName, int quantity, int technicianId)
+public class ProductionOrder
{
- public string OrderId { get; } = Guid.NewGuid().ToString().Substring(0, 8);
- public string ProductName { get; init; } = productName;
- public int Quantity { get; init; } = quantity;
- public int CompletedCount { get; private set; }
- public int AssignedTechnicianId { get; set; } = technicianId;
+ public ProductionOrder()
+ {
+ }
+
+ public ProductionOrder(string productName, int quantity, int technicianId)
+ {
+ ProductName = productName;
+ Quantity = quantity;
+ AssignedTechnicianId = technicianId;
+ }
+
+ public string OrderId { get; set; } = Guid.NewGuid().ToString().Substring(0, 8);
+ public string ProductName { get; set; } = string.Empty;
+ public int Quantity { get; set; }
+ public int CompletedCount { get; set; }
+ public int AssignedTechnicianId { get; set; }
public bool IsComplete => CompletedCount >= Quantity;
public string CustomProductName { get; set; } = string.Empty;
@@ -16,9 +27,10 @@ public class ProductionOrder(string productName, int quantity, int technicianId)
public string? PhysicalForm { get; set; }
public string PlacedBy { get; set; } = string.Empty;
- public bool IsNotifiedComplete { get; set; } = false;
+ public bool IsNotifiedComplete { get; set; }
+ public string? BatchId { get; set; }
- public DateTime CreatedAt { get; } = DateTime.UtcNow;
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public double GetPriorityScore()
{
diff --git a/Core/ReportRequest.cs b/Core/ReportRequest.cs
index c0bbd76..de8e959 100644
--- a/Core/ReportRequest.cs
+++ b/Core/ReportRequest.cs
@@ -6,13 +6,24 @@ public enum ReportStatus
Fulfilled
}
-public class ReportRequest(string reportType, string requestedByDirectorName)
+public class ReportRequest
{
- public string RequestId { get; } = Guid.NewGuid().ToString().Substring(0, 8);
- public string ReportType { get; init; } = reportType;
- public string RequestedByDirectorName { get; } = requestedByDirectorName;
- public bool IsNotifiedComplete { get; set; } = false;
- public ReportStatus Status { get; private set; } = ReportStatus.Pending;
+ public ReportRequest()
+ {
+ }
+
+ public ReportRequest(string reportType, string requestedByDirectorName)
+ {
+ ReportType = reportType;
+ RequestedByDirectorName = requestedByDirectorName;
+ }
+
+ public string RequestId { get; set; } = Guid.NewGuid().ToString().Substring(0, 8);
+ public string ReportType { get; set; } = string.Empty;
+ public string RequestedByDirectorName { get; set; } = string.Empty;
+ public bool IsNotifiedComplete { get; set; }
+ public string ExportDestination { get; set; } = "ReportsFolder";
+ public ReportStatus Status { get; set; } = ReportStatus.Pending;
public void Fulfill()
{
diff --git a/Services/DataSeeder.cs b/Services/DataSeeder.cs
index 8fe1134..abc188e 100644
--- a/Services/DataSeeder.cs
+++ b/Services/DataSeeder.cs
@@ -16,14 +16,10 @@ public void Seed()
{
var initialEmployees = new List
{
- new Director("Andrei Popescu", "andrei", SecurityHelper.HashPassword("password"),
- false),
- new Technician("Maria Ionescu", "maria", SecurityHelper.HashPassword("password"),
- false),
- new SalesAgent("Alexandru Dumitru", "alex", SecurityHelper.HashPassword("password"),
- false),
- new Accountant("Elena Vasilescu", "elena", SecurityHelper.HashPassword("password"),
- false)
+ new Director("Andrei Popescu", "andrei", SecurityHelper.HashPassword("password")),
+ new Technician("Maria Ionescu", "maria", SecurityHelper.HashPassword("password")),
+ new SalesAgent("Alexandru Dumitru", "alex", SecurityHelper.HashPassword("password")),
+ new Accountant("Elena Vasilescu", "elena", SecurityHelper.HashPassword("password"))
};
employeeRepo.Save(initialEmployees);
}
@@ -87,8 +83,20 @@ public void Seed()
{
var initialProducts = new List
{
- new Microprocessor("ARM Cortex-M4", 50, 74.99, 10, 4, 2.5),
- new Motherboard("Motherboard ATX", 50.00, 99.99, 5, "AM4", "ATX")
+ // Unsold stock (Total: 70 units - triggers the < 200 unit alert banner demo)
+ new Microprocessor("ARM Cortex-M4", 50.00, 0, 10, 4, 2.5) { BatchId = "BATCH-01", IsSold = false },
+ new Microprocessor("Intel Core i5", 120.00, 0, 15, 6, 3.2) { BatchId = "BATCH-02", IsSold = false },
+ new Microprocessor("AMD Ryzen 5", 110.00, 0, 20, 6, 3.6) { BatchId = "BATCH-03", IsSold = false },
+ new Motherboard("Motherboard ATX", 50.00, 0, 5, "AM4", "ATX") { BatchId = "BATCH-04", IsSold = false },
+ new Motherboard("Asus ROG Strix", 150.00, 0, 8, "LGA1700", "ATX")
+ { BatchId = "BATCH-05", IsSold = false },
+ new Motherboard("MSI Tomahawk", 90.00, 0, 12, "AM5", "ATX") { BatchId = "BATCH-06", IsSold = false },
+
+ // Pre-sold history (for revenue/financial dashboard demonstration)
+ new Microprocessor("ARM Cortex-M4", 50.00, 74.99, 50, 4, 2.5) { BatchId = "BATCH-S1", IsSold = true },
+ new Microprocessor("Intel Core i5", 120.00, 199.99, 30, 6, 3.2) { BatchId = "BATCH-S2", IsSold = true },
+ new Motherboard("Motherboard ATX", 50.00, 99.99, 25, "AM4", "ATX")
+ { BatchId = "BATCH-S3", IsSold = true }
};
productRepo.Save(initialProducts);
}
diff --git a/Services/LoggerService.cs b/Services/LoggerService.cs
index 2710cee..73dd19a 100644
--- a/Services/LoggerService.cs
+++ b/Services/LoggerService.cs
@@ -4,9 +4,9 @@ namespace Smart_Factory_Management_System;
public enum LogOrigin
{
- SYSTEM,
- USER,
- UNDO
+ System,
+ User,
+ Undo
}
public enum LogEvent
@@ -43,7 +43,8 @@ public enum LogEvent
UsernameUpdated,
PasswordUpdated,
PasswordChangedFirstLogin,
- OperationUndone
+ OperationUndone,
+ EmployeeRemoved
}
public class LoggerService(IFileSystemService fileService) : ILoggerService
@@ -89,7 +90,7 @@ public void LogInfo(LogOrigin origin, LogEvent eventType, string? context = "")
_ => "Unknown event occurred"
};
- WriteToFile("INFO", origin, message);
+ WriteToFile("INFO", nameof(origin).ToUpper(), message);
}
public void LogWarning(LogOrigin origin, LogEvent eventType, string context = "")
@@ -100,12 +101,12 @@ public void LogWarning(LogOrigin origin, LogEvent eventType, string context = ""
_ => "Unknown event occurred"
};
- WriteToFile("WARN", origin, message);
+ WriteToFile("WARN", nameof(origin).ToUpper(), message);
}
public void LogError(string message)
{
- WriteToFile("ERROR", LogOrigin.SYSTEM, message);
+ WriteToFile("ERROR", nameof(LogOrigin.System).ToUpper(), message);
}
public void ShowOperationHistory()
@@ -173,7 +174,7 @@ public void ShowOperationHistory()
AnsiConsole.Write(table);
}
- private void WriteToFile(string level, LogOrigin origin, string message)
+ private void WriteToFile(string level, string origin, string message)
{
var entry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] [{origin}] {message}";
fileService.AppendToFile(LogFileName, entry);
diff --git a/Services/UndoService.cs b/Services/UndoService.cs
index d498ad1..d2f2d06 100644
--- a/Services/UndoService.cs
+++ b/Services/UndoService.cs
@@ -17,7 +17,7 @@ public interface IUndoService
public class UndoService : IUndoService
{
- private static readonly List _commands = new();
+ private static readonly List Commands = new();
private UndoService()
{
@@ -27,21 +27,21 @@ private UndoService()
public void RegisterCommand(ICommand command)
{
- _commands.Add(command);
- while (_commands.Count > 10) _commands.RemoveAt(0);
+ Commands.Add(command);
+ while (Commands.Count > 10) Commands.RemoveAt(0);
}
public IReadOnlyList GetUndoableCommands(Employee user)
{
- if (user is Director) return _commands;
+ if (user is Director) return Commands;
- return _commands.FindAll(c => c.ExecutedBy.Equals(user.Username, StringComparison.OrdinalIgnoreCase));
+ return Commands.FindAll(c => c.ExecutedBy.Equals(user.Username, StringComparison.OrdinalIgnoreCase));
}
public void UndoCommand(ICommand command)
{
command.Undo();
- _commands.Remove(command);
+ Commands.Remove(command);
}
}
@@ -125,6 +125,37 @@ public void Undo()
chosen.AddQuantity(qty);
factory.RemoveProduct(soldProduct);
factory.RemoveBatch(batch);
+
+ var originalBatch = factory.Batches.FirstOrDefault(b => b.BatchId == chosen.BatchId);
+ if (originalBatch != null)
+ {
+ originalBatch.Quantity += qty;
+ originalBatch.MarkAsUnsold();
+ }
+
productRepo.Save(factory.Inventory);
}
+}
+
+public class RemoveEmployeeCommand(
+ Employee employee,
+ Factory factory,
+ IJsonRepository employeeRepo,
+ string executedBy) : ICommand
+{
+ public string Description => $"Removed employee '{employee.Name}' ({employee.Role})";
+ public string ExecutedBy => executedBy;
+ public DateTime ExecutedAt { get; } = DateTime.Now;
+
+ public void Undo()
+ {
+ factory.AddEmployee(employee);
+
+ var users = employeeRepo.Load();
+ if (!users.Any(u => u.Id == employee.Id))
+ {
+ users.Add(employee);
+ employeeRepo.Save(users);
+ }
+ }
}
\ No newline at end of file
diff --git a/Text/Accounting.cs b/Text/Accounting.cs
index 4469dd8..e89802a 100644
--- a/Text/Accounting.cs
+++ b/Text/Accounting.cs
@@ -16,4 +16,10 @@ public static class Accounting
public const string PendingReportHeader = "Pending Report Requests";
public const string PendingReportNotification = "Report Request [cyan]#{0}[/] ({1}) is pending your fulfillment!";
public const string PendingReportAcknowledge = "[grey]Press any key to acknowledge...[/]";
+
+ public const string QuickActionsTitle = "Accountant Quick Actions";
+ public const string QuickActionViewBatches = "1. View Batches";
+ public const string QuickActionViewProductionSummary = "2. View Production Summary";
+ public const string QuickActionProcessReportRequests = "3. Process Report Requests";
+ public const string QuickActionReturnToMainMenu = "4. Return to Main Menu";
}
\ No newline at end of file
diff --git a/Text/Employees.cs b/Text/Employees.cs
index c9b1821..399086c 100644
--- a/Text/Employees.cs
+++ b/Text/Employees.cs
@@ -14,4 +14,11 @@ public static class Employees
public const string SelectJobTitle = "Select Job Title:";
public const string InvalidRole = "[red]❌ Invalid role selection. Operation aborted.[/]";
public const string RegisteredSuccessfully = "[green]✔ Employee '{0}' registered successfully![/]";
+
+ public const string RemoveTitle = "Remove Employee";
+ public const string NoEmployeesToRemove = "[yellow]No other employees registered in the system to remove.[/]";
+ public const string SelectEmployeeToRemove = "Select employee to remove:";
+ public const string RemoveConfirmPrompt = "Are you sure you want to permanently remove employee [red]{0}[/] ({1})?";
+ public const string RemoveSuccess = "[green]✔ Employee {0} has been successfully removed from the system.[/]";
+ public const string RemoveCancelled = "[yellow]Removal cancelled.[/]";
}
\ No newline at end of file
diff --git a/Text/Machines.cs b/Text/Machines.cs
index 0e3f870..0ba1216 100644
--- a/Text/Machines.cs
+++ b/Text/Machines.cs
@@ -78,4 +78,7 @@ public static class Machines
public const string LitographyStart = "[cyan]🏭 Starting processing sequence for: [underline]{0}[/][/]\n";
public const string LitographySpinner = "Exposing wafer structure using optical masks...";
+ public const string RepairSpinnerOpeningPanels = "Opening maintenance access panels for {0}...";
+ public const string RepairSpinnerServicingPart = "Repairing/calibrating component: {0}...";
+ public const string RepairSpinnerDiagnostics = "Running diagnostic calibration cycles...";
}
\ No newline at end of file
diff --git a/UI/MenuOptions.cs b/Text/MenuOptions.cs
similarity index 93%
rename from UI/MenuOptions.cs
rename to Text/MenuOptions.cs
index 7a1d2ff..103a970 100644
--- a/UI/MenuOptions.cs
+++ b/Text/MenuOptions.cs
@@ -1,6 +1,6 @@
namespace Smart_Factory_Management_System;
-internal static class MenuOptions
+public static class MenuOptions
{
public static readonly string[] ProductTypes = ["Microprocessor", "Motherboard"];
@@ -16,7 +16,6 @@ internal static class MenuOptions
"Product Management",
"Accounting",
"Reports",
- "Factory Information",
"Log Out / Exit Session"
];
@@ -49,7 +48,6 @@ internal static class MenuOptions
[
"Production Summary",
"Employee Report",
- "Batch Revenue Summary",
"Order Backlog Summary",
"Return to Main Menu"
];
@@ -65,7 +63,6 @@ internal static class MenuOptions
public static readonly string[] AccountingMenu =
[
"View Batches",
- "Set Unit Sell Price for Batch",
"Process Report Requests",
"Return to Main Menu"
];
@@ -76,6 +73,7 @@ internal static class MenuOptions
[
"View All Registered Staff",
"Add New Employee",
+ "Remove Employee",
"Return to Main Menu"
];
}
\ No newline at end of file
diff --git a/Text/Production.cs b/Text/Production.cs
index 1ed8f55..b32f591 100644
--- a/Text/Production.cs
+++ b/Text/Production.cs
@@ -40,4 +40,26 @@ public static class Production
"Order [cyan]#{0}[/] ({1} x{2}) has been issued and auto-assigned to you!";
public const string AutoAssignAcknowledge = "[grey]Press any key to acknowledge...[/]";
+
+ public const string CostAdvice =
+ "[yellow]ℹ️ ADVICE: When determining unit production cost, please account for any maintenance, repairs, or machine break-downs that occurred during this cycle.[/]";
+
+ public const string CostPrompt = "Enter the final unit production cost ($):";
+ public const string CostValidationInvalidNumber = "[red]Please enter a valid positive number.[/]";
+ public const string CostValidationNegative = "[red]Cost cannot be negative.[/]";
+
+ public const string ProductionLineTitle = "[yellow]Production Line: {0}[/]";
+
+ public const string MachineTrippedAlert =
+ "[bold red]⚠️ MACHINE TRIPPED: {0} has stopped due to a critical part breakdown! Please perform repairs. ⚠️[/]";
+
+ public const string CompletedUnitsStatus = "[bold green]Completed: {0} / {1} units[/]";
+ public const string MotherboardLineTitle = "[yellow]Motherboard Production Line[/]";
+
+ public const string WorkflowTrippedAlert =
+ "[bold red]⚠️ WORKFLOW TRIPPED: One or more machines have broken down! Please perform repairs. ⚠️[/]";
+
+ public const string StepSmt = "1. Solder Paste Printing (SMT)";
+ public const string StepPap = "2. Pick & Place Assembly (PaP)";
+ public const string StepOven = "3. Reflow Baking (Oven)";
}
\ No newline at end of file
diff --git a/Text/Products.cs b/Text/Products.cs
index c7cb22f..1d6d49d 100644
--- a/Text/Products.cs
+++ b/Text/Products.cs
@@ -18,7 +18,7 @@ public static class Products
public const string ProductionCostColumn = "[bold green]Production Cost ($)[/]";
public const string SpecsColumn = "[bold yellow]Technical Specifications[/]";
public const string ValueColumn = "[bold magenta]Value ($)[/]";
- public const string PortfolioTitle = "📈 Inventory Portfolio & Capacity Metrics[/]";
+ public const string PortfolioTitle = "📈 Inventory Portfolio & Capacity Metrics";
public const string StatsGridTotalVolume = "[bold white]Total Volume Level:[/]";
public const string StatsGridTotalVolumeValue = "[green]{0} items[/] (📊 CPUs: {1} | ⚙️ PCBs: {2} )";
public const string StatsGridValuation = "[bold white]Asset Portfolio Valuation:[/]";
diff --git a/Text/Reports.cs b/Text/Reports.cs
index 293917a..41f21ef 100644
--- a/Text/Reports.cs
+++ b/Text/Reports.cs
@@ -44,4 +44,33 @@ public static class Reports
public const string ReportFulfilledHeader = "Printable Report Fulfilled";
public const string ReportFulfilledNotification = "Your requested report '{0}' (Req ID: [cyan]#{1}[/]) is ready!";
public const string ReportFulfilledAcknowledge = "[grey]Press any key to acknowledge...[/]";
+
+ public const string ChooseExportFolderPrompt = "Choose target export folder for the report:";
+ public const string ExportDestDesktop = "Desktop";
+ public const string ExportDestReportsFolder = "Program Reports Folder";
+ public const string ExportDestSelected = "Export destination selected: [cyan]{0}[/]";
+
+ public const string ProductionSummaryHeader = "Production Summary & Financials";
+ public const string BatchesDetailsHeader = "[bold cyan] Factory Production Batches Details [/]";
+ public const string ColumnBatchId = "BatchId";
+ public const string ColumnProduct = "Product";
+ public const string ColumnQty = "Qty";
+ public const string ColumnProductionCostUnit = "Production Cost/Unit";
+ public const string ColumnSellPriceUnit = "Sell Price/Unit";
+ public const string ColumnStatus = "Status";
+ public const string StatusSold = "[green]Sold[/]";
+ public const string StatusProduced = "[grey]Produced[/]";
+ public const string NoBatchesProduced = "[grey]No batches produced yet.[/]";
+
+ public const string FinanceExpensesLabel = "[bold white]Total Expenses (Production Cost):[/]";
+ public const string FinanceIncomeLabel = "[bold white]Total Income (Sales Revenue):[/]";
+ public const string FinanceProfitLabel = "[bold white]Total Net Profit/Loss:[/]";
+ public const string FinanceHeader = " Factory Financial Summary ";
+
+ public const string OrderBacklogSummaryHeader = "Order Backlog Summary";
+ public const string ColumnOrderId = "OrderId";
+ public const string ColumnCompleted = "Completed";
+ public const string ColumnAssignedTech = "Assigned Tech";
+ public const string StatusComplete = "Complete";
+ public const string StatusInProgress = "In Progress";
}
\ No newline at end of file
diff --git a/Text/Sales.cs b/Text/Sales.cs
index 37db915..d7b5565 100644
--- a/Text/Sales.cs
+++ b/Text/Sales.cs
@@ -40,4 +40,5 @@ public static class Sales
"Your order [cyan]#{0}[/] ({1} x{2}) has been successfully completed by production!";
public const string OrderCompleteAcknowledge = "[grey]Press any key to acknowledge...[/]";
+ public const string NoUnsoldBatchesToSell = "[yellow]No unsold batches available to sell.[/]";
}
\ No newline at end of file
diff --git a/UI/AccountSettingsMenuHandler.cs b/UI/AccountSettingsMenuHandler.cs
index 4358362..fa57e03 100644
--- a/UI/AccountSettingsMenuHandler.cs
+++ b/UI/AccountSettingsMenuHandler.cs
@@ -71,7 +71,7 @@ private static void ChangeFullName(Employee user, IAccountService accountService
if (accountService.UpdateFullName(user, newName))
{
AnsiConsole.MarkupLine(AccountSettings.FullNameUpdated);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.FullNameUpdated, user.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.FullNameUpdated, user.Username);
UndoService.Instance.RegisterCommand(new ChangeFullNameCommand(user, oldName, newName, accountService,
user.Username));
}
@@ -111,7 +111,7 @@ private static void ChangeUsername(Employee user, IAccountService accountService
if (accountService.UpdateUsername(user, newUsername))
{
AnsiConsole.MarkupLine(AccountSettings.UsernameUpdated);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.UsernameUpdated, user.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.UsernameUpdated, user.Username);
UndoService.Instance.RegisterCommand(new ChangeUsernameCommand(user, oldUsername, newUsername,
accountService, user.Username));
}
@@ -167,7 +167,7 @@ private static void ChangePassword(Employee user, IAccountService accountService
if (accountService.UpdatePassword(user, newPassword))
{
AnsiConsole.MarkupLine(AccountSettings.PasswordUpdated);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.PasswordUpdated, user.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.PasswordUpdated, user.Username);
}
else
{
diff --git a/UI/AccountingMenuHandler.cs b/UI/AccountingMenuHandler.cs
index 6338166..dca40cd 100644
--- a/UI/AccountingMenuHandler.cs
+++ b/UI/AccountingMenuHandler.cs
@@ -1,10 +1,12 @@
+using System.Text;
using Spectre.Console;
namespace Smart_Factory_Management_System;
internal static class AccountingMenuHandler
{
- public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService)
+ public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService,
+ IFileSystemService fileSystem, IJsonRepository reportRequestsRepo)
{
if (loggedInUser is not Accountant)
{
@@ -30,13 +32,10 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
{
case "View Batches":
ShowBatches(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.BatchesViewed, loggedInUser.Username);
- break;
- case "Set Unit Sell Price for Batch":
- SetBatchPrice(factory, loggerService, loggedInUser);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.BatchesViewed, loggedInUser.Username);
break;
case "Process Report Requests":
- ProcessReportRequests(factory, loggedInUser, loggerService);
+ ProcessReportRequests(factory, loggedInUser, loggerService, fileSystem, reportRequestsRepo);
break;
case "Return to Main Menu":
return;
@@ -68,32 +67,9 @@ private static void ShowBatches(Factory factory)
AnsiConsole.Write(table);
}
- private static void SetBatchPrice(Factory factory, ILoggerService loggerService, Employee loggedInUser)
- {
- if (factory.BatchCount == 0)
- {
- AnsiConsole.MarkupLine(Accounting.NoBatchesToPrice);
- return;
- }
-
- var selector = new SelectionPrompt().Title(Accounting.SelectBatchToPrice);
- for (var i = 0; i < factory.BatchCount; i++) selector.AddChoice(factory.Batches[i]);
-
- var chosen = AnsiConsole.Prompt(selector);
- var price = AnsiConsole.Ask(Accounting.SetUnitPricePrompt);
- chosen.SetUnitSellPrice(price);
-
- // Apply price to linked inventory items
- foreach (var idx in chosen.InventoryIndexes)
- if (idx >= 0 && idx < factory.Inventory.Count)
- factory.Inventory[idx].UpdateSellingPrice(price);
-
- AnsiConsole.MarkupLine(string.Format(Accounting.BatchPriced, chosen.BatchId, price));
- loggerService.LogInfo(LogOrigin.USER, LogEvent.BatchPriceSet,
- $"Batch {chosen.BatchId} priced at ${price:F2} by {loggedInUser.Username}");
- }
- private static void ProcessReportRequests(Factory factory, Employee accountant, ILoggerService loggerService)
+ private static void ProcessReportRequests(Factory factory, Employee accountant, ILoggerService loggerService,
+ IFileSystemService fileSystem, IJsonRepository reportRequestsRepo)
{
AnsiConsole.Clear();
AnsiConsole.Write(new Rule($"[magenta]{Accounting.ProcessReportRequestsTitle}[/]").Centered());
@@ -114,10 +90,138 @@ private static void ProcessReportRequests(Factory factory, Employee accountant,
var chosen = AnsiConsole.Prompt(selector);
chosen.Fulfill();
+ reportRequestsRepo.Save(factory.PendingReportRequests);
+
+ ExportReportToCsv(chosen, factory, fileSystem);
AnsiConsole.MarkupLine(
string.Format(Accounting.RequestFulfilled, chosen.ReportType, chosen.RequestId, accountant.Name));
- loggerService.LogInfo(LogOrigin.USER, LogEvent.ReportRequestsProcessed,
+ loggerService.LogInfo(LogOrigin.User, LogEvent.ReportRequestsProcessed,
$"Report '{chosen.ReportType}' (Req ID: {chosen.RequestId}) fulfilled by {accountant.Username}");
}
+
+ private static void ExportReportToCsv(ReportRequest request, Factory factory, IFileSystemService fileSystem)
+ {
+ string directoryPath;
+ if (request.ExportDestination == "Desktop")
+ directoryPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
+ else
+ directoryPath = Path.Combine(fileSystem.BaseDirectory, "Reports");
+
+ if (!Directory.Exists(directoryPath)) Directory.CreateDirectory(directoryPath);
+
+ var fileName =
+ $"{request.ReportType.Replace(" ", "_")}_{request.RequestId}_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
+ var fullPath = Path.Combine(directoryPath, fileName);
+
+ var csvBuilder = new StringBuilder();
+
+ if (request.ReportType == "Production Summary")
+ {
+ var inventoryValue = factory.Inventory.Where(p => !p.IsSold).Sum(p => p.SellingPrice * p.Quantity);
+ var totalUnits = factory.Inventory.Where(p => !p.IsSold).Sum(p => p.Quantity);
+ csvBuilder.AppendLine("Metric,Value");
+ csvBuilder.AppendLine($"Generated By,{request.RequestedByDirectorName}");
+ csvBuilder.AppendLine($"Timestamp,{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
+ csvBuilder.AppendLine($"Inventory Item Types,{factory.Inventory.Count(p => !p.IsSold)}");
+ csvBuilder.AppendLine($"Inventory Units,{totalUnits}");
+ csvBuilder.AppendLine($"Est Inventory Value,${inventoryValue:F2}");
+ csvBuilder.AppendLine($"Employees,{factory.Employees.Count}");
+ csvBuilder.AppendLine($"Machines,{factory.Machines.Count}");
+
+ // Batches Section
+ csvBuilder.AppendLine();
+ csvBuilder.AppendLine("Production Batches");
+ csvBuilder.AppendLine("BatchId,Product,Qty,Production Cost/Unit,Sell Price/Unit,Status");
+
+ double totalExpenses = 0;
+ double totalIncome = 0;
+ foreach (var batch in factory.Batches)
+ {
+ totalExpenses += batch.TotalCost;
+ if (batch.IsSold && batch.UnitSellPrice.HasValue)
+ totalIncome += batch.UnitSellPrice.Value * batch.Quantity;
+
+ var status = batch.IsSold ? "Sold" : "Produced";
+ var sellPrice = batch.IsSold ? $"{batch.UnitSellPrice ?? 0.0:F2}" : "0.00";
+ csvBuilder.AppendLine(
+ $"{batch.BatchId},{batch.ProductName},{batch.Quantity},{batch.UnitProductionCost:F2},{sellPrice},{status}");
+ }
+
+ // Financial Summary Section
+ var totalProfit = totalIncome - totalExpenses;
+ csvBuilder.AppendLine();
+ csvBuilder.AppendLine("Financial Summary");
+ csvBuilder.AppendLine($"Total Expenses (Production Cost),${totalExpenses:F2}");
+ csvBuilder.AppendLine($"Total Income (Sales Revenue),${totalIncome:F2}");
+ csvBuilder.AppendLine($"Total Net Profit/Loss,${totalProfit:F2}");
+ }
+ else if (request.ReportType == "Employee Report")
+ {
+ csvBuilder.AppendLine("Id,Name,Username,Role,Activity");
+ foreach (var emp in factory.Employees)
+ {
+ var activity = emp.ShowActivity().Replace(",", ";");
+ csvBuilder.AppendLine($"{emp.Id},{emp.Name},{emp.Username},{emp.Role},{activity}");
+ }
+ }
+ else if (request.ReportType == "Order Backlog Summary")
+ {
+ csvBuilder.AppendLine("OrderId,Product,Qty,Completed,AssignedTechId,Status");
+ foreach (var order in factory.PendingOrders)
+ {
+ var status = order.IsComplete ? "Complete" : "In Progress";
+ csvBuilder.AppendLine(
+ $"{order.OrderId},{order.ProductName},{order.Quantity},{order.CompletedCount},{order.AssignedTechnicianId},{status}");
+ }
+ }
+
+ File.WriteAllText(fullPath, csvBuilder.ToString());
+ AnsiConsole.MarkupLine($"[green]✔ Success: Report exported to CSV at: [underline]{fullPath}[/][/]");
+ }
+
+ public static void RunQuickActions(Factory factory, Employee loggedInUser, ILoggerService loggerService,
+ IFileSystemService fileSystem, IJsonRepository reportRequestsRepo)
+ {
+ while (true)
+ {
+ AnsiConsole.Clear();
+ AnsiConsole.Write(new Rule($"[magenta]{Accounting.QuickActionsTitle}[/]").Centered());
+
+ var menuChoices = new List
+ {
+ Accounting.QuickActionViewBatches,
+ Accounting.QuickActionViewProductionSummary,
+ Accounting.QuickActionProcessReportRequests,
+ Accounting.QuickActionReturnToMainMenu
+ };
+
+ var choice = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title(Common.ChooseAction)
+ .AddChoices(menuChoices));
+
+ var option = choice.Split(". ", 2)[1];
+ switch (option)
+ {
+ case "View Batches":
+ ShowBatches(factory);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.BatchesViewed, loggedInUser.Username);
+ break;
+ case "Process Report Requests":
+ ProcessReportRequests(factory, loggedInUser, loggerService, fileSystem, reportRequestsRepo);
+ break;
+ case "View Production Summary":
+ ReportMenuHandler.ShowProductionSummary(factory, loggedInUser);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.ProductionSummaryReportGenerated,
+ loggedInUser.Username);
+ break;
+ case "Return to Main Menu":
+ return;
+ }
+
+ AnsiConsole.MarkupLine(Common.PressKeyToContinue);
+ Console.ReadKey(true);
+ }
+ }
}
\ No newline at end of file
diff --git a/UI/EmployeeMenuHandler.cs b/UI/EmployeeMenuHandler.cs
index badf567..18d8ac5 100644
--- a/UI/EmployeeMenuHandler.cs
+++ b/UI/EmployeeMenuHandler.cs
@@ -26,7 +26,7 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
case "View Registered Staff":
case "View All Registered Staff":
DisplayStaffTable(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.StaffViewed, loggedInUser.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.StaffViewed, loggedInUser.Username);
break;
case "Add New Employee":
@@ -38,6 +38,15 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
break;
+ case "Remove Employee":
+ if (loggedInUser is not Director)
+ AnsiConsole.MarkupLine(
+ string.Format(Common.AccessDenied, loggedInUser.Role));
+ else
+ RemoveEmployeeFlow(factory, repository, loggerService, loggedInUser);
+
+ break;
+
case "Return to Main Menu":
inRoom = false;
break;
@@ -108,7 +117,53 @@ private static void AddNewEmployeeFlow(Factory factory, IJsonRepository repository,
+ ILoggerService loggerService, Employee loggedInUser)
+ {
+ AnsiConsole.Clear();
+ AnsiConsole.Write(new Rule($"[red]{Employees.RemoveTitle}[/]").Centered());
+
+ var candidates = factory.Employees.Where(e => e.Id != loggedInUser.Id).ToList();
+
+ if (candidates.Count == 0)
+ {
+ AnsiConsole.MarkupLine(Employees.NoEmployeesToRemove);
+ return;
+ }
+
+ var selector = new SelectionPrompt()
+ .Title(Employees.SelectEmployeeToRemove)
+ .UseConverter(e => $"{e.Id} - {e.Name} ({e.Role})")
+ .AddChoices(candidates);
+
+ var chosen = AnsiConsole.Prompt(selector);
+
+ if (AnsiConsole.Confirm(string.Format(Employees.RemoveConfirmPrompt, chosen.Name, chosen.Role)))
+ {
+ factory.RemoveEmployee(chosen);
+
+ var users = repository.Load();
+ var matchedUser = users.FirstOrDefault(u => u.Id == chosen.Id);
+ if (matchedUser != null)
+ {
+ users.Remove(matchedUser);
+ repository.Save(users);
+ }
+
+ UndoService.Instance.RegisterCommand(new RemoveEmployeeCommand(chosen, factory, repository,
+ loggedInUser.Username));
+
+ AnsiConsole.MarkupLine(string.Format(Employees.RemoveSuccess, chosen.Name));
+ loggerService.LogInfo(LogOrigin.User, LogEvent.EmployeeRemoved,
+ $"Employee '{chosen.Username}' ({chosen.Role}) removed by '{loggedInUser.Username}'");
+ }
+ else
+ {
+ AnsiConsole.MarkupLine(Employees.RemoveCancelled);
+ }
+ }
}
\ No newline at end of file
diff --git a/UI/FactoryReportMenuHandler.cs b/UI/FactoryReportMenuHandler.cs
deleted file mode 100644
index 7ea3232..0000000
--- a/UI/FactoryReportMenuHandler.cs
+++ /dev/null
@@ -1,169 +0,0 @@
-using Spectre.Console;
-
-namespace Smart_Factory_Management_System;
-
-internal static class FactoryReportMenuHandler
-{
- public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService)
- {
- while (true)
- {
- AnsiConsole.Clear();
- AnsiConsole.Write(new Rule($"[cyan]{Reports.OverviewTitle}[/]").Centered());
-
- var menuOptions = MenuOptions.FactoryReportMenu.Select((item, index) => $"{index + 1}. {item}").ToList();
- var choice = AnsiConsole.Prompt(
- new SelectionPrompt()
- .Title(Reports.ChooseFactoryReport)
- .AddChoices(menuOptions));
-
- var option = choice.Split(". ", 2)[1];
- switch (option)
- {
- case "Factory Overview":
- ShowOverview(factory, loggedInUser);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.FactoryOverviewReportGenerated,
- loggedInUser.Username);
- break;
- case "Staffing Report":
- ShowStaffingReport(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.StaffingReportGenerated, loggedInUser.Username);
- break;
- case "Machine Fleet Report":
- ShowMachineFleetReport(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.MachineFleetReportGenerated, loggedInUser.Username);
- break;
- case "Inventory Report":
- ShowInventoryReport(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.InventoryReportGenerated, loggedInUser.Username);
- break;
- case "Return to Main Menu":
- return;
- }
- }
- }
-
-
- private static void ShowOverview(Factory factory, Employee loggedInUser)
- {
- AnsiConsole.Clear();
- AnsiConsole.Write(new Rule($"[cyan]{Reports.FactoryOverviewTitle}[/]").Centered());
-
- var grid = new Grid().AddColumns(2);
- grid.AddRow(Reports.RequestedBy, Markup.Escape(loggedInUser.Name));
- grid.AddRow(Reports.Employees, factory.Employees.Count.ToString());
- grid.AddRow(Reports.Machines, factory.Machines.Count.ToString());
- grid.AddRow(Reports.InventoryItemTypes, factory.Inventory.Count.ToString());
- grid.AddRow(Reports.PendingOrders, factory.OrderCount.ToString());
- grid.AddRow(Reports.Batches, factory.BatchCount.ToString());
-
- AnsiConsole.Write(new Panel(grid).Border(BoxBorder.Rounded)
- .Header($"[bold]{Reports.FactorySnapshotHeader}[/]"));
- Pause();
- }
-
- private static void ShowStaffingReport(Factory factory)
- {
- AnsiConsole.Clear();
- AnsiConsole.Write(new Rule($"[cyan]{Reports.StaffingReportTitle}[/]").Centered());
-
- if (factory.Employees.Count == 0)
- {
- AnsiConsole.MarkupLine(Reports.NoStaff);
- Pause();
- return;
- }
-
- EmployeeMenuHandler.DisplayStaffTable(factory);
- Pause();
- }
-
- private static void ShowMachineFleetReport(Factory factory)
- {
- AnsiConsole.Clear();
- AnsiConsole.Write(new Rule($"[cyan]{Reports.MachineFleetReportTitle}[/]").Centered());
-
- if (factory.Machines.Count == 0)
- {
- AnsiConsole.MarkupLine(Reports.NoMachines);
- Pause();
- return;
- }
-
- var table = new Table().Border(TableBorder.Rounded);
- table.AddColumn("ID");
- table.AddColumn("Name");
- table.AddColumn("Manufacturer");
- table.AddColumn("Condition");
- table.AddColumn("Status");
- table.AddColumn("Age (days)");
-
- foreach (var machine in factory.Machines)
- {
- table.AddRow(
- machine.Id.ToString(),
- machine.Name ?? "-",
- machine.Manufacturer ?? "-",
- machine.Condition.ToString(),
- machine.Status.ToString(),
- ((int)machine.GetMachineAge().TotalDays).ToString()
- );
- }
-
- AnsiConsole.Write(table);
- Pause();
- }
-
- private static void ShowInventoryReport(Factory factory)
- {
- AnsiConsole.Clear();
- AnsiConsole.Write(new Rule($"[cyan]{Reports.InventoryReportTitle}[/]").Centered());
-
- if (!factory.Inventory.Any(p => !p.IsSold))
- {
- AnsiConsole.MarkupLine(Reports.NoInventory);
- Pause();
- return;
- }
-
- var table = new Table().Border(TableBorder.Rounded);
- table.AddColumn("Item");
- table.AddColumn("Qty");
- table.AddColumn("Cost");
- table.AddColumn("Price");
- table.AddColumn("Inventory Value");
-
- double totalValue = 0;
- var totalQuantity = 0;
-
- foreach (var product in factory.Inventory.Where(p => !p.IsSold))
- {
- {
- var itemValue = product.SellingPrice * product.Quantity;
- totalValue += itemValue;
- totalQuantity += product.Quantity;
-
- table.AddRow(
- product.Name ?? "-",
- product.Quantity.ToString(),
- $"${product.ProductionCost:F2}",
- $"${product.SellingPrice:F2}",
- $"${itemValue:F2}"
- );
- }
- }
-
- AnsiConsole.Write(table);
- AnsiConsole.Write(new Panel(new Markup(
- $"{Reports.TotalUnits} {totalQuantity}\n" +
- $"{Reports.EstInventoryValueTotal} ${totalValue:F2}")
- ).Border(BoxBorder.Rounded).Header($"[bold]{Reports.InventoryTotalsHeader}[/]"));
- Pause();
- }
-
- private static void Pause()
- {
- AnsiConsole.MarkupLine(Common.PressKeyToReturn);
- Console.ReadKey(true);
- }
-}
\ No newline at end of file
diff --git a/UI/LoginMenuHandler.cs b/UI/LoginMenuHandler.cs
index e6a15fb..46ac95f 100644
--- a/UI/LoginMenuHandler.cs
+++ b/UI/LoginMenuHandler.cs
@@ -40,14 +40,14 @@ internal static class LoginMenuHandler
// Authentication logic
if (matchedEmployee != null)
{
- loggerService.LogInfo(LogOrigin.SYSTEM, LogEvent.LoginSuccess, matchedEmployee.Username);
+ loggerService.LogInfo(LogOrigin.System, LogEvent.LoginSuccess, matchedEmployee.Username);
AnsiConsole.MarkupLine(Login.AccessGranted);
Thread.Sleep(600); // Visual feedback pause
return matchedEmployee;
}
// Error boundary feedback
- loggerService.LogInfo(LogOrigin.SYSTEM, LogEvent.LoginFailed, username);
+ loggerService.LogInfo(LogOrigin.System, LogEvent.LoginFailed, username);
AnsiConsole.MarkupLine(Login.InvalidCredentials);
AnsiConsole.MarkupLine(Login.PressKeyToTryAgain);
diff --git a/UI/MachineMenuHandler.cs b/UI/MachineMenuHandler.cs
index 20e24b2..de1e504 100644
--- a/UI/MachineMenuHandler.cs
+++ b/UI/MachineMenuHandler.cs
@@ -5,7 +5,8 @@ namespace Smart_Factory_Management_System;
internal static class MachineMenuHandler
{
public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService,
- IJsonRepository machineRepo, IJsonRepository productRepo)
+ IJsonRepository machineRepo, IJsonRepository productRepo,
+ IJsonRepository ordersRepo)
{
var inRoom = true;
while (inRoom)
@@ -24,7 +25,7 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
{
case "Overall Fleet Status Overview":
DisplayFleetOverview(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.FleetStatusViewed, loggedInUser.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.FleetStatusViewed, loggedInUser.Username);
break;
case "Run Deep Component Inspection":
@@ -42,7 +43,8 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
break;
case "Fulfill Pending Orders":
- ProductionMenuHandler.Run(factory, loggedInUser, loggerService, machineRepo, productRepo);
+ ProductionMenuHandler.Run(factory, loggedInUser, loggerService, machineRepo, productRepo,
+ ordersRepo);
break;
case "Return to Main Menu":
inRoom = false;
@@ -110,7 +112,7 @@ private static void RunInspection(Factory factory, ILoggerService loggerService)
AnsiConsole.MarkupLine(Machines.RepairNotNeeded);
}
- loggerService.LogInfo(LogOrigin.USER, LogEvent.MaintenancePerformed,
+ loggerService.LogInfo(LogOrigin.User, LogEvent.MaintenancePerformed,
$"{chosenMachine.Id} - {chosenMachine.Name}");
}
}
\ No newline at end of file
diff --git a/UI/PasswordChangeHandler.cs b/UI/PasswordChangeHandler.cs
index a0ac591..a1c7bb6 100644
--- a/UI/PasswordChangeHandler.cs
+++ b/UI/PasswordChangeHandler.cs
@@ -68,7 +68,7 @@ public static void Run(Employee user, IJsonRepository repository, ILog
}
AnsiConsole.MarkupLine(Login.PasswordUpdated);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.PasswordChangedFirstLogin, user.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.PasswordChangedFirstLogin, user.Username);
Thread.Sleep(1000);
}
}
\ No newline at end of file
diff --git a/UI/ProductMenuHandler.cs b/UI/ProductMenuHandler.cs
index e4ff369..416dd69 100644
--- a/UI/ProductMenuHandler.cs
+++ b/UI/ProductMenuHandler.cs
@@ -1,11 +1,11 @@
-using Spectre.Console;
+using Spectre.Console;
namespace Smart_Factory_Management_System;
internal static class ProductMenuHandler
{
public static void Run(Factory factory, Employee currentUser, ILoggerService loggerService,
- IJsonRepository productRepo)
+ IJsonRepository productRepo, IJsonRepository ordersRepo)
{
while (true)
{
@@ -17,14 +17,25 @@ public static void Run(Factory factory, Employee currentUser, ILoggerService log
summaryGrid.AddColumn();
summaryGrid.AddRow(new Markup(
string.Format(Products.OperatorSession, currentUser.Name, currentUser.Role)));
+
+ var totalUnits = factory.GetTotalUnsoldUnits();
+ var maxUnits = factory.MaxCapacity;
+ var activeBatches = factory.Batches.Count(b => !b.IsSold);
+ var maxBatches = factory.MaxBatches;
+
summaryGrid.AddRow(new Markup(
- string.Format(Products.WarehouseStock, factory.Inventory.Count, factory.InventoryCapacity)));
+ $"[grey]Warehouse Storage Stock:[/] [green]{totalUnits} / {maxUnits} units[/] ({activeBatches} / {maxBatches} batches)"));
+
+ var isLowInventory = totalUnits < factory.MinStockThreshold;
+ if (isLowInventory)
+ summaryGrid.AddRow(new Markup(
+ $"\n[blink bold red]⚠️ INVENTORY ALERT: Stock level ({totalUnits} units) is below the minimum threshold of {factory.MinStockThreshold} units! Please start production orders. ⚠️[/]"));
AnsiConsole.Write(
new Panel(summaryGrid)
.Header(Products.PanelHeader)
.Border(BoxBorder.Rounded)
- .BorderStyle(new Style(Color.Blue))
+ .BorderStyle(new Style(isLowInventory ? Color.Red : Color.Blue))
);
AnsiConsole.WriteLine();
@@ -40,16 +51,16 @@ public static void Run(Factory factory, Employee currentUser, ILoggerService log
{
case "View Finished Goods Stock":
DisplayInventoryTable(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.InventoryViewed, currentUser.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.InventoryViewed, currentUser.Username);
break;
case "View Inventory Financial & Capacity Analytics":
DisplayInventoryAnalytics(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.InventoryAnalyticsViewed, currentUser.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.InventoryAnalyticsViewed, currentUser.Username);
break;
case "Sales & Orders":
// Reuse Sales menu view; if user is SalesAgent, open full Sales UI
if (currentUser is SalesAgent || currentUser is Director)
- SalesMenuHandler.Run(factory, currentUser, loggerService, productRepo);
+ SalesMenuHandler.Run(factory, currentUser, loggerService, productRepo, ordersRepo);
else
SalesMenuHandler.ShowPendingOrders(factory);
break;
@@ -122,20 +133,22 @@ private static void DisplayInventoryAnalytics(Factory factory)
foreach (var product in unsoldProducts)
{
- cumulativeValue += product.SellingPrice;
+ cumulativeValue += product.SellingPrice * product.Quantity;
// Type checking subclasses safely for metrics grouping
- if (product is Microprocessor) cpuCount++;
- else if (product is Motherboard) pcbCount++;
+ if (product is Microprocessor) cpuCount += product.Quantity;
+ else if (product is Motherboard) pcbCount += product.Quantity;
}
- var storageUtilization = unsoldProducts.Count == 0
+ var unsoldUnitsCount = factory.GetTotalUnsoldUnits();
+ var activeBatches = factory.Batches.Count(b => !b.IsSold);
+ var storageUtilization = unsoldUnitsCount == 0
? 0.0
- : (double)unsoldProducts.Count / factory.InventoryCapacity * 100;
+ : (double)unsoldUnitsCount / factory.MaxCapacity * 100;
var statsGrid = new Grid().AddColumns(2);
statsGrid.AddRow(Products.StatsGridTotalVolume,
- string.Format(Products.StatsGridTotalVolumeValue, unsoldProducts.Count, cpuCount, pcbCount));
+ string.Format(Products.StatsGridTotalVolumeValue, unsoldUnitsCount, cpuCount, pcbCount));
statsGrid.AddRow(Products.StatsGridValuation, string.Format(Products.StatsGridValuationValue, cumulativeValue));
statsGrid.AddRow(Products.StatsGridOccupancy,
string.Format(Products.StatsGridOccupancyValue, storageUtilization));
diff --git a/UI/ProductionMenuHandler.cs b/UI/ProductionMenuHandler.cs
index ad604e2..fb545b9 100644
--- a/UI/ProductionMenuHandler.cs
+++ b/UI/ProductionMenuHandler.cs
@@ -5,7 +5,8 @@ namespace Smart_Factory_Management_System;
internal static class ProductionMenuHandler
{
public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService,
- IJsonRepository machineRepo, IJsonRepository productRepo)
+ IJsonRepository machineRepo, IJsonRepository productRepo,
+ IJsonRepository ordersRepo)
{
AnsiConsole.Clear();
AnsiConsole.Write(Align.Left(new Rule($"[yellow]{Production.Title}[/]")));
@@ -19,6 +20,17 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
return;
}
+ if (factory.Batches.Count(b => !b.IsSold) >= factory.MaxBatches)
+ {
+ AnsiConsole.MarkupLine(
+ $"[red]Error: Warehouse is at maximum batch capacity ({factory.MaxBatches} active batches).[/]");
+ AnsiConsole.MarkupLine(
+ "[red]Please record sales to clear warehouse space before fulfilling new orders.[/]");
+ AnsiConsole.WriteLine(Common.PressKeyToReturn);
+ Console.ReadKey(true);
+ return;
+ }
+
var pendingOrders = GetPendingOrders(factory);
if (pendingOrders.Count == 0)
{
@@ -46,7 +58,7 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
if (order.ProductName == "Motherboard")
{
- RunMotherboardWorkflow(factory, order, loggerService);
+ RunMotherboardWorkflow(factory, order, loggerService, ordersRepo);
machineRepo.Save(factory.Machines);
productRepo.Save(factory.Inventory);
AnsiConsole.WriteLine();
@@ -104,42 +116,138 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
var batch = StartProductionBatch(factory, order, unitCost);
- AnsiConsole.Write(new Markup(
- string.Format(Production.StartProduction, order.OrderId, order.ProductName, order.Quantity)));
+ Machine.SilentMode = true;
- while (!order.IsComplete && selectedMachine.Status == MachineStatus.Running)
- {
- var produced = CreateProducedProduct(template);
-
- selectedMachine.StartOrder(order);
- var success = selectedMachine.Produce(produced);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.ProductionStarted,
- $"${order.ProductName} * {order.Quantity}");
+ var liveGrid = new Markup("");
+ AnsiConsole.Clear();
+ AnsiConsole.Write(new Rule(string.Format(Production.ProductionLineTitle, selectedMachine.Name)).Centered());
+ AnsiConsole.WriteLine();
- if (success)
+ AnsiConsole.Live(liveGrid)
+ .Start(ctx =>
{
- var completedCount = CommitProducedUnit(factory, order, batch, produced);
- AnsiConsole.MarkupLine(
- string.Format(Production.ProducedOneUnit, produced.Name, completedCount, order.Quantity));
- }
+ while (!order.IsComplete && selectedMachine.Status == MachineStatus.Running)
+ {
+ var produced = CreateProducedProduct(template);
+
+ selectedMachine.StartOrder(order);
+
+ // Simulate longer active machinery delay
+ Thread.Sleep(1200);
+
+ var success = selectedMachine.Produce(produced);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.ProductionStarted,
+ $"${order.ProductName} * {order.Quantity}");
+
+ if (success) CommitProducedUnit(factory, order, batch, produced);
+
+ // Render updated static state
+ var progressPercentage = (double)order.CompletedCount / order.Quantity * 100;
+ var progressBar = GetProgressBar(order.CompletedCount, order.Quantity);
+
+ var table = new Table().Border(TableBorder.Rounded).Expand();
+ table.AddColumn("Asset");
+ table.AddColumn("Operation Progress");
+ table.AddColumn("Status");
+
+ var statusString = selectedMachine.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : "[red]TRIPPED[/]";
+
+ table.AddRow(
+ selectedMachine.Name ?? "Machine",
+ $"{progressBar} {order.CompletedCount} / {order.Quantity} ({progressPercentage:F0}%)",
+ statusString
+ );
+
+ var outerGrid = new Grid().AddColumn();
+ outerGrid.AddRow(table);
+
+ if (selectedMachine.Status != MachineStatus.Running)
+ outerGrid.AddRow(
+ new Markup($"\n{string.Format(Production.MachineTrippedAlert, selectedMachine.Name)}"));
+
+ outerGrid.AddRow(new Align(
+ new Markup(string.Format(Production.CompletedUnitsStatus, order.CompletedCount,
+ order.Quantity)),
+ HorizontalAlignment.Right
+ ));
+
+ ctx.UpdateTarget(outerGrid);
+
+ if (selectedMachine.Status != MachineStatus.Running) break;
+ }
+
+ // Final draw update
+ var finalProgressPercentage = (double)order.CompletedCount / order.Quantity * 100;
+ var finalProgressBar = GetProgressBar(order.CompletedCount, order.Quantity);
+
+ var finalTable = new Table().Border(TableBorder.Rounded).Expand();
+ finalTable.AddColumn("Asset");
+ finalTable.AddColumn("Operation Progress");
+ finalTable.AddColumn("Status");
+
+ var finalStatusString = selectedMachine.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : "[red]TRIPPED[/]";
+
+ finalTable.AddRow(
+ selectedMachine.Name ?? "Machine",
+ $"{finalProgressBar} {order.CompletedCount} / {order.Quantity} ({finalProgressPercentage:F0}%)",
+ finalStatusString
+ );
+
+ var finalOuterGrid = new Grid().AddColumn();
+ finalOuterGrid.AddRow(finalTable);
+
+ if (selectedMachine.Status != MachineStatus.Running)
+ finalOuterGrid.AddRow(
+ new Markup($"\n{string.Format(Production.MachineTrippedAlert, selectedMachine.Name)}"));
- if (selectedMachine.Status != MachineStatus.Running)
+ finalOuterGrid.AddRow(new Align(
+ new Markup(string.Format(Production.CompletedUnitsStatus, order.CompletedCount, order.Quantity)),
+ HorizontalAlignment.Right
+ ));
+
+ ctx.UpdateTarget(finalOuterGrid);
+ });
+
+ Machine.SilentMode = false;
+
+ if (order.CompletedCount > 0)
+ {
+ batch.Quantity = order.CompletedCount;
+
+ if (order.IsComplete)
{
- AnsiConsole.MarkupLine(Production.MachineTripped);
- break;
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine(Production.CostAdvice);
+ var actualCost = AnsiConsole.Prompt(
+ new TextPrompt(Production.CostPrompt)
+ .ValidationErrorMessage(Production.CostValidationInvalidNumber)
+ .Validate(c =>
+ c >= 0
+ ? ValidationResult.Success()
+ : ValidationResult.Error(Production.CostValidationNegative)));
+
+ batch.UnitProductionCost = actualCost;
+
+ foreach (var product in factory.Inventory)
+ if (product.BatchId == batch.BatchId)
+ product.ProductionCost = actualCost;
}
}
if (order.IsComplete)
{
- loggerService.LogInfo(LogOrigin.SYSTEM, LogEvent.ProductionCompleted,
+ loggerService.LogInfo(LogOrigin.System, LogEvent.ProductionCompleted,
$"{batch.ProductName} * {batch.Quantity}");
AnsiConsole.MarkupLine(
string.Format(Production.BatchCompleted, batch.BatchId, batch.InventoryIndexes.Count));
}
else
{
- loggerService.LogWarning(LogOrigin.SYSTEM, LogEvent.ProductionInterrupted,
+ loggerService.LogWarning(LogOrigin.System, LogEvent.ProductionInterrupted,
$"{order.ProductName} * {order.Quantity}");
AnsiConsole.MarkupLine(
string.Format(Production.OrderIncomplete, order.CompletedCount, order.Quantity));
@@ -147,6 +255,7 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
machineRepo.Save(factory.Machines);
productRepo.Save(factory.Inventory);
+ ordersRepo.Save(factory.PendingOrders);
AnsiConsole.WriteLine();
AnsiConsole.Write(new Markup(Common.PressKeyToReturnProduction));
@@ -178,7 +287,8 @@ private static Product CreateProducedProduct(Product template)
};
}
- private static void RunMotherboardWorkflow(Factory factory, ProductionOrder order, ILoggerService loggerService)
+ private static void RunMotherboardWorkflow(Factory factory, ProductionOrder order, ILoggerService loggerService,
+ IJsonRepository ordersRepo)
{
var batch = StartProductionBatch(factory, order, 20);
@@ -192,91 +302,201 @@ private static void RunMotherboardWorkflow(Factory factory, ProductionOrder orde
return;
}
- AnsiConsole.MarkupLine(
- string.Format(Production.StartMotherboardWorkflow, order.OrderId, order.ProductName, order.Quantity));
- loggerService.LogInfo(LogOrigin.USER, LogEvent.ProductionStarted,
- $"{order.ProductName} * {order.Quantity}");
+ Machine.SilentMode = true;
+
+ var liveGrid = new Markup("");
+ AnsiConsole.Clear();
+ AnsiConsole.Write(new Rule(Production.MotherboardLineTitle).Centered());
+ AnsiConsole.WriteLine();
var motherboardTemplate = new Motherboard(order.CustomProductName, 20, 0, 1, order.SocketStandard ?? "AM4",
order.PhysicalForm ?? "ATX");
- while (!order.IsComplete)
- {
- var board = factory.Inventory.OfType()
- .FirstOrDefault(b =>
- b.Name == order.CustomProductName && b.CurrentState != BoardState.BakedAndSoldered && !b.IsSold);
-
- var isNewBoard = false;
- if (board == null)
- {
- board = (Motherboard)CreateProducedProduct(motherboardTemplate);
- isNewBoard = true;
- }
-
- board.BatchId = batch.BatchId;
-
- if (isNewBoard) factory.AddProduct(board, batch.BatchId);
-
- if (board.CurrentState == BoardState.BlankBoard)
- if (!RunMotherboardStage(
- solderPrinter,
- order,
- board,
- Production.SolderPastePrinting,
- Production.SolderPastePrintingSpinner,
- Production.SolderPastePrintingFail))
- break;
-
- if (board.CurrentState == BoardState.SolderPrinted)
- if (!RunMotherboardStage(
- pickAndPlace,
- order,
- board,
- Production.PickAndPlace,
- Production.PickAndPlaceSpinner,
- Production.PickAndPlaceFail))
- break;
-
- if (board.CurrentState == BoardState.ComponentsPlaced)
- if (!RunMotherboardStage(
- reflowOven,
- order,
- board,
- Production.ReflowBaking,
- Production.ReflowBakingSpinner,
- Production.ReflowBakingFail))
- break;
-
- if (board.CurrentState == BoardState.BakedAndSoldered)
+ AnsiConsole.Live(liveGrid)
+ .Start(ctx =>
{
- order.IncrementCompletedCount();
-
- var completedBoard = factory.Inventory.OfType()
- .FirstOrDefault(b =>
- b.BatchId == batch.BatchId && b != board && b.CurrentState == BoardState.BakedAndSoldered &&
- !b.IsSold);
-
- if (completedBoard != null)
+ while (!order.IsComplete)
{
- completedBoard.AddQuantity(board.Quantity);
- factory.RemoveProduct(board);
+ // Refresh view
+ var progressPercentage = (double)order.CompletedCount / order.Quantity * 100;
+ var progressBar = GetProgressBar(order.CompletedCount, order.Quantity);
+
+ var table = new Table().Border(TableBorder.Rounded).Expand();
+ table.AddColumn("Production Step (Machine)");
+ table.AddColumn("Status");
+
+ table.AddRow(Production.StepSmt,
+ solderPrinter.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : solderPrinter.Condition == MachineCondition.Critical
+ ? "[red]TRIPPED[/]"
+ : "[yellow]IDLE[/]");
+ table.AddRow(Production.StepPap,
+ pickAndPlace.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : pickAndPlace.Condition == MachineCondition.Critical
+ ? "[red]TRIPPED[/]"
+ : "[yellow]IDLE[/]");
+ table.AddRow(Production.StepOven,
+ reflowOven.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : reflowOven.Condition == MachineCondition.Critical
+ ? "[red]TRIPPED[/]"
+ : "[yellow]IDLE[/]");
+
+ var outerGrid = new Grid().AddColumn();
+ outerGrid.AddRow(new Markup(
+ $"[bold cyan]Progress:[/] {progressBar} {order.CompletedCount}/{order.Quantity} ({progressPercentage:F0}%)\n"));
+ outerGrid.AddRow(table);
+
+ if (solderPrinter.Condition == MachineCondition.Critical ||
+ pickAndPlace.Condition == MachineCondition.Critical ||
+ reflowOven.Condition == MachineCondition.Critical)
+ outerGrid.AddRow(new Markup($"\n{Production.WorkflowTrippedAlert}"));
+
+ outerGrid.AddRow(new Align(
+ new Markup(string.Format(Production.CompletedUnitsStatus, order.CompletedCount,
+ order.Quantity)),
+ HorizontalAlignment.Right
+ ));
+
+ ctx.UpdateTarget(outerGrid);
+
+ if (solderPrinter.Condition == MachineCondition.Critical ||
+ pickAndPlace.Condition == MachineCondition.Critical ||
+ reflowOven.Condition == MachineCondition.Critical)
+ break;
+
+ var board = factory.Inventory.OfType()
+ .FirstOrDefault(b =>
+ b.Name == order.CustomProductName && b.CurrentState != BoardState.BakedAndSoldered &&
+ !b.IsSold);
+
+ var isNewBoard = false;
+ if (board == null)
+ {
+ board = (Motherboard)CreateProducedProduct(motherboardTemplate);
+ isNewBoard = true;
+ }
+
+ board.BatchId = batch.BatchId;
+ if (isNewBoard) factory.AddProduct(board, batch.BatchId);
+
+ // Run Solder Printer stage
+ if (board.CurrentState == BoardState.BlankBoard)
+ if (!RunMotherboardStage(solderPrinter, order, board, Production.SolderPastePrinting, "", ""))
+ break;
+
+ // Run Pick & Place stage
+ if (board.CurrentState == BoardState.SolderPrinted)
+ if (!RunMotherboardStage(pickAndPlace, order, board, Production.PickAndPlace, "", ""))
+ break;
+
+ // Run Reflow Oven stage
+ if (board.CurrentState == BoardState.ComponentsPlaced)
+ if (!RunMotherboardStage(reflowOven, order, board, Production.ReflowBaking, "", ""))
+ break;
+
+ if (board.CurrentState == BoardState.BakedAndSoldered)
+ {
+ order.IncrementCompletedCount();
+
+ var completedBoard = factory.Inventory.OfType()
+ .FirstOrDefault(b =>
+ b.BatchId == batch.BatchId && b != board && b is
+ { CurrentState: BoardState.BakedAndSoldered, IsSold: false });
+
+ if (completedBoard != null)
+ {
+ completedBoard.AddQuantity(board.Quantity);
+ factory.RemoveProduct(board);
+ }
+ }
}
- AnsiConsole.MarkupLine(string.Format(Production.MotherboardCompleted, order.CompletedCount,
- order.Quantity));
+ // Final draw update
+ var finalProgressPercentage = (double)order.CompletedCount / order.Quantity * 100;
+ var finalProgressBar = GetProgressBar(order.CompletedCount, order.Quantity);
+
+ var finalTable = new Table().Border(TableBorder.Rounded).Expand();
+ finalTable.AddColumn("Production Step (Machine)");
+ finalTable.AddColumn("Status");
+
+ finalTable.AddRow(Production.StepSmt,
+ solderPrinter.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : solderPrinter.Condition == MachineCondition.Critical
+ ? "[red]TRIPPED[/]"
+ : "[yellow]IDLE[/]");
+ finalTable.AddRow(Production.StepPap,
+ pickAndPlace.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : pickAndPlace.Condition == MachineCondition.Critical
+ ? "[red]TRIPPED[/]"
+ : "[yellow]IDLE[/]");
+ finalTable.AddRow(Production.StepOven,
+ reflowOven.Status == MachineStatus.Running
+ ? "[green]RUNNING[/]"
+ : reflowOven.Condition == MachineCondition.Critical
+ ? "[red]TRIPPED[/]"
+ : "[yellow]IDLE[/]");
+
+ var finalOuterGrid = new Grid().AddColumn();
+ finalOuterGrid.AddRow(new Markup(
+ $"[bold cyan]Progress:[/] {finalProgressBar} {order.CompletedCount}/{order.Quantity} ({finalProgressPercentage:F0}%)\n"));
+ finalOuterGrid.AddRow(finalTable);
+
+ if (solderPrinter.Condition == MachineCondition.Critical ||
+ pickAndPlace.Condition == MachineCondition.Critical ||
+ reflowOven.Condition == MachineCondition.Critical)
+ finalOuterGrid.AddRow(new Markup($"\n{Production.WorkflowTrippedAlert}"));
+
+ finalOuterGrid.AddRow(new Align(
+ new Markup(string.Format(Production.CompletedUnitsStatus, order.CompletedCount, order.Quantity)),
+ HorizontalAlignment.Right
+ ));
+
+ ctx.UpdateTarget(finalOuterGrid);
+ });
+
+ Machine.SilentMode = false;
+
+ if (order.CompletedCount > 0)
+ {
+ batch.Quantity = order.CompletedCount;
+
+ if (order.IsComplete)
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine(Production.CostAdvice);
+ var actualCost = AnsiConsole.Prompt(
+ new TextPrompt(Production.CostPrompt)
+ .ValidationErrorMessage(Production.CostValidationInvalidNumber)
+ .Validate(c =>
+ c >= 0
+ ? ValidationResult.Success()
+ : ValidationResult.Error(Production.CostValidationNegative)));
+
+ batch.UnitProductionCost = actualCost;
+
+ foreach (var product in factory.Inventory)
+ if (product.BatchId == batch.BatchId)
+ product.ProductionCost = actualCost;
}
}
+ ordersRepo.Save(factory.PendingOrders);
+
if (order.IsComplete)
{
- loggerService.LogInfo(LogOrigin.SYSTEM, LogEvent.ProductionCompleted,
+ loggerService.LogInfo(LogOrigin.System, LogEvent.ProductionCompleted,
$"{batch.ProductName} * {batch.Quantity}");
AnsiConsole.MarkupLine(
string.Format(Production.BatchCompleted, batch.BatchId, batch.InventoryIndexes.Count));
}
else
{
- loggerService.LogWarning(LogOrigin.SYSTEM, LogEvent.ProductionInterrupted,
+ loggerService.LogWarning(LogOrigin.System, LogEvent.ProductionInterrupted,
$"{order.ProductName} * {order.Quantity}");
AnsiConsole.MarkupLine(
string.Format(Production.OrderIncomplete, order.CompletedCount, order.Quantity));
@@ -296,19 +516,27 @@ private static bool RunMotherboardStage(
if (machine.Status != MachineStatus.Running)
{
- AnsiConsole.MarkupLine($"[red]{failureMessage}[/]");
+ if (!Machine.SilentMode) AnsiConsole.MarkupLine($"[red]{failureMessage}[/]");
return false;
}
- AnsiConsole.Write(new Rule($"[cyan]{stageName}[/]").Centered());
- AnsiConsole.Status()
- .Spinner(Spinner.Known.BouncingBar)
- .SpinnerStyle(Style.Parse("cyan bold"))
- .Start(spinnerText, _ => { Thread.Sleep(700); });
+ if (!Machine.SilentMode)
+ {
+ AnsiConsole.Write(new Rule($"[cyan]{stageName}[/]").Centered());
+ AnsiConsole.Status()
+ .Spinner(Spinner.Known.BouncingBar)
+ .SpinnerStyle(Style.Parse("cyan bold"))
+ .Start(spinnerText, _ => { Thread.Sleep(700); });
+ }
+ else
+ {
+ // Simulate the active machine delay!
+ Thread.Sleep(500);
+ }
if (!machine.Produce(board))
{
- AnsiConsole.MarkupLine($"[red]{failureMessage}[/]");
+ if (!Machine.SilentMode) AnsiConsole.MarkupLine($"[red]{failureMessage}[/]");
return false;
}
@@ -337,7 +565,18 @@ private static List GetPendingOrders(Factory factory)
private static ProductionBatch StartProductionBatch(Factory factory, ProductionOrder order, double unitCost)
{
+ if (!string.IsNullOrEmpty(order.BatchId))
+ {
+ var existing = factory.Batches.FirstOrDefault(b => b.BatchId == order.BatchId);
+ if (existing != null)
+ {
+ existing.Quantity = order.Quantity;
+ return existing;
+ }
+ }
+
var batch = new ProductionBatch(order.ProductName, order.Quantity, unitCost);
+ order.BatchId = batch.BatchId;
factory.AddBatch(batch);
return batch;
}
@@ -358,4 +597,14 @@ private static int CommitProducedUnit(Factory factory, ProductionOrder order, Pr
return null;
}
+
+ private static string GetProgressBar(int completed, int total)
+ {
+ var width = 20;
+ var completedWidth = total > 0 ? (int)Math.Round((double)completed / total * width) : 0;
+ if (completedWidth > width) completedWidth = width;
+ var filled = new string('█', completedWidth);
+ var empty = new string('░', width - completedWidth);
+ return $"[green]{filled}[/][grey]{empty}[/]";
+ }
}
\ No newline at end of file
diff --git a/UI/Program.cs b/UI/Program.cs
index aa60e51..d6443ea 100644
--- a/UI/Program.cs
+++ b/UI/Program.cs
@@ -17,6 +17,8 @@ private static void Main()
var employeeRepo = new JsonRepository(fileSystem, "employees.json");
var machinesRepo = new JsonRepository(fileSystem, "machines.json");
var productsRepo = new JsonRepository(fileSystem, "products.json");
+ var ordersRepo = new JsonRepository(fileSystem, "orders.json");
+ var reportRequestsRepo = new JsonRepository(fileSystem, "report_requests.json");
var accountService = new AccountService(employeeRepo);
var notificationService = new NotificationService();
@@ -26,7 +28,9 @@ private static void Main()
factory.LoadFromRepository(
employeeRepo.Load(),
machinesRepo.Load(),
- productsRepo.Load()
+ productsRepo.Load(),
+ ordersRepo.Load(),
+ reportRequestsRepo.Load()
);
while (true)
@@ -37,7 +41,7 @@ private static void Main()
var loggedInUser = LoginMenuHandler.ShowLoginScreen(employeeRepo, loggerService);
if (loggedInUser == null)
{
- loggerService.LogInfo(LogOrigin.SYSTEM, LogEvent.ClosingApplication, loggedInUser?.Username);
+ loggerService.LogInfo(LogOrigin.System, LogEvent.ClosingApplication, loggedInUser?.Username);
AnsiConsole.MarkupLine(Login.AppShuttingDown);
break;
}
@@ -53,7 +57,7 @@ private static void Main()
while (sessionActive)
{
// Render header and session info
- TuiHelper.RenderSessionHeader(loggedInUser, factory);
+ TuiHelper.RenderSessionHeader(loggedInUser);
// Build role-filtered main menu
var available = loggedInUser.GetAvailableMenuOptions().ToList();
@@ -81,7 +85,8 @@ private static void Main()
if (option == loggedInUser.QuickActionName)
{
- ExecuteQuickAction(loggedInUser, factory, employeeRepo, loggerService, machinesRepo, productsRepo);
+ ExecuteQuickAction(loggedInUser, factory, employeeRepo, loggerService, machinesRepo, productsRepo,
+ ordersRepo, reportRequestsRepo, fileSystem);
}
else
{
@@ -91,10 +96,11 @@ private static void Main()
EmployeeMenuHandler.Run(factory, loggedInUser, loggerService, employeeRepo);
break;
case "Machine Management":
- MachineMenuHandler.Run(factory, loggedInUser, loggerService, machinesRepo, productsRepo);
+ MachineMenuHandler.Run(factory, loggedInUser, loggerService, machinesRepo, productsRepo,
+ ordersRepo);
break;
case "Product Management":
- ProductMenuHandler.Run(factory, loggedInUser, loggerService, productsRepo);
+ ProductMenuHandler.Run(factory, loggedInUser, loggerService, productsRepo, ordersRepo);
break;
case "View Operation History":
loggerService.ShowOperationHistory();
@@ -102,13 +108,11 @@ private static void Main()
Console.ReadKey(true);
break;
case "Accounting":
- AccountingMenuHandler.Run(factory, loggedInUser, loggerService);
+ AccountingMenuHandler.Run(factory, loggedInUser, loggerService, fileSystem,
+ reportRequestsRepo);
break;
case "Reports":
- ReportMenuHandler.Run(factory, loggedInUser, loggerService);
- break;
- case "Factory Information":
- FactoryReportMenuHandler.Run(factory, loggedInUser, loggerService);
+ ReportMenuHandler.Run(factory, loggedInUser, loggerService, reportRequestsRepo);
break;
case "Account Settings":
AccountSettingsMenuHandler.Run(loggedInUser, accountService, loggerService);
@@ -117,7 +121,7 @@ private static void Main()
UndoMenuHandler.Run(loggedInUser, loggerService);
break;
case "Log Out / Exit Session":
- loggerService.LogInfo(LogOrigin.SYSTEM, LogEvent.Logout, loggedInUser.Username);
+ loggerService.LogInfo(LogOrigin.System, LogEvent.Logout, loggedInUser.Username);
AnsiConsole.MarkupLine(Login.LoggedOut);
Thread.Sleep(600);
sessionActive = false;
@@ -129,11 +133,15 @@ private static void Main()
}
private static void ExecuteQuickAction(Employee user, Factory factory, IJsonRepository authRepository,
- ILoggerService loggerService, IJsonRepository machinesRepo, IJsonRepository productsRepo)
+ ILoggerService loggerService, IJsonRepository machinesRepo, IJsonRepository productsRepo,
+ IJsonRepository ordersRepo, IJsonRepository reportRequestsRepo,
+ IFileSystemService fileSystem)
{
if (user is Director) EmployeeMenuHandler.Run(factory, user, loggerService, authRepository);
- else if (user is Technician) MachineMenuHandler.Run(factory, user, loggerService, machinesRepo, productsRepo);
- else if (user is SalesAgent) SalesMenuHandler.Run(factory, user, loggerService, productsRepo);
- else if (user is Accountant) AccountingMenuHandler.Run(factory, user, loggerService);
+ else if (user is Technician)
+ MachineMenuHandler.Run(factory, user, loggerService, machinesRepo, productsRepo, ordersRepo);
+ else if (user is SalesAgent) SalesMenuHandler.Run(factory, user, loggerService, productsRepo, ordersRepo);
+ else if (user is Accountant)
+ AccountingMenuHandler.RunQuickActions(factory, user, loggerService, fileSystem, reportRequestsRepo);
}
}
\ No newline at end of file
diff --git a/UI/ReportMenuHandler.cs b/UI/ReportMenuHandler.cs
index 1aa2afe..eb1a989 100644
--- a/UI/ReportMenuHandler.cs
+++ b/UI/ReportMenuHandler.cs
@@ -4,7 +4,8 @@ namespace Smart_Factory_Management_System;
internal static class ReportMenuHandler
{
- public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService)
+ public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService,
+ IJsonRepository reportRequestsRepo)
{
while (true)
{
@@ -25,23 +26,19 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
{
case "Production Summary":
ShowProductionSummary(factory, loggedInUser);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.ProductionSummaryReportGenerated,
+ loggerService.LogInfo(LogOrigin.User, LogEvent.ProductionSummaryReportGenerated,
loggedInUser.Username);
break;
case "Employee Report":
ShowEmployeeReport(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.EmployeeReportGenerated, loggedInUser.Username);
- break;
- case "Batch Revenue Summary":
- ShowBatchRevenueSummary(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.BatchRevenueReportGenerated, loggedInUser.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.EmployeeReportGenerated, loggedInUser.Username);
break;
case "Order Backlog Summary":
ShowOrderBacklogSummary(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.OrderBacklogReportGenerated, loggedInUser.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.OrderBacklogReportGenerated, loggedInUser.Username);
break;
case "Request Printable Report":
- RequestPrintableReport(factory, loggedInUser, loggerService);
+ RequestPrintableReport(factory, loggedInUser, loggerService, reportRequestsRepo);
break;
case "Return to Main Menu":
return;
@@ -57,7 +54,8 @@ private static void ShowEmployeeReport(Factory factory)
Pause();
}
- private static void RequestPrintableReport(Factory factory, Employee director, ILoggerService loggerService)
+ private static void RequestPrintableReport(Factory factory, Employee director, ILoggerService loggerService,
+ IJsonRepository reportRequestsRepo)
{
AnsiConsole.Clear();
AnsiConsole.Write(new Rule($"[magenta]{Reports.RequestPrintableReport}[/]").Centered());
@@ -67,90 +65,102 @@ private static void RequestPrintableReport(Factory factory, Employee director, I
.Title(Reports.SelectReportToRequest)
.AddChoices("Production Summary", "Employee Report", "Batch Revenue Summary", "Order Backlog Summary"));
- factory.AddReportRequest(new ReportRequest(reportType, director.Name));
+ var destination = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title(Reports.ChooseExportFolderPrompt)
+ .AddChoices(Reports.ExportDestDesktop, Reports.ExportDestReportsFolder));
+
+ var request = new ReportRequest(reportType, director.Name)
+ {
+ ExportDestination = destination == Reports.ExportDestDesktop ? "Desktop" : "ReportsFolder"
+ };
+
+ factory.AddReportRequest(request);
+ reportRequestsRepo.Save(factory.PendingReportRequests);
AnsiConsole.MarkupLine(string.Format(Reports.RequestSubmitted, reportType));
- loggerService.LogInfo(LogOrigin.USER, LogEvent.PrintableReportRequested,
- $"Report '{reportType}' requested by {director.Username}");
+ AnsiConsole.MarkupLine(string.Format(Reports.ExportDestSelected, destination));
+ loggerService.LogInfo(LogOrigin.User, LogEvent.PrintableReportRequested,
+ $"Report '{reportType}' (Target: {destination}) requested by {director.Username}");
Pause();
}
- private static void ShowProductionSummary(Factory factory, Employee loggedInUser)
+ internal static void ShowProductionSummary(Factory factory, Employee loggedInUser)
{
AnsiConsole.Clear();
- AnsiConsole.Write(new Rule("[magenta]Production Summary[/]").Centered());
+ AnsiConsole.Write(new Rule($"[magenta]{Reports.ProductionSummaryHeader}[/]").Centered());
- double inventoryValue = 0;
- foreach (var product in factory.Inventory.Where(p => !p.IsSold))
- {
- inventoryValue += product.SellingPrice * product.Quantity;
- }
+ var inventoryValue = factory.Inventory.Where(p => !p.IsSold).Sum(p => p.SellingPrice * p.Quantity);
+ var totalUnsoldUnits = GetTotalInventoryUnits(factory);
var grid = new Grid().AddColumns(2);
grid.AddRow(Reports.GeneratedBy, Markup.Escape(loggedInUser.Name));
grid.AddRow(Reports.Timestamp, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
grid.AddRow(Reports.InventoryItemTypes, factory.Inventory.Count(p => !p.IsSold).ToString());
- grid.AddRow(Reports.InventoryUnits, GetTotalInventoryUnits(factory).ToString());
+ grid.AddRow(Reports.InventoryUnits, totalUnsoldUnits.ToString());
grid.AddRow(Reports.EstInventoryValue, $"${inventoryValue:F2}");
grid.AddRow(Reports.Employees, factory.Employees.Count.ToString());
grid.AddRow(Reports.Machines, factory.Machines.Count.ToString());
AnsiConsole.Write(new Panel(grid).Header($"[bold]{Reports.SnapshotPanelHeader}[/]").Border(BoxBorder.Rounded));
- Pause();
- }
-
- private static void ShowBatchRevenueSummary(Factory factory)
- {
- AnsiConsole.Clear();
- AnsiConsole.Write(new Rule("[magenta]Batch Revenue Summary[/]").Centered());
-
- if (factory.BatchCount == 0)
- {
- AnsiConsole.MarkupLine(Reports.NoBatches);
- Pause();
- return;
- }
+ AnsiConsole.WriteLine();
+ // 1. Batches Table (Sold and Unsold)
+ AnsiConsole.MarkupLine(Reports.BatchesDetailsHeader);
var table = new Table().Border(TableBorder.Rounded);
- table.AddColumn("BatchId");
- table.AddColumn("Product");
- table.AddColumn("Qty");
- table.AddColumn("Cost");
- table.AddColumn("Sell Price");
- table.AddColumn("Status");
+ table.AddColumn(Reports.ColumnBatchId);
+ table.AddColumn(Reports.ColumnProduct);
+ table.AddColumn(Reports.ColumnQty);
+ table.AddColumn(Reports.ColumnProductionCostUnit);
+ table.AddColumn(Reports.ColumnSellPriceUnit);
+ table.AddColumn(Reports.ColumnStatus);
- double totalPotentialRevenue = 0;
- double totalCost = 0;
+ double totalExpenses = 0;
+ double totalIncome = 0;
- for (var i = 0; i < factory.BatchCount; i++)
+ foreach (var batch in factory.Batches)
{
- var batch = factory.Batches[i];
-
- totalCost += batch.TotalCost;
- if (batch.UnitSellPrice.HasValue) totalPotentialRevenue += batch.UnitSellPrice.Value * batch.Quantity;
+ totalExpenses += batch.TotalCost;
+ if (batch.IsSold && batch.UnitSellPrice.HasValue) totalIncome += batch.UnitSellPrice.Value * batch.Quantity;
table.AddRow(
batch.BatchId,
batch.ProductName,
batch.Quantity.ToString(),
$"${batch.UnitProductionCost:F2}",
- batch.UnitSellPrice?.ToString("F2") ?? "-",
- batch.IsSold ? "Sold" : batch.IsPriced ? "Priced" : "Open"
+ batch.IsSold ? $"${batch.UnitSellPrice ?? 0.0:F2}" : "$0.00",
+ batch.IsSold ? Reports.StatusSold : Reports.StatusProduced
);
}
- AnsiConsole.Write(table);
- AnsiConsole.Write(new Panel(new Markup(
- $"{Reports.TotalProductionCost} ${totalCost:F2}\n" +
- $"{Reports.PotentialRevenue} ${totalPotentialRevenue:F2}")
- ).Border(BoxBorder.Rounded).Header($"[bold]{Reports.BatchSummaryPanelHeader}[/]"));
+ if (factory.BatchCount > 0)
+ AnsiConsole.Write(table);
+ else
+ AnsiConsole.MarkupLine(Reports.NoBatchesProduced);
+
+ AnsiConsole.WriteLine();
+
+ // 2. Financial Summary
+ var totalProfit = totalIncome - totalExpenses;
+ var profitColor = totalProfit >= 0 ? "green" : "red";
+
+ var financeGrid = new Grid().AddColumns(2);
+ financeGrid.AddRow(Reports.FinanceExpensesLabel, $"[red]${totalExpenses:F2}[/]");
+ financeGrid.AddRow(Reports.FinanceIncomeLabel, $"[green]${totalIncome:F2}[/]");
+ financeGrid.AddRow(Reports.FinanceProfitLabel, $"[{profitColor}]${totalProfit:F2}[/]");
+
+ AnsiConsole.Write(new Panel(financeGrid)
+ .Header($"[bold yellow]{Reports.FinanceHeader}[/]")
+ .Border(BoxBorder.Rounded));
+ AnsiConsole.WriteLine();
+
Pause();
}
private static void ShowOrderBacklogSummary(Factory factory)
{
AnsiConsole.Clear();
- AnsiConsole.Write(new Rule("[magenta]Order Backlog Summary[/]").Centered());
+ AnsiConsole.Write(new Rule($"[magenta]{Reports.OrderBacklogSummaryHeader}[/]").Centered());
if (factory.OrderCount == 0)
{
@@ -160,12 +170,12 @@ private static void ShowOrderBacklogSummary(Factory factory)
}
var table = new Table().Border(TableBorder.Rounded);
- table.AddColumn("OrderId");
- table.AddColumn("Product");
- table.AddColumn("Qty");
- table.AddColumn("Completed");
- table.AddColumn("Assigned Tech");
- table.AddColumn("Status");
+ table.AddColumn(Reports.ColumnOrderId);
+ table.AddColumn(Reports.ColumnProduct);
+ table.AddColumn(Reports.ColumnQty);
+ table.AddColumn(Reports.ColumnCompleted);
+ table.AddColumn(Reports.ColumnAssignedTech);
+ table.AddColumn(Reports.ColumnStatus);
foreach (var order in factory.PendingOrders)
{
@@ -175,7 +185,7 @@ private static void ShowOrderBacklogSummary(Factory factory)
order.Quantity.ToString(),
order.CompletedCount.ToString(),
order.AssignedTechnicianId.ToString(),
- order.IsComplete ? "Complete" : "In Progress"
+ order.IsComplete ? Reports.StatusComplete : Reports.StatusInProgress
);
}
diff --git a/UI/SalesMenuHandler.cs b/UI/SalesMenuHandler.cs
index d50f68b..d35353b 100644
--- a/UI/SalesMenuHandler.cs
+++ b/UI/SalesMenuHandler.cs
@@ -5,7 +5,7 @@ namespace Smart_Factory_Management_System;
internal static class SalesMenuHandler
{
public static void Run(Factory factory, Employee loggedInUser, ILoggerService loggerService,
- IJsonRepository productRepo)
+ IJsonRepository productRepo, IJsonRepository ordersRepo)
{
if (loggedInUser is not SalesAgent && loggedInUser is not Director)
{
@@ -30,11 +30,11 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
switch (option)
{
case "Place Production Order":
- PlaceOrder(factory, loggerService, loggedInUser);
+ PlaceOrder(factory, loggerService, loggedInUser, ordersRepo);
break;
case "View Pending Orders":
ShowPendingOrders(factory);
- loggerService.LogInfo(LogOrigin.USER, LogEvent.PendingOrdersViewed, loggedInUser.Username);
+ loggerService.LogInfo(LogOrigin.User, LogEvent.PendingOrdersViewed, loggedInUser.Username);
break;
case "Record Sale for Batch":
RecordSale(factory, loggerService, loggedInUser, productRepo);
@@ -49,7 +49,8 @@ public static void Run(Factory factory, Employee loggedInUser, ILoggerService lo
}
}
- private static void PlaceOrder(Factory factory, ILoggerService loggerService, Employee loggedInUser)
+ private static void PlaceOrder(Factory factory, ILoggerService loggerService, Employee loggedInUser,
+ IJsonRepository ordersRepo)
{
AnsiConsole.WriteLine();
var productChoice = AnsiConsole.Prompt(
@@ -58,7 +59,15 @@ private static void PlaceOrder(Factory factory, ILoggerService loggerService, Em
.AddChoices(MenuOptions.ProductTypes));
var customName = AnsiConsole.Ask(Sales.EnterCustomProductName);
- var quantity = AnsiConsole.Ask(Sales.EnterQuantity);
+ var quantity = AnsiConsole.Prompt(
+ new TextPrompt(Sales.EnterQuantity)
+ .ValidationErrorMessage("[red]Quantity must be between 1 and 100 units per batch.[/]")
+ .Validate(q => q switch
+ {
+ <= 0 => ValidationResult.Error("[red]Quantity must be positive.[/]"),
+ > 100 => ValidationResult.Error("[red]Maximum quantity per batch is 100 units.[/]"),
+ _ => ValidationResult.Success()
+ }));
int? cores = null;
double? clockSpeed = null;
@@ -89,24 +98,14 @@ private static void PlaceOrder(Factory factory, ILoggerService loggerService, Em
PlacedBy = loggedInUser.Username
};
factory.AddOrder(order);
+ ordersRepo.Save(factory.PendingOrders);
AnsiConsole.MarkupLine(
string.Format(Sales.OrderPlacedOnHold, order.OrderId, order.CustomProductName, order.Quantity));
- loggerService.LogInfo(LogOrigin.USER, LogEvent.OrderPlaced,
+ loggerService.LogInfo(LogOrigin.User, LogEvent.OrderPlaced,
$"Order {order.OrderId}: {order.CustomProductName} x{order.Quantity} (Status: On Hold / Unassigned) by {loggedInUser.Username}");
}
- private static List GetTechnicians(Factory factory)
- {
- var technicians = new List();
-
- foreach (var t in factory.Employees)
- if (t is Technician)
- technicians.Add(t);
-
- return technicians;
- }
-
public static void ShowPendingOrders(Factory factory)
{
AnsiConsole.WriteLine();
@@ -150,16 +149,21 @@ private static void RecordSale(Factory factory, ILoggerService loggerService, Em
if (pickContext == "Sell from Batch")
{
- var selector = new SelectionPrompt().Title(Sales.SelectBatchToSell);
- for (var i = 0; i < factory.BatchCount; i++) selector.AddChoice(factory.Batches[i]);
-
- var chosen = AnsiConsole.Prompt(selector);
- if (chosen.IsSold)
+ var unsoldBatches = factory.Batches.Where(b => !b.IsSold).ToList();
+ if (!unsoldBatches.Any())
{
- AnsiConsole.MarkupLine(Sales.BatchAlreadySold);
+ AnsiConsole.MarkupLine(Sales.NoUnsoldBatchesToSell);
return;
}
+ var selector = new SelectionPrompt()
+ .Title(Sales.SelectBatchToSell)
+ .UseConverter(b =>
+ $"{b.BatchId} - {b.ProductName} (Qty: {b.Quantity}, Cost: ${b.UnitProductionCost:F2})")
+ .AddChoices(unsoldBatches);
+
+ var chosen = AnsiConsole.Prompt(selector);
+
var soldPrice = AnsiConsole.Ask(Sales.EnterUnitSoldPrice);
chosen.MarkAsSold();
chosen.SetUnitSellPrice(soldPrice);
@@ -176,7 +180,7 @@ private static void RecordSale(Factory factory, ILoggerService loggerService, Em
AnsiConsole.MarkupLine(
string.Format(Sales.SaleRecordedBatch, chosen.BatchId, soldPrice));
- loggerService.LogInfo(LogOrigin.USER, LogEvent.SaleRecorded,
+ loggerService.LogInfo(LogOrigin.User, LogEvent.SaleRecorded,
$"Batch {chosen.BatchId} sold at ${soldPrice:F2}/unit by {loggedInUser.Username}");
UndoService.Instance.RegisterCommand(new SellFromBatchCommand(chosen, factory, soldPrice, inventoryIndexes,
@@ -187,7 +191,7 @@ private static void RecordSale(Factory factory, ILoggerService loggerService, Em
var products = new List();
foreach (var p in factory.Inventory)
{
- if (p.Quantity > 0 && !p.IsSold)
+ if (p is { Quantity: > 0, IsSold: false })
products.Add(p);
}
@@ -242,6 +246,14 @@ private static void RecordSale(Factory factory, ILoggerService loggerService, Em
factory.AddProduct(soldProduct, soldProduct.BatchId);
+ // Find and deduct quantity from the original production batch
+ var originalBatch = factory.Batches.FirstOrDefault(b => b.BatchId == chosen.BatchId);
+ if (originalBatch != null)
+ {
+ originalBatch.Quantity -= qty;
+ if (originalBatch.Quantity <= 0) originalBatch.MarkAsSold();
+ }
+
var batch = new ProductionBatch(chosen.Name ?? "Inventory Sale", qty, chosen.ProductionCost);
batch.MarkAsSold();
batch.SetUnitSellPrice(soldPrice);
@@ -249,7 +261,7 @@ private static void RecordSale(Factory factory, ILoggerService loggerService, Em
AnsiConsole.MarkupLine(
string.Format(Sales.SaleRecordedInventory, qty, chosen.Name, soldPrice, batch.BatchId));
- loggerService.LogInfo(LogOrigin.USER, LogEvent.SaleRecorded,
+ loggerService.LogInfo(LogOrigin.User, LogEvent.SaleRecorded,
$"{qty} units of {chosen.Name} sold at ${soldPrice:F2}/unit by {loggedInUser.Username}");
UndoService.Instance.RegisterCommand(new SellFromInventoryCommand(chosen, qty, soldPrice, soldProduct,
diff --git a/UI/TUIHelpers.cs b/UI/TUIHelpers.cs
index 2d29109..575f680 100644
--- a/UI/TUIHelpers.cs
+++ b/UI/TUIHelpers.cs
@@ -4,7 +4,7 @@ namespace Smart_Factory_Management_System;
internal static class TuiHelper
{
- public static void RenderSessionHeader(Employee user, Factory factory)
+ public static void RenderSessionHeader(Employee user)
{
AnsiConsole.Clear();
var header = new Rule($"[blue]FACTORY CONTROL PANEL - Session: {user.Name}[/]").Centered();
diff --git a/UI/UndoMenuHandler.cs b/UI/UndoMenuHandler.cs
index d643d26..8d61cb9 100644
--- a/UI/UndoMenuHandler.cs
+++ b/UI/UndoMenuHandler.cs
@@ -24,7 +24,7 @@ public static void Run(Employee user, ILoggerService loggerService)
}
// Get the last action (newest command in the filtered list)
- var lastCommand = commands[commands.Count - 1];
+ var lastCommand = commands[^1];
AnsiConsole.MarkupLine(string.Format(UndoText.LastActionInfo, lastCommand.ExecutedAt,
Markup.Escape(lastCommand.ExecutedBy), Markup.Escape(lastCommand.Description)));
@@ -43,7 +43,7 @@ public static void Run(Employee user, ILoggerService loggerService)
try
{
UndoService.Instance.UndoCommand(lastCommand);
- loggerService.LogInfo(LogOrigin.UNDO, LogEvent.OperationUndone,
+ loggerService.LogInfo(LogOrigin.Undo, LogEvent.OperationUndone,
$"{lastCommand.Description} (Executed by {lastCommand.ExecutedBy})");
AnsiConsole.MarkupLine(UndoText.UndoSuccess);
}