Update Quantity

This commit is contained in:
Naz 2026-01-29 17:25:20 +08:00
parent fce9d7b6c7
commit ab79a16a6c
9 changed files with 640 additions and 79 deletions

View File

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

View File

@ -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(),

View File

@ -1,4 +1,435 @@
@{
ViewData["Title"] = "Dashboard";
ViewData["Title"] = "Report";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="container" id="invAdmin">
<div class="row">
<div class="text-center">
<p><h1 class="display-4">Reporting Dashboard</h1></p>
</div>
</div>
<div class="row card">
<div class="card-header">
<h3 class="card-title">Report Filter</h3>
</div>
<div class="card-body">
<div v-if="reportData">
<div class="row justify-content-center">
<div class="col-3">
<div class="row col-10">
<h4>Department</h4>
<multiselect v-model="selectedDepartment" :options="compDeptList" :multiple="true" group-values="departments" group-label="companyName"
:group-select="true" placeholder="Seach Department" track-by="departmentId" label="departmentName">
</multiselect>
</div>
</div>
<div class="col-3">
<div class="row col-10">
<h4>Category</h4>
<multiselect v-model="selectedCategory" :options="categoryList" :multiple="true" placeholder="Seach Category">
</multiselect>
</div>
</div>
<div class="col-4">
<div class="row col-11">
<div class="d-flex justify-content-between align-items-center">
<h4>Date Filter</h4>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" v-model="isMonthMode" id="modeToggle">
<label class="form-check-label" for="modeToggle" style="font-size: 0.8rem;">Month Mode</label>
</div>
</div>
<vue-date-picker v-model="selectedDate"
:range="!isMonthMode"
:month-picker="isMonthMode"
placeholder="Select Date or Month Range"
:teleport="true"
v-on:update:model-value="onDateChange">
</vue-date-picker>
</div>
</div>
<div class=""><button class="btn btn-danger" v-on:click="clearFilters">Clear Filter</button></div>
</div>
</div>
</div>
</div>
<div class="row card">
<div class="card-header">
<h3 class="card-title">Inventory Report</h3>
<ul class="nav nav-tabs card-header-tabs" id="reportTabs" role="tablist">
<li class="nav-item">
<button class="nav-link" :class="{ active: activeTab === 'overview' }" v-on:click="activeTab = 'overview'">Overview</button>
</li>
<li class="nav-item">
<button class="nav-link" :class="{ active: activeTab === 'stockReceive' }" v-on:click="activeTab = 'stockReceive'">Stock Receive</button>
</li>
<li class="nav-item">
<button class="nav-link" :class="{ active: activeTab === 'stockIssue' }" v-on:click="activeTab = 'stockIssue'">Stock Issue</button>
</li>
<li class="nav-item">
<button class="nav-link" :class="{ active: activeTab === 'stockCard' }" v-on:click="activeTab = 'stockCard'">Stock Card</button>
</li>
<li class="nav-item">
<button class="nav-link" :class="{ active: activeTab === 'stockBalanceReport' }" v-on:click="activeTab = 'stockBalanceReport'">Stock Balance Report</button>
</li>
</ul>
</div>
<div class="card-body">
<div v-if="reportData">
<div class="tab-content">
@* Overview *@
<div v-if="activeTab === 'overview'" class="row">
<div class="col-md-3">
<div class="card bg-light">
<div class="card-body text-center">
<h6>Total Items Registered</h6>
<h3>{{ reportData.itemCountRegistered }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h6>Items Still In Stock</h6>
<h3>{{ reportData.itemCountStillInStock }}</h3>
</div>
</div>
</div>
</div>
@* Stock Receive *@
<div v-if="activeTab === 'stockReceive'">
<h5>Stock Receive Listing</h5>
<table class="table table-sm table-bordered">
<thead class="table-primary">
<tr>
<th>No.</th>
<th>Date</th>
<th>Doc No.</th>
<th>Vendor</th>
<th>Stock ID</th>
<th>Description</th>
<th>Quantity</th>
<th>Foreign Currency</th>
<th>Exchange Rate</th>
<th>RM</th>
<th>Total (RM)</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in reportData.receivedItems" :key="item.uniqueID">
<td>{{ index + 1 }}</td>
<td>{{ item.purchaseDate }}</td>
<td>{{ item.invoiceNo }}</td>
<td>{{ item.supplier }}</td>
<td>{{ item.uniqueID }}</td>
<td>{{ item.productName }} ({{ item.category }})</td>
<td>{{ item.quantity }}</td>
<td>{{ item.currency }}</td>
<td>{{ item.currencyRate }}</td>
<td>{{ item.convertPrice.toFixed(2) }}</td>
<td>{{ (item.quantity * item.convertPrice).toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
@* Stock Issue *@
<div v-if="activeTab === 'stockIssue'">
<h5>Stock Issue Listing</h5>
<h5>Month: ----- </h5>
<table class="table table-sm table-bordered">
<thead class="table-primary">
<tr>
<th>No.</th>
<th>Stock ID</th>
<th>Description</th>
<th>Qty</th>
<th>Unit Price (RM)</th>
<th>Total (RM)</th>
<th>Station</th>
<th>Requestor</th>
</tr>
</thead>
<tbody>
<tr v-for="(m, index) in reportData.issuedItems" :key="index">
<td>{{ index + 1 }}</td>
<td>{{ m.uniqueID }}</td>
<td>{{ m.description }}</td>
<td>{{ m.quantity }}</td>
<td>{{ m.unitPrice.toFixed(2) }}</td>
<td>{{ (m.quantity * m.unitPrice).toFixed(2) }}</td>
<td>{{ m.stationName }}</td>
<td>{{ m.requestorName }}</td>
</tr>
</tbody>
</table>
</div>
@* Stock Card *@
<div v-if="activeTab === 'stockCard'">
<h5>Stock Card</h5>
<h5>Stock ID: ------</h5>
<h5>Stock Description: ---------</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead>
<tr class="table-secondary">
<th>Location</th>
<th>Date</th>
<th>Doc No.</th>
<th>B/F Qty</th>
<th>In</th>
<th>Out</th>
<th>Balance</th>
<th>Unit Price</th>
<th>Total Cost</th>
<th>Balance Cost</th>
</tr>
</thead>
<tbody>
<tr v-for="item in reportData.receivedItems" :key="item.itemID">
<td>HQ</td>
<td>{{ item.invoiceDate }}</td>
<td>{{ item.invoiceNo }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
@* Stock Balance Report *@
<div v-if="activeTab === 'stockBalanceReport'">
<h5>Stock Balance Report For The Month of {{ reportData.selectedPeriodLabel }}</h5>
<div class="table-responsive">
<table class="table table-sm table-bordered table-striped">
<thead class="table-primary">
<tr>
<th>Stock ID</th>
<th>Description</th>
<th>B/F Qty</th>
<th>In</th>
<th>Out</th>
<th>Balance</th>
<th>Cost Balance (RM)</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$(function () {
app.mount('#invAdmin');
$('.closeModal').on('click', function () {
// Show the modal with the ID 'addManufacturerModal'.
$('.modal').modal('hide');
});
});
const app = Vue.createApp({
components: {
'multiselect': window.VueMultiselect.default,
VueDatePicker,
},
data() {
return {
activeTab: 'overview',
currentUser: null,
currentUserCompanyDept: {
departmentName: null,
departmentId: null
},
reportData: null,
compDeptList: {},
productList: {},
categoryList:['Asset', 'Part', 'Disposable'],
monthList: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
isMonthMode: false, // Tracks if we are picking a month or a range
selectedDate: null, // Holds the date value
filteredProduct: [],
selectedMonth: [],
selectedDepartment: [],
selectedItem: [],
selectedCategory: []
}
},
mounted() {
this.fetchUser();
this.fetchProductList();
this.fetchDepartmentsCompaniesList();
},
watch: {
isMonthMode() {
this.selectedDate = null; // Reset date when switching modes
},
//watch selectedDepartment. when selectedDepartment is changed, if selectedCategory is null filter productList based on selectedDepartment only. otherwise filter the productList based on selectedDepartment and selectedCategory
selectedDepartment() {
this.filterProducts();
},
selectedCategory() {
this.filterProducts();
}
},
methods: {
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.currentUser = data?.userInfo || null;
const companyDeptData = await this.currentUser.department;
const userRole = this.currentUser.role;
if(userRole == "SuperAdmin" || userRole == "SystemAdmin"){
this.currentUserCompanyDept = {departmentId : 0, departmentName : "All"}
this.fetchInventoryReport(0);
}
else{
this.currentUserCompanyDept = companyDeptData;
this.fetchInventoryReport(0);
}
}
else {
console.error(`Failed to fetch user: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
onDateChange() {
// Trigger report fetch whenever date changes
const deptId = this.selectedDepartment.length > 0
? this.selectedDepartment[0].departmentId
: (this.currentUserCompanyDept?.departmentId || 0);
this.fetchInventoryReport(deptId);
},
async fetchInventoryReport(deptId) {
try {
// Ensure we have a valid deptId even if the filter is empty
const id = (deptId !== undefined) ? deptId : 0;
const filters = {
deptId: id,
isMonthMode: this.isMonthMode,
// Only send date details if selectedDate is not null
month: this.selectedDate?.month !== undefined ? this.selectedDate.month + 1 : null,
year: this.selectedDate?.year || null,
startDate: !this.isMonthMode && this.selectedDate ? this.selectedDate[0] : null,
endDate: !this.isMonthMode && this.selectedDate ? this.selectedDate[1] : null
};
const response = await fetch(`/ReportingAPI/GetInventoryReport`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(filters)
});
if (response.ok) {
this.reportData = await response.json();
}
} catch (error) {
console.error('Fetch Error:', error);
}
},
async fetchDepartmentsCompaniesList(){
try {
const response = await fetch(`/AdminAPI/GetDepartmentWithCompanyList/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.compDeptList = data;
}
else {
console.error(`Failed to fetch company & department list: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
async fetchProductList(){
try {
const response = await fetch(`/InvMainAPI/ItemList/`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
this.productList = data;
}
else {
console.error(`Failed to fetch item list: ${response.statusText}`);
}
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
},
filterProducts() {
const selectedDepartmentIds = this.selectedDepartment.map(department => department.departmentId);
const selectedCategory = this.selectedCategory;
if (selectedDepartmentIds.length === 0 && selectedCategory.length === 0) {
// No filters applied
this.filteredProduct = this.productList;
}
else if (selectedDepartmentIds.length === 0) {
// Filter by category only
this.filteredProduct = this.productList.filter(product =>
selectedCategory.includes(product.category)
);
}
else if (selectedCategory.length === 0) {
// Filter by department only
this.filteredProduct = this.productList.filter(product =>
selectedDepartmentIds.includes(product.departmentId)
);
}
else {
// Filter by both department and category
this.filteredProduct = this.productList.filter(product =>
selectedDepartmentIds.includes(product.departmentId) &&
selectedCategory.includes(product.category)
);
}
},
clearFilters() {
this.selectedDepartment = [];
this.selectedCategory = [];
this.selectedDate = null;
this.isMonthMode = false;
// Trigger fetch with ID 0 (All) and no dates
this.fetchInventoryReport(0);
},
},
});
</script>
}

View File

@ -354,15 +354,15 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
}
#endregion Product
#region Supplier
[HttpPost("SupplierList")]
public async Task<IActionResult> SupplierList()
{
var supplierList = await _centralDbContext.Suppliers.ToListAsync();
return Json(supplierList);
}
#region Supplier
[HttpPost("AddSupplier")]
public async Task<IActionResult> 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<IActionResult> 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();
}
}

View File

@ -117,5 +117,135 @@ namespace PSTW_CentralSystem.Controllers.API
}
#endregion
#region Report Inventory ii
[HttpPost("GetInventoryReport")]
public async Task<IActionResult> 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<ItemModel> 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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB