diff --git a/Areas/Inventory/Models/ProductModel.cs b/Areas/Inventory/Models/ProductModel.cs index cc889c5..3aa8bc1 100644 --- a/Areas/Inventory/Models/ProductModel.cs +++ b/Areas/Inventory/Models/ProductModel.cs @@ -13,6 +13,7 @@ namespace PSTW_CentralSystem.Areas.Inventory.Models public required string Category { get; set; } public required string ModelNo { get; set; } public int? QuantityProduct { get; set; } + public string? QuantityJSON { get; set; } public required string ImageProduct { get; set; } [ForeignKey("ManufacturerId")] public virtual ManufacturerModel? Manufacturer { get; set; } diff --git a/Areas/Inventory/Views/ItemMovement/QrUser.cshtml b/Areas/Inventory/Views/ItemMovement/QrUser.cshtml index 72bd12d..c874f73 100644 --- a/Areas/Inventory/Views/ItemMovement/QrUser.cshtml +++ b/Areas/Inventory/Views/ItemMovement/QrUser.cshtml @@ -548,7 +548,8 @@ ToOther: "Return", SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(), Action: "StockIn", - Quantity: this.thisItem.quantity, + // Quantity: this.thisItem.quantity, + Quantity: this.thisItem.movementQuantity || 1, Remark: this.remark, ConsignmentNote: this.consignmentNote, Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(), diff --git a/Areas/Report/Views/Reporting/InventoryReportManagement.cshtml b/Areas/Report/Views/Reporting/InventoryReportManagement.cshtml index a1641a2..a8574f0 100644 --- a/Areas/Report/Views/Reporting/InventoryReportManagement.cshtml +++ b/Areas/Report/Views/Reporting/InventoryReportManagement.cshtml @@ -1,4 +1,435 @@ @{ - ViewData["Title"] = "Dashboard"; + ViewData["Title"] = "Report"; Layout = "~/Views/Shared/_Layout.cshtml"; } +
+
+
+

Reporting Dashboard

+
+
+
+
+

Report Filter

+
+
+
+
+
+
+

Department

+ + +
+
+
+
+

Category

+ + +
+
+
+
+
+

Date Filter

+
+ + +
+
+ + + +
+
+
+
+
+
+
+
+
+

Inventory Report

+ +
+ +
+
+
+ @* Overview *@ +
+
+
+
+
Total Items Registered
+

{{ reportData.itemCountRegistered }}

+
+
+
+
+
+
+
Items Still In Stock
+

{{ reportData.itemCountStillInStock }}

