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); }