Update Quantity
This commit is contained in:
parent
fce9d7b6c7
commit
ab79a16a6c
@ -13,6 +13,7 @@ namespace PSTW_CentralSystem.Areas.Inventory.Models
|
|||||||
public required string Category { get; set; }
|
public required string Category { get; set; }
|
||||||
public required string ModelNo { get; set; }
|
public required string ModelNo { get; set; }
|
||||||
public int? QuantityProduct { get; set; }
|
public int? QuantityProduct { get; set; }
|
||||||
|
public string? QuantityJSON { get; set; }
|
||||||
public required string ImageProduct { get; set; }
|
public required string ImageProduct { get; set; }
|
||||||
[ForeignKey("ManufacturerId")]
|
[ForeignKey("ManufacturerId")]
|
||||||
public virtual ManufacturerModel? Manufacturer { get; set; }
|
public virtual ManufacturerModel? Manufacturer { get; set; }
|
||||||
|
|||||||
@ -548,7 +548,8 @@
|
|||||||
ToOther: "Return",
|
ToOther: "Return",
|
||||||
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||||
Action: "StockIn",
|
Action: "StockIn",
|
||||||
Quantity: this.thisItem.quantity,
|
// Quantity: this.thisItem.quantity,
|
||||||
|
Quantity: this.thisItem.movementQuantity || 1,
|
||||||
Remark: this.remark,
|
Remark: this.remark,
|
||||||
ConsignmentNote: this.consignmentNote,
|
ConsignmentNote: this.consignmentNote,
|
||||||
Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
|
||||||
|
|||||||
@ -1,4 +1,435 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Dashboard";
|
ViewData["Title"] = "Report";
|
||||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
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>
|
||||||
|
}
|
||||||
@ -354,15 +354,15 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion Product
|
#endregion Product
|
||||||
|
|
||||||
#region Supplier
|
|
||||||
|
|
||||||
[HttpPost("SupplierList")]
|
[HttpPost("SupplierList")]
|
||||||
public async Task<IActionResult> SupplierList()
|
public async Task<IActionResult> SupplierList()
|
||||||
{
|
{
|
||||||
var supplierList = await _centralDbContext.Suppliers.ToListAsync();
|
var supplierList = await _centralDbContext.Suppliers.ToListAsync();
|
||||||
return Json(supplierList);
|
return Json(supplierList);
|
||||||
}
|
}
|
||||||
|
#region Supplier
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("AddSupplier")]
|
[HttpPost("AddSupplier")]
|
||||||
public async Task<IActionResult> AddSupplier([FromBody] SupplierModel supplier)
|
public async Task<IActionResult> AddSupplier([FromBody] SupplierModel supplier)
|
||||||
@ -873,6 +873,8 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
|
|||||||
item.ProductId,
|
item.ProductId,
|
||||||
item.SerialNumber,
|
item.SerialNumber,
|
||||||
item.Quantity,
|
item.Quantity,
|
||||||
|
StockQuantity = item.Quantity,
|
||||||
|
MovementQuantity = item.Movement?.Quantity ?? 0,
|
||||||
item.Supplier,
|
item.Supplier,
|
||||||
PurchaseDate = item.PurchaseDate.ToString("dd/MM/yyyy"),
|
PurchaseDate = item.PurchaseDate.ToString("dd/MM/yyyy"),
|
||||||
item.PONo,
|
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
|
// 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
|
// You'll need to fetch the Product to know if it's Disposable
|
||||||
var product = await _centralDbContext.Products.FindAsync(updateItem.ProductId);
|
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;
|
// Handle variable quantity for Disposables
|
||||||
if (updateItem.Quantity < 0)
|
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")]
|
[HttpPost("UpdateItemMovementMaster")]
|
||||||
public async Task<IActionResult> UpdateItemMovementMaster([FromBody] ItemMovementModel receiveMovement)
|
public async Task<IActionResult> UpdateItemMovementMaster([FromBody] ItemMovementModel receiveMovement)
|
||||||
{
|
{
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
var updatedList = await _centralDbContext.ItemMovements.FindAsync(receiveMovement.Id);
|
var updatedList = await _centralDbContext.ItemMovements.FindAsync(receiveMovement.Id);
|
||||||
|
|
||||||
if (updatedList == null)
|
if (updatedList == null)
|
||||||
@ -1120,20 +1127,38 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
|
|||||||
updatedList.Remark = receiveMovement.Remark;
|
updatedList.Remark = receiveMovement.Remark;
|
||||||
updatedList.MovementComplete = true;
|
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);
|
_centralDbContext.ItemMovements.Update(updatedList);
|
||||||
await _centralDbContext.SaveChangesAsync();
|
await _centralDbContext.SaveChangesAsync();
|
||||||
|
|
||||||
//var receiveItems = await _centralDbContext.Items.FindAsync(receiveMovement.ItemId);
|
return Ok(new
|
||||||
|
{
|
||||||
//if (receiveItems != null)
|
Success = true,
|
||||||
//{
|
Message = "Item received successfully",
|
||||||
// receiveItems.ItemStatus = 3;
|
Id = updatedList.Id,
|
||||||
// _centralDbContext.Items.Update(receiveItems);
|
LatestStatus = updatedList.LatestStatus
|
||||||
|
});
|
||||||
// await _centralDbContext.SaveChangesAsync(); // Simpan perubahan
|
|
||||||
//}
|
|
||||||
|
|
||||||
return Json(updatedList);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -1146,62 +1171,36 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Find the item
|
|
||||||
var item = await _centralDbContext.Items
|
var item = await _centralDbContext.Items
|
||||||
.Include(i => i.Product) // Include product for category check
|
.Include(i => i.Product)
|
||||||
.FirstOrDefaultAsync(i => i.ItemID == model.ItemId);
|
.FirstOrDefaultAsync(i => i.ItemID == model.ItemId);
|
||||||
|
|
||||||
if (item == null)
|
if (item == null) return NotFound("Item not found.");
|
||||||
{
|
|
||||||
return NotFound("Item not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process if it's a disposable item
|
// 1. Logic for Disposable (Variable)
|
||||||
if (item.Product?.Category == "Disposable")
|
if (item.Product?.Category == "Disposable")
|
||||||
{
|
{
|
||||||
// Get the original movement to find the exact quantity that was assigned
|
|
||||||
var originalMovement = await _centralDbContext.ItemMovements
|
var originalMovement = await _centralDbContext.ItemMovements
|
||||||
.FirstOrDefaultAsync(m => m.Id == model.MovementId);
|
.FirstOrDefaultAsync(m => m.Id == model.MovementId);
|
||||||
|
|
||||||
if (originalMovement == null)
|
if (originalMovement == null) return BadRequest("Original movement not found.");
|
||||||
{
|
|
||||||
return BadRequest("Original movement record not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The quantity to return is the original movement's quantity
|
item.Quantity += (originalMovement.Quantity ?? 1);
|
||||||
var quantityToReturn = originalMovement.Quantity ?? 1;
|
}
|
||||||
|
// 2. Logic for Part and Asset (Binary)
|
||||||
// Update the item quantity by adding back the assigned amount
|
else if (item.Product?.Category == "Part" || item.Product?.Category == "Asset")
|
||||||
item.Quantity += quantityToReturn;
|
{
|
||||||
|
item.Quantity = 1; // Mark as back in stock/available
|
||||||
// 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"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-disposable items, just return success without changing quantity
|
_centralDbContext.Items.Update(item);
|
||||||
return Ok(new
|
await _centralDbContext.SaveChangesAsync();
|
||||||
{
|
|
||||||
item.ItemID,
|
return Ok(new { item.ItemID, NewQuantity = item.Quantity });
|
||||||
Message = "No quantity change - item is not disposable"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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 findUniqueUser = _centralDbContext.Users.FirstOrDefault(r => r.Id == returnMovement.ToUser);
|
||||||
|
|
||||||
var bytes = Convert.FromBase64String(returnMovement.ConsignmentNote);
|
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());
|
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");
|
Directory.CreateDirectory(folderPath);
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await System.IO.File.WriteAllBytesAsync(filePath, bytes);
|
await System.IO.File.WriteAllBytesAsync(filePath, bytes);
|
||||||
|
|
||||||
|
returnMovement.ConsignmentNote = "/" + relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
_centralDbContext.ItemMovements.Add(returnMovement);
|
_centralDbContext.ItemMovements.Add(returnMovement);
|
||||||
await _centralDbContext.SaveChangesAsync();
|
await _centralDbContext.SaveChangesAsync();
|
||||||
|
|
||||||
var updateItemIdMovement = await _centralDbContext.ItemMovements
|
var updateItemIdMovement = await _centralDbContext.ItemMovements
|
||||||
.FirstOrDefaultAsync(m => m.Id == returnMovement.Id && m.MovementComplete == false);
|
.FirstOrDefaultAsync(m => m.Id == returnMovement.Id);
|
||||||
|
|
||||||
if (updateItemIdMovement != null)
|
if (updateItemIdMovement != null)
|
||||||
{
|
{
|
||||||
var returnItems = await _centralDbContext.Items.FindAsync(updateItemIdMovement.ItemId);
|
var returnItems = await _centralDbContext.Items.FindAsync(updateItemIdMovement.ItemId);
|
||||||
|
|
||||||
if (returnItems != null)
|
if (returnItems != null)
|
||||||
{
|
{
|
||||||
returnItems.MovementId = updateItemIdMovement.Id;
|
returnItems.MovementId = updateItemIdMovement.Id;
|
||||||
returnItems.ItemStatus = 2;
|
returnItems.ItemStatus = 2;
|
||||||
_centralDbContext.Items.Update(returnItems);
|
_centralDbContext.Items.Update(returnItems);
|
||||||
await _centralDbContext.SaveChangesAsync(); // Simpan perubahan
|
await _centralDbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -117,5 +117,135 @@ namespace PSTW_CentralSystem.Controllers.API
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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 |
Loading…
Reference in New Issue
Block a user