+
+
+
+
+ + @* Stock Receive *@ +
+
Stock Receive Listing
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No.DateDoc No.VendorStock IDDescriptionQuantityForeign CurrencyExchange RateRMTotal (RM)
{{ index + 1 }}{{ item.purchaseDate }}{{ item.invoiceNo }}{{ item.supplier }}{{ item.uniqueID }}{{ item.productName }} ({{ item.category }}){{ item.quantity }}{{ item.currency }}{{ item.currencyRate }}{{ item.convertPrice.toFixed(2) }}{{ (item.quantity * item.convertPrice).toFixed(2) }}
+
+ + @* Stock Issue *@ +
+
Stock Issue Listing
+
Month: -----
+ + + + + + + + + + + + + + + + + + + + + + + + + +
No.Stock IDDescriptionQtyUnit Price (RM)Total (RM)StationRequestor
{{ index + 1 }}{{ m.uniqueID }}{{ m.description }}{{ m.quantity }}{{ m.unitPrice.toFixed(2) }}{{ (m.quantity * m.unitPrice).toFixed(2) }}{{ m.stationName }}{{ m.requestorName }}
+
+ + @* Stock Card *@ +
+
Stock Card
+
Stock ID: ------
+
Stock Description: ---------
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LocationDateDoc No.B/F QtyInOutBalanceUnit PriceTotal CostBalance Cost
HQ{{ item.invoiceDate }}{{ item.invoiceNo }}
+
+
+ + @* Stock Balance Report *@ +
+
Stock Balance Report For The Month of {{ reportData.selectedPeriodLabel }}
+
+ + + + + + + + + + + + + + + +
Stock IDDescriptionB/F QtyInOutBalanceCost Balance (RM)
+
+
+
+
+
+
+
+@section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } + +} \ No newline at end of file diff --git a/Controllers/API/Inventory/InvMainAPI.cs b/Controllers/API/Inventory/InvMainAPI.cs index d225655..f50b736 100644 --- a/Controllers/API/Inventory/InvMainAPI.cs +++ b/Controllers/API/Inventory/InvMainAPI.cs @@ -354,15 +354,15 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory } #endregion Product - - #region Supplier - [HttpPost("SupplierList")] public async Task SupplierList() { var supplierList = await _centralDbContext.Suppliers.ToListAsync(); return Json(supplierList); } + #region Supplier + + [HttpPost("AddSupplier")] public async Task AddSupplier([FromBody] SupplierModel supplier) @@ -873,6 +873,8 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory item.ProductId, item.SerialNumber, item.Quantity, + StockQuantity = item.Quantity, + MovementQuantity = item.Movement?.Quantity ?? 0, item.Supplier, PurchaseDate = item.PurchaseDate.ToString("dd/MM/yyyy"), item.PONo, @@ -1054,12 +1056,19 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory // This is crucial: if it's a disposable item, decrement the Item's Quantity // You'll need to fetch the Product to know if it's Disposable var product = await _centralDbContext.Products.FindAsync(updateItem.ProductId); - if (product != null && product.Category == "Disposable" && itemmovement.Quantity.HasValue) + + if (product != null) { - updateItem.Quantity -= itemmovement.Quantity.Value; - if (updateItem.Quantity < 0) + // Handle variable quantity for Disposables + if (product.Category == "Disposable" && itemmovement.Quantity.HasValue) { - updateItem.Quantity = 0; // Prevent negative quantity + updateItem.Quantity -= itemmovement.Quantity.Value; + if (updateItem.Quantity < 0) updateItem.Quantity = 0; + } + // Handle binary quantity for Parts and Assets + else if (product.Category == "Part" || product.Category == "Asset") + { + updateItem.Quantity = 0; // The unique item is now unavailable } } @@ -1103,10 +1112,8 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory [HttpPost("UpdateItemMovementMaster")] public async Task UpdateItemMovementMaster([FromBody] ItemMovementModel receiveMovement) { - try { - var updatedList = await _centralDbContext.ItemMovements.FindAsync(receiveMovement.Id); if (updatedList == null) @@ -1120,20 +1127,38 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory updatedList.Remark = receiveMovement.Remark; updatedList.MovementComplete = true; + var item = await _centralDbContext.Items + .Include(i => i.Product) + .FirstOrDefaultAsync(i => i.ItemID == updatedList.ItemId); + + if (item != null) + { + if (updatedList.ToOther == "Return" || receiveMovement.LatestStatus == "Ready To Deploy") + { + if (item.Product?.Category == "Disposable") + { + // from movement + item.Quantity += (updatedList.Quantity ?? 0); + } + else + { + item.Quantity = 1; + } + item.ItemStatus = 1; + _centralDbContext.Items.Update(item); + } + } + _centralDbContext.ItemMovements.Update(updatedList); await _centralDbContext.SaveChangesAsync(); - //var receiveItems = await _centralDbContext.Items.FindAsync(receiveMovement.ItemId); - - //if (receiveItems != null) - //{ - // receiveItems.ItemStatus = 3; - // _centralDbContext.Items.Update(receiveItems); - - // await _centralDbContext.SaveChangesAsync(); // Simpan perubahan - //} - - return Json(updatedList); + return Ok(new + { + Success = true, + Message = "Item received successfully", + Id = updatedList.Id, + LatestStatus = updatedList.LatestStatus + }); } catch (Exception ex) { @@ -1146,62 +1171,36 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory { try { - // Find the item var item = await _centralDbContext.Items - .Include(i => i.Product) // Include product for category check + .Include(i => i.Product) .FirstOrDefaultAsync(i => i.ItemID == model.ItemId); - if (item == null) - { - return NotFound("Item not found."); - } + if (item == null) return NotFound("Item not found."); - // Only process if it's a disposable item + // 1. Logic for Disposable (Variable) if (item.Product?.Category == "Disposable") { - // Get the original movement to find the exact quantity that was assigned var originalMovement = await _centralDbContext.ItemMovements .FirstOrDefaultAsync(m => m.Id == model.MovementId); - if (originalMovement == null) - { - return BadRequest("Original movement record not found."); - } + if (originalMovement == null) return BadRequest("Original movement not found."); - // The quantity to return is the original movement's quantity - var quantityToReturn = originalMovement.Quantity ?? 1; - - // Update the item quantity by adding back the assigned amount - item.Quantity += quantityToReturn; - - // Ensure quantity doesn't go negative (just in case) - if (item.Quantity < 0) - { - item.Quantity = 0; - } - - _centralDbContext.Items.Update(item); - await _centralDbContext.SaveChangesAsync(); - - return Ok(new - { - item.ItemID, - OriginalQuantity = originalMovement.Quantity, - NewQuantity = item.Quantity, - Message = $"Successfully returned {quantityToReturn} to item quantity" - }); + item.Quantity += (originalMovement.Quantity ?? 1); + } + // 2. Logic for Part and Asset (Binary) + else if (item.Product?.Category == "Part" || item.Product?.Category == "Asset") + { + item.Quantity = 1; // Mark as back in stock/available } - // For non-disposable items, just return success without changing quantity - return Ok(new - { - item.ItemID, - Message = "No quantity change - item is not disposable" - }); + _centralDbContext.Items.Update(item); + await _centralDbContext.SaveChangesAsync(); + + return Ok(new { item.ItemID, NewQuantity = item.Quantity }); } catch (Exception ex) { - return BadRequest($"Error updating item quantity: {ex.Message}"); + return BadRequest(ex.Message); } } @@ -1844,45 +1843,44 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory var findUniqueUser = _centralDbContext.Users.FirstOrDefault(r => r.Id == returnMovement.ToUser); var bytes = Convert.FromBase64String(returnMovement.ConsignmentNote); - string filePath = ""; + + string safeUserName = string.Join("_", (findUniqueUser?.FullName ?? "Unknown").Split(Path.GetInvalidFileNameChars())); + string safeModelNo = string.Join("_", (findUniqueCode?.Product?.ModelNo ?? "NA").Split(Path.GetInvalidFileNameChars())); var uniqueAbjad = new string(Enumerable.Range(0, 8).Select(_ => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[new Random().Next(36)]).ToArray()); + string extension = IsPdf(bytes) ? ".pdf" : ".jpg"; + if (!IsImage(bytes) && !IsPdf(bytes)) return BadRequest("Unsupported file format."); - if (IsImage(bytes)) + string relativePath = $"media/inventory/itemmovement/{safeUserName}_{safeModelNo}_{uniqueAbjad}_Return{extension}"; + string folderPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "media", "inventory", "itemmovement"); + string filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", relativePath); + + if (!Directory.Exists(folderPath)) { - filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/media/inventory/itemmovement", findUniqueUser.FullName + " " + findUniqueCode.Product?.ModelNo + "(" + uniqueAbjad + ") Return.jpg"); - returnMovement.ConsignmentNote = "/media/inventory/itemmovement/" + findUniqueUser.FullName + " " + findUniqueCode.Product?.ModelNo + "(" + uniqueAbjad + ") Return.jpg"; - } - else if (IsPdf(bytes)) - { - filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/media/inventory/itemmovement", findUniqueUser.FullName + " " + findUniqueCode.Product?.ModelNo + "Return.pdf"); - returnMovement.ConsignmentNote = "/media/inventory/itemmovement/" + findUniqueUser.FullName + " " + findUniqueCode.Product?.ModelNo + "(" + uniqueAbjad + ") Return.pdf"; - } - else - { - return BadRequest("Unsupported file format."); + Directory.CreateDirectory(folderPath); } await System.IO.File.WriteAllBytesAsync(filePath, bytes); + + returnMovement.ConsignmentNote = "/" + relativePath; } _centralDbContext.ItemMovements.Add(returnMovement); await _centralDbContext.SaveChangesAsync(); var updateItemIdMovement = await _centralDbContext.ItemMovements - .FirstOrDefaultAsync(m => m.Id == returnMovement.Id && m.MovementComplete == false); + .FirstOrDefaultAsync(m => m.Id == returnMovement.Id); if (updateItemIdMovement != null) { var returnItems = await _centralDbContext.Items.FindAsync(updateItemIdMovement.ItemId); - if (returnItems != null) { returnItems.MovementId = updateItemIdMovement.Id; returnItems.ItemStatus = 2; _centralDbContext.Items.Update(returnItems); - await _centralDbContext.SaveChangesAsync(); // Simpan perubahan + await _centralDbContext.SaveChangesAsync(); } } diff --git a/Controllers/API/ReportingAPI.cs b/Controllers/API/ReportingAPI.cs index dea8cd6..1f70369 100644 --- a/Controllers/API/ReportingAPI.cs +++ b/Controllers/API/ReportingAPI.cs @@ -117,5 +117,135 @@ namespace PSTW_CentralSystem.Controllers.API } #endregion + + #region Report Inventory ii + [HttpPost("GetInventoryReport")] + public async Task GetInventoryReport([FromBody] ReportFilterDTO filter) + { + try + { + int? deptId = filter.DeptId; + + // 1. Setup the Date Range + DateTime start = new DateTime(2000, 1, 1); + DateTime end = DateTime.Now.AddDays(1); + + if (filter.IsMonthMode && filter.Month.HasValue && filter.Year.HasValue) + { + start = new DateTime(filter.Year.Value, filter.Month.Value, 1); + end = start.AddMonths(1).AddDays(-1).AddHours(23).AddMinutes(59); + } + else if (filter.StartDate.HasValue && filter.EndDate.HasValue) + { + start = filter.StartDate.Value; + end = filter.EndDate.Value.AddHours(23).AddMinutes(59); + } + + // 2. Fetch Movements DURING the range (Required for Stock Issue logic) + var movementsInRange = await _centralDbContext.ItemMovements + .Include(m => m.Item).ThenInclude(i => i.Product) + .Include(m => m.FromStation).Include(m => m.FromUser) + .Include(m => m.NextStation).Include(m => m.NextUser) + .Include(m => m.NextStore) + .Where(m => m.Date >= start && m.Date <= end) + .OrderBy(m => m.Date) + .ToListAsync(); + + var totalGlobalProductQuantity = await _centralDbContext.Products.SumAsync(p => p.QuantityProduct ?? 0); + + // 3. Filter Items based on Department + IQueryable itemQuery = _centralDbContext.Items + .Include(i => i.CreatedBy) + .Include(i => i.Department) + .Include(i => i.Product); + + if (deptId != null && deptId != 0) + { + itemQuery = itemQuery.Where(i => i.DepartmentId == deptId); + } + var items = await itemQuery.ToListAsync(); + + // 4. Generate Issued Items List + var latestMovementsPerItem = movementsInRange + .Where(m => m.ItemId.HasValue) + .GroupBy(m => m.ItemId) + .Select(g => g.OrderByDescending(m => m.Date).ThenByDescending(m => m.Id).First()) + .ToList(); + + var issuedItemsList = latestMovementsPerItem.Where(m => + { + string? status = m.LatestStatus; + string? action = m.Action; + string? other = m.ToOther; + + if (string.IsNullOrEmpty(status)) + { + if (other == "Faulty") return false; + return (action == "Stock Out" || other == "Return"); + } + + if (status == "Ready to deploy" || status == "Cancelled") return false; + + if (status == "Delivered") + { + return (action == "Stock Out" || action == "Assign"); + } + + return false; + }) + .Select(m => new { + uniqueID = m.Item?.UniqueID, + description = m.Item?.Product != null ? $"{m.Item.Product.ProductName} ({m.Item.Product.Category})" : "N/A", + quantity = m.Quantity ?? 0, + unitPrice = m.Item?.ConvertPrice ?? 0, + stationName = m.NextStation?.StationName ?? "N/A", + requestorName = m.NextUser?.UserName ?? "N/A", + action = m.Action, + status = m.LatestStatus ?? "N/A", + other = m.ToOther ?? "" + }).ToList(); + + // 5. Final Output (Removed balanceReport and stockCardLogs) + var finalReport = new + { + itemCountRegistered = totalGlobalProductQuantity, + itemCountStillInStock = items.Sum(i => i.Quantity), + receivedItems = items.Where(i => i.PurchaseDate >= start && i.PurchaseDate <= end).Select(i => new { + i.ItemID, + i.UniqueID, + PurchaseDate = i.PurchaseDate.ToString("dd/MM/yyyy"), + i.InvoiceNo, + // Add this line below to get the InvoiceDate formatted + InvoiceDate = i.InvoiceDate.HasValue ? i.InvoiceDate.Value.ToString("dd/MM/yyyy") : "N/A", + i.Supplier, + ProductName = i.Product?.ProductName, + Category = i.Product?.Category, + i.Quantity, + i.Currency, + i.CurrencyRate, + i.ConvertPrice + }).ToList(), + issuedItems = issuedItemsList, + selectedPeriodLabel = (filter.Month == null && filter.StartDate == null) ? "All Time" : (filter.IsMonthMode ? $"{filter.Month}/{filter.Year}" : $"{start:dd/MM/yyyy} - {end:dd/MM/yyyy}") + }; + + return Json(finalReport); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + public class ReportFilterDTO + { + public int? DeptId { get; set; } + public bool IsMonthMode { get; set; } + public int? Month { get; set; } + public int? Year { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + } + #endregion + } } diff --git a/wwwroot/Media/Inventory/Images/ABC123.jpg b/wwwroot/Media/Inventory/Images/ABC123.jpg deleted file mode 100644 index 459f10a..0000000 Binary files a/wwwroot/Media/Inventory/Images/ABC123.jpg and /dev/null differ diff --git a/wwwroot/Media/Inventory/itemmovement/1481_525e2c4f-c1cb-477c-af28-bd50b8b02df51481_Request.jpg b/wwwroot/Media/Inventory/itemmovement/1481_525e2c4f-c1cb-477c-af28-bd50b8b02df51481_Request.jpg deleted file mode 100644 index 459f10a..0000000 Binary files a/wwwroot/Media/Inventory/itemmovement/1481_525e2c4f-c1cb-477c-af28-bd50b8b02df51481_Request.jpg and /dev/null differ diff --git a/wwwroot/Media/Inventory/itemmovement/1481_89c5731a-e3f8-4bfb-8535-b6cc621791911481_Request.jpg b/wwwroot/Media/Inventory/itemmovement/1481_89c5731a-e3f8-4bfb-8535-b6cc621791911481_Request.jpg deleted file mode 100644 index 459f10a..0000000 Binary files a/wwwroot/Media/Inventory/itemmovement/1481_89c5731a-e3f8-4bfb-8535-b6cc621791911481_Request.jpg and /dev/null differ diff --git a/wwwroot/Media/Inventory/itemmovement/hilmi.rezuan Latitude 7410(UN9DWPFT) Station.jpg b/wwwroot/Media/Inventory/itemmovement/hilmi.rezuan Latitude 7410(UN9DWPFT) Station.jpg deleted file mode 100644 index 2e6c3a7..0000000 Binary files a/wwwroot/Media/Inventory/itemmovement/hilmi.rezuan Latitude 7410(UN9DWPFT) Station.jpg and /dev/null differ