Update Inv Master > Inv Master

This commit is contained in:
Naz 2026-03-19 11:23:24 +08:00
parent 81013fa710
commit 6535050179
3 changed files with 421 additions and 322 deletions

View File

@ -273,28 +273,36 @@
</div>
</div>
@* Inv Master Return Item & Deploy to Station*@
@* Inv Master Return Item & Assign*@
<div v-if="itemlateststatus == 'Delivered' && (this.thisItem.toUser == this.currentUser.id || this.thisItem.toStore == this.currentUser.store || this.isStationPIC )">
<h2 class="text-center register-heading">Item Actions</h2>
<div class="col-sm-12 d-flex justify-content-center mt-3">
<button type="button" v-on:click="ReturnMessage" class="btn btn-warning m-2" style="width: 200px; padding: 10px;">
<i class="fas fa-undo me-2"></i>Return Item
</button>
<button type="button" v-on:click="StationMessage" class="btn btn-primary m-2" style="width: 200px; padding: 10px;">
@* <button type="button" v-on:click="StationMessage" class="btn btn-primary m-2" style="width: 200px; padding: 10px;">
<i class="fas fa-broadcast-tower me-2"></i>
{{ (thisItem?.currentStationId || thisItem?.toStationId) ? "Change Station" : "Deploy To Station" }}
</button>
</button> *@
</div>
<h2 class="register-heading mt-3 text-center">Add Item Movement</h2>
<div class="col-sm-6 offset-sm-3">
<div class="dropdown">
<select class="btn btn-primary dropdown-toggle col-md-10 " v-model="selectedAction" required>
<option class="btn-light" value="" disabled selected>Select Action</option>
<option class="btn-light" value="user">Assign to User</option>
<option class="btn-light" value="station">Assign to Station</option>
</select>
</div>
</div>
</div>
<div v-if="itemlateststatus == 'Return' && this.itemassignedtouser">
<div v-if="itemlateststatus == 'Return' && thisItem.toStore == currentUser.store">
<h2 class="register-heading">Receive Item Return</h2>
<div class="col-sm-3"></div>
<div class="col-sm-6 offset-sm-3">
<form v-on:submit.prevent="receiveItemMovement" data-aos="fade-right">
<div class="row register-form">
<div style="display: flex; justify-content: center; margin-top: 20px;">
<button type="submit" class="btn btn-primary" style="width: 200px; padding: 10px; font-size: 16px;">
Receive
@ -807,8 +815,8 @@
storelist: null,
supplierlist: null,
selectedUser: "",
selectedStation: "",
selectedStationPIC: null,
// selectedStation: "",
// selectedStationPIC: null,
selectedStore: "",
selectedAction: "",
assigndate: null,
@ -863,33 +871,31 @@
this.trackFunctionSelected.value = this.paintOutline;
},
watch: {
selectedStation(newStationId) {
// Find the station object that matches the selectedStation id
const selectedStationObj = this.stationlist.find(station => station.stationId === newStationId);
// Set the selectedPIC based on the stationPIC
this.selectedStationPIC = selectedStationObj ? selectedStationObj.stationPicID : "";
// this.selectedStationPIC = selectedStationObj ? selectedStationObj : null;
},
// Watch for changes in `thisItem` to reset `quantity` when a new item is scanned
thisItem: {
handler(newItem) {
if (newItem && newItem.category === 'Disposable') {
this.quantity = 1; // Reset to 1 or default quantity when a disposable item is scanned
this.maxQuantity = newItem.quantity; // Set maxQuantity for disposable items
this.quantity = 1;
// 🌟 HIERARCHICAL FIX: Check if Owner or Borrower
const isOwner = (newItem.departmentId === this.selectedDepartment);
// If Owner, use main table. If Borrower, use movement table!
this.maxQuantity = isOwner ? newItem.quantity : (newItem.movementQuantity || 1);
} else {
this.quantity = 1; // Ensure it's 1 for non-disposable items
this.maxQuantity = null; // Clear maxQuantity for non-disposable items
this.quantity = 1;
this.maxQuantity = null;
}
},
immediate: true // Run the handler immediately when the component is mounted
immediate: true
}
},
computed: {
selectedStationPicName() {
const selectedStationObj = this.stationlist.find(station => station.stationId === this.selectedStation);
return selectedStationObj?.stationPic?.fullName || "";
},
// selectedStationPicName() {
// const selectedStationObj = this.stationlist.find(station => station.stationId === this.selectedStation);
// return selectedStationObj?.stationPic?.fullName || "";
// },
filteredDepartments() {
if (!this.selectedCompany) {
return [];
@ -1107,14 +1113,14 @@
}
},
async addItemMovement() {
// Client-side validation for quantity
if (this.thisItem && this.thisItem.category === "Disposable" && this.quantity > this.thisItem.quantity) {
alert('Error!', `The quantity you entered (${this.quantity}) exceeds the available stock (${this.thisItem.quantity}). Please enter a quantity less than or equal to the available stock.`, 'error');
// 🌟 HIERARCHICAL FIX: Validate against maxQuantity, not thisItem.quantity
if (this.thisItem && this.thisItem.category === "Disposable" && this.quantity > this.maxQuantity) {
alert('Error!', `The quantity you entered (${this.quantity}) exceeds your available stock (${this.maxQuantity}). Please enter a quantity less than or equal to the available stock.`, 'error');
return; // Prevent form submission
}
// Check if the item is disposable and set serial number accordingly
let itemQuantityToSend = 1; // Default quantity for non-disposable
let itemQuantityToSend = 1;
if (this.thisItem && this.thisItem.category === "Disposable") {
// Ensure serial number is null for disposable items
this.thisItem.serialNumber = null;
@ -1219,23 +1225,27 @@
async receiveItemMovement() {
const now = new Date();
// Determine the status based on specific conditions
let statusToSave = "";
let statusToSave = "Delivered";
// If it's On Delivery and the current user/store is the target toUser/toStore, set to Delivered
if (this.thisItem.toOther === "On Delivery" &&
(this.thisItem.toUser == this.currentUser.id || this.thisItem.toStore == this.currentUser.store || this.isStationPIC))
{
const isSameStore = String(this.thisItem.toStore) === String(this.currentUser.store);
const isTargetUser = String(this.thisItem.toUser) === String(this.currentUser.id);
const isSameDepartment = String(this.thisItem.departmentId) === String(this.selectedDepartment);
if (this.thisItem.toOther === "On Delivery") {
// Condition: On Delivery + Same Store -> Delivered
if (isSameStore || isTargetUser || this.isStationPIC) {
statusToSave = "Delivered";
}
// Calibration/Repair/Return/General Delivery goes to Ready to Deploy
else if (
this.thisItem.toOther === "Return" ||
this.thisItem.toOther === "Calibration" ||
this.thisItem.toOther === "Repair" ||
this.thisItem.toOther === "On Delivery"
) {
statusToSave = "Ready To Deploy";
}
else if (this.thisItem.toOther === "Return") {
// Condition: Return + Same Dept -> Ready To Deploy
// Condition: Return + Different Dept -> Delivered
statusToSave = isSameDepartment ? "Ready To Deploy" : "Delivered";
}
else if (this.thisItem.toOther === "Calibration" || this.thisItem.toOther === "Repair") {
// Condition: Calibration/Repair + Same Store -> Ready To Deploy
statusToSave = isSameStore ? "Ready To Deploy" : "Delivered";
}
let receiveToUser = null;
@ -1246,12 +1256,7 @@
receiveToUser = null;
receiveToStore = null;
receiveToStation = this.thisItem.toStation;
}
else if (this.thisItem.toStore) {
receiveToUser = this.currentUser.id;
receiveToStore = this.currentUser.store;
}
else if (this.thisItem.toUser) {
} else {
receiveToUser = this.currentUser.id;
receiveToStore = this.currentUser.store;
}
@ -1261,7 +1266,7 @@
ReceiveDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
Remark: this.thisItem.remark,
LatestStatus: statusToSave,
Action: "Stock In",
ToUser: receiveToUser,
ToStore: receiveToStore,
ToStation: receiveToStation,
@ -1270,6 +1275,7 @@
LastStore: this.thisItem.lastStore,
LastStation: this.thisItem.lastStation
};
try {
const response = await fetch('/InvMainAPI/UpdateItemMovementMaster', {
method: 'POST',
@ -1278,14 +1284,15 @@
});
if (response.ok) {
alert(`Success! Item received and set to: ${statusToSave}`);
this.resetScanner();
// window.location.href = '/Inventory/InventoryMaster/ItemMovement';
window.location.reload();
} else {
throw new Error('Failed to submit form.');
}
} catch (error) {
console.error('Error:', error);
alert('Inventory PSTW Error', `An error occurred: ${error.message}`, 'error');
alert('Inventory PSTW Error: An error occurred.');
}
},
async fetchItem(itemid) {
@ -1296,10 +1303,14 @@
if (response.ok) {
this.thisItem = await response.json();
// If the item is disposable, set the quantity to 1 by default, or to its current quantity if available
// 🌟 HIERARCHICAL FIX: Set max quantity based on role
if (this.thisItem.category === 'Disposable') {
this.quantity = 1;
this.maxQuantity = this.thisItem.quantity;
const isOwner = (this.thisItem.departmentId === this.selectedDepartment);
this.maxQuantity = isOwner ? this.thisItem.quantity : (this.thisItem.movementQuantity || 1);
} else {
this.quantity = 1;
this.maxQuantity = null;
@ -1379,8 +1390,6 @@
const now = new Date();
try {
// Fetch the current item movement details to get the existing Remark
const originalMovementDetailsResponse = await fetch(`/InvMainAPI/GetItemMovementById?id=${this.thisItem.movementId}`);
if (!originalMovementDetailsResponse.ok) {
throw new Error('Failed to retrieve original item movement details.');
@ -1388,25 +1397,31 @@
const originalMovementDetails = await originalMovementDetailsResponse.json();
const currentRemark = originalMovementDetails.remark || '';
// Second Movement: Re-registration/Re-stock Record
const isSameDepartment = this.thisItem.departmentId === this.selectedDepartment;
const targetLatestStatus = isSameDepartment ? 'Ready To Deploy' : 'Delivered';
const registrationMovementData = {
ItemId: this.thisItem.itemID,
LastStore: this.currentUser.store,
LastUser: this.currentUser.id,
LastStore: originalMovementDetails.toStore,
LastUser: originalMovementDetails.toUser,
LastStation: originalMovementDetails.toStation,
ToOther: null,
sendDate: null,
Action: 'Register',
Action: 'Stock In',
Quantity: this.thisItem.movementQuantity,
Remark: null,
ConsignmentNote: null,
ToUser: this.currentUser.id,
ToStore: this.currentUser.store,
LatestStatus: 'Ready To Deploy',
LatestStatus: targetLatestStatus,
receiveDate: null,
MovementComplete: true,
};
// Update the original movement to mark it as cancelled
const updateOriginalResponse = await fetch('/InvMainAPI/UpdateItemMovementMaster', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -1417,7 +1432,6 @@
ToStation: originalMovementDetails.toStation,
MovementComplete: true,
LatestStatus: 'Cancelled',
// Append the cancellation remark to the current remark
Remark: `Current: ${currentRemark.trim()}${currentRemark.trim() ? ' / ' : ''}Movement cancelled: ${this.cancelRemark}`
}),
});
@ -1426,7 +1440,6 @@
throw new Error('Failed to update original movement as cancelled.');
}
// Send the second movement (registration/re-stock)
const response2 = await fetch('/InvMainAPI/AddItemMovement', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -1437,13 +1450,12 @@
throw new Error('Failed to record re-registration movement.');
}
// Update the item's quantity by adding back the assigned quantity
const updateItemResponse = await fetch('/InvMainAPI/UpdateItemQuantity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ItemId: this.thisItem.itemID,
MovementId: this.thisItem.movementId // Pass the movement ID to find the exact quantity
MovementId: this.thisItem.movementId
}),
});
@ -1460,7 +1472,6 @@
alert('Error', `An error occurred during cancellation: ${error.message}`, 'error');
}
},
async fetchUser() {
try {
const response = await fetch(`/IdentityAPI/GetUserInformation/`, {
@ -1575,7 +1586,7 @@
ToOther: "Return",
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
Action: "StockIn",
Action: "Stock Out",
Quantity: this.thisItem.movementQuantity || 1,
Remark: this.remark,
ConsignmentNote: this.document,
@ -1608,10 +1619,10 @@
}
},
StationMessage() {
this.selectedStation = "";
$("#stationMessageModal").modal("show");
},
// StationMessage() {
// this.selectedStation = "";
// $("#stationMessageModal").modal("show");
// },
async confirmDeployStation() {
if (!this.selectedStation) {

View File

@ -277,7 +277,7 @@
<label class="col-sm-4 col-form-label">Deploy Station : </label>
<div class="col-sm-8">
<select class="btn btn-primary dropdown-toggle col-md-10" v-model="selectedStation">
<option class="btn-light" value="" disabled selected>Select Station</option>
<option class="btn-light" value="" disabled selected>-- Select Station --</option>
<option v-if="stationList.length === 0" class="btn-light" disabled>No Station Assigned to You</option>
<option class="btn-light" v-for="(station, index) in stationList" :key="index" :value="station.stationId">{{ station.stationName}}</option>
</select>
@ -619,7 +619,7 @@
}
try {
// 1. LOGIC: Identify the Origin (Last location)
let returnLastUser = null;
let returnLastStore = null;
let returnLastStation = null;
@ -631,19 +631,16 @@
} else if (this.thisItem.toUser) {
returnLastUser = this.thisItem.toUser;
} else {
returnLastUser = this.currentUserId; // Fallback to current user if not found
returnLastUser = this.currentUserId;
}
const now = new Date();
const formData = {
ItemId: this.thisItem.itemID,
// ORIGINS: Where the item is currently sitting
LastUser: returnLastUser,
LastStore: returnLastStore,
LastStation: returnLastStation,
// DESTINATIONS: Set to null (the C# API logic overrides this)
ToUser: null,
ToStore: null,
ToStation: null,
@ -655,32 +652,29 @@
ConsignmentNote: this.consignmentNote,
SendDate: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
Date: new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString(),
LatestStatus: null,
ReceiveDate: null,
MovementComplete: false,
};
const response = await fetch('/InvMainAPI/ReturnItemMovementUser', {
const response = await fetch('/InvMainAPI/ReturnToStore', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
alert('Success! Item is on the delivery to return to Inventory Master.');
alert('Success! Item is returning to the Store.');
this.thisItem = await response.json();
$('#returnModal').modal('hide');
$('#returnMessage').modal('hide');
this.displayStatus = "requestAgain";
this.resetForm();
} else {
throw new Error('Failed to submit form.');
const errorText = await response.text();
throw new Error(errorText || 'Failed to submit form.');
}
} catch (error) {
console.error('Error:', error);
alert('Inventory PSTW Error: An error occurred.');
alert('Error: ' + error.message);
}
},
async fetchItem(itemid) {

View File

@ -687,19 +687,15 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
var oldProduct = savedItem.Product;
var newProduct = await _centralDbContext.Products.FirstOrDefaultAsync(p => p.ProductId == item.ProductId) ?? throw new Exception("New Product not found");
// --- PREPARE DEPT CODES FOR JSON ---
var oldDept = await GetDepartmentWithCompany(savedItem.CompanyId, savedItem.DepartmentId);
var newDept = await GetDepartmentWithCompany(item.CompanyId, item.DepartmentId);
string oldDeptCode = oldDept?.DepartmentCode ?? "UNKNOWN";
string newDeptCode = newDept?.DepartmentCode ?? "UNKNOWN";
// Quantity adjustment logic based on category changes (JSON logic added inside each block)
if (oldProduct?.Category == "Disposable" && newProduct.Category == "Disposable")
{
int quantityDifference = item.Quantity - savedItem.Quantity;
newProduct.QuantityProduct += quantityDifference;
// If changing department but same product
if (savedItem.DepartmentId != item.DepartmentId)
{
UpdateQuantityJson(newProduct, oldDeptCode, -savedItem.Quantity);
@ -715,23 +711,23 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
if (oldProduct != null && oldProduct.QuantityProduct > 0)
{
oldProduct.QuantityProduct = (oldProduct.QuantityProduct ?? 0) - 1;
UpdateQuantityJson(oldProduct, oldDeptCode, -1); // <--- ADDED
UpdateQuantityJson(oldProduct, oldDeptCode, -1);
_centralDbContext.Products.Update(oldProduct);
}
newProduct.QuantityProduct += item.Quantity;
UpdateQuantityJson(newProduct, newDeptCode, item.Quantity); // <--- ADDED
UpdateQuantityJson(newProduct, newDeptCode, item.Quantity);
}
else if (oldProduct?.Category == "Disposable" && (newProduct.Category == "Asset" || newProduct.Category == "Part"))
{
if (oldProduct != null)
{
oldProduct.QuantityProduct = (oldProduct.QuantityProduct ?? 0) - savedItem.Quantity;
UpdateQuantityJson(oldProduct, oldDeptCode, -savedItem.Quantity); // <--- ADDED
UpdateQuantityJson(oldProduct, oldDeptCode, -savedItem.Quantity);
if (oldProduct.QuantityProduct < 0) oldProduct.QuantityProduct = 0;
_centralDbContext.Products.Update(oldProduct);
}
newProduct.QuantityProduct = (newProduct.QuantityProduct ?? 0) + 1;
UpdateQuantityJson(newProduct, newDeptCode, 1); // <--- ADDED
UpdateQuantityJson(newProduct, newDeptCode, 1);
item.Quantity = 1;
}
else if ((oldProduct?.Category == "Asset" || oldProduct?.Category == "Part") && (newProduct.Category == "Asset" || newProduct.Category == "Part"))
@ -741,22 +737,20 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
if (oldProduct != null && oldProduct.QuantityProduct > 0)
{
oldProduct.QuantityProduct = (oldProduct.QuantityProduct ?? 0) - 1;
UpdateQuantityJson(oldProduct, oldDeptCode, -1); // <--- ADDED
UpdateQuantityJson(oldProduct, oldDeptCode, -1);
_centralDbContext.Products.Update(oldProduct);
}
newProduct.QuantityProduct = (newProduct.QuantityProduct ?? 0) + 1;
UpdateQuantityJson(newProduct, newDeptCode, 1); // <--- ADDED
UpdateQuantityJson(newProduct, newDeptCode, 1);
}
item.Quantity = 1;
}
// Handle serial number based on the new product's category
if (newProduct.Category == "Disposable")
{
item.SerialNumber = null;
}
// Update savedItem properties (Existing logic kept exactly as provided)
savedItem.DefaultPrice = item.DefaultPrice;
savedItem.CompanyId = item.CompanyId;
savedItem.DepartmentId = item.DepartmentId;
@ -777,15 +771,23 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
savedItem.InvoiceNo = item.InvoiceNo;
savedItem.InvoiceDate = item.InvoiceDate;
savedItem.PartNumber = item.PartNumber;
savedItem.ModifiedDate = DateTime.Now;
var companyDepartment = await GetDepartmentWithCompany(savedItem.CompanyId, savedItem.DepartmentId);
string? deptCode = companyDepartment!.DepartmentCode?.ToString();
string? dCode = companyDepartment!.DepartmentCode?.ToString();
char? initialCategory = newProduct!.Category.ToString().Substring(0, 1).ToUpper().FirstOrDefault();
string? productId = newProduct!.ProductId.ToString("D3");
string? itemIdString = savedItem.ItemID.ToString("D5");
savedItem.UniqueID = $"{deptCode}{initialCategory}{productId}{itemIdString}".ToUpper();
savedItem.UniqueID = $"{dCode}{initialCategory}{productId}{itemIdString}".ToUpper();
var regMovement = await _centralDbContext.ItemMovements
.FirstOrDefaultAsync(m => m.ItemId == savedItem.ItemID && m.Action == "Register");
if (regMovement != null)
{
regMovement.Quantity = savedItem.Quantity;
_centralDbContext.ItemMovements.Update(regMovement);
}
_centralDbContext.Products.Update(newProduct);
_centralDbContext.Items.Update(savedItem);
@ -1064,29 +1066,12 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
try
{
//itemmovement.sendDate = DateTime.Now; // This ensures hours/minutes/seconds are captured
//itemmovement.Date = DateTime.Now; // Set the general record date as well
//var inventoryMaster = await _centralDbContext.InventoryMasters.Include("User").FirstOrDefaultAsync(i => i.UserId == itemmovement.LastUser);
//if (inventoryMaster != null)
//{
// itemmovement.LastStore = inventoryMaster.StoreId;
//}
// 1. FIX DATE OVERRULE:
// Use the date from the frontend (assigndate) if it exists.
// Only set to DateTime.Now if the frontend sent null/empty.
if (itemmovement.sendDate == default || itemmovement.sendDate == null)
{
itemmovement.sendDate = DateTime.Now;
}
itemmovement.Date = DateTime.Now; // Log the entry creation time
itemmovement.Date = DateTime.Now;
// 2. FIX STORE/USER OVERRULE:
// Only auto-fill LastStore if:
// - The frontend didn't send one (null)
// - We have a LastUser to look up
// - AND it is NOT a "user" assignment (because for 'user', we want it to stay NULL)
if (itemmovement.LastStore == null && itemmovement.LastUser != null && itemmovement.ToUser == null)
{
var inventoryMaster = await _centralDbContext.InventoryMasters
@ -1124,51 +1109,48 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
}
_centralDbContext.ItemMovements.Add(itemmovement);
await _centralDbContext.SaveChangesAsync();
await _centralDbContext.SaveChangesAsync(); // This generates the auto-incremented ItemID
var updateItem = await _centralDbContext.Items.FindAsync(itemmovement.ItemId); //only access after it have its own itemmovent
var updateItem = await _centralDbContext.Items.FindAsync(itemmovement.ItemId);
if (updateItem != null)
{
if (itemmovement.ToOther == "On Delivery")
{
updateItem.ItemStatus = 2;
}
else if (itemmovement.ToOther == "Repair" || itemmovement.ToOther == "Calibration")
{
updateItem.ItemStatus = 4;
}
else if (itemmovement.ToOther == "Faulty")
{
updateItem.ItemStatus = 8;
}
// Handle quantity update for disposable items here
// 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);
// Update statuses unconditionally
if (itemmovement.ToOther == "On Delivery") updateItem.ItemStatus = 2;
else if (itemmovement.ToOther == "Repair" || itemmovement.ToOther == "Calibration") updateItem.ItemStatus = 4;
else if (itemmovement.ToOther == "Faulty") updateItem.ItemStatus = 8;
// --- HIERARCHICAL CUSTODY CHECK ---
// Verify if the sender (LastUser) belongs to the Owner Department.
// *Note: Adjust 'DepartmentId' below to match your actual User/Item schema properties*
var currentUserDept = await _centralDbContext.Users
.Where(u => u.Id == itemmovement.LastUser)
.Select(u => u.departmentId)
.FirstOrDefaultAsync();
bool isOwnerDepartment = (updateItem.DepartmentId == currentUserDept);
// ONLY affect the Items table quantity if the Owner is sending it
if (isOwnerDepartment)
{
var product = await _centralDbContext.Products.FindAsync(updateItem.ProductId);
if (product != null)
{
// Handle variable quantity for Disposables
if (product.Category == "Disposable" && itemmovement.Quantity.HasValue)
{
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
updateItem.Quantity = 0;
}
}
}
updateItem.MovementId = itemmovement.Id;
_centralDbContext.Items.Update(updateItem);
await _centralDbContext.SaveChangesAsync(); // save changes for table item - movementid
await _centralDbContext.SaveChangesAsync();
}
return Json(new
{
@ -1207,21 +1189,19 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
{
var updatedList = await _centralDbContext.ItemMovements.FindAsync(receiveMovement.Id);
if (updatedList == null)
{
return NotFound("Item movement record not found.");
}
if (updatedList == null) return NotFound("Item movement record not found.");
updatedList.ToUser = receiveMovement.ToUser ?? updatedList.ToUser;
updatedList.ToStore = receiveMovement.ToStore ?? updatedList.ToStore;
updatedList.ToStation = receiveMovement.ToStation ?? updatedList.ToStation;
//updatedList.ToUser = receiveMovement.ToUser;
//updatedList.ToStore = receiveMovement.ToStore;
updatedList.LatestStatus = receiveMovement.LatestStatus;
updatedList.receiveDate = receiveMovement.receiveDate;
updatedList.Remark = receiveMovement.Remark;
updatedList.MovementComplete = true;
updatedList.Action = receiveMovement.Action;
var item = await _centralDbContext.Items
.Include(i => i.Product)
.FirstOrDefaultAsync(i => i.ItemID == updatedList.ItemId);
@ -1229,17 +1209,31 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
if (item != null)
{
if (updatedList.ToOther == "Return" || receiveMovement.LatestStatus == "Ready To Deploy")
{
// --- HIERARCHICAL CUSTODY CHECK ---
// Verify if the person receiving the item belongs to the Owner Department
var receiverUserId = updatedList.ToUser;
var receiverDept = await _centralDbContext.Users
.Where(u => u.Id == receiverUserId)
.Select(u => u.departmentId)
.FirstOrDefaultAsync();
bool isOwnerDepartment = (item.DepartmentId == receiverDept);
// ONLY affect the Items table quantity if the Owner is receiving it back
if (isOwnerDepartment)
{
if (item.Product?.Category == "Disposable")
{
// from movement
item.Quantity += (updatedList.Quantity ?? 0);
}
else
{
item.Quantity = 1;
}
item.ItemStatus = 1;
}
item.ItemStatus = 1; // Mark status as available
_centralDbContext.Items.Update(item);
}
}
@ -1272,14 +1266,29 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
if (item == null) return NotFound("Item not found.");
// 1. Logic for Disposable (Variable)
if (item.Product?.Category == "Disposable")
{
// Fetch original movement first so we can check who initiated it
var originalMovement = await _centralDbContext.ItemMovements
.FirstOrDefaultAsync(m => m.Id == model.MovementId);
if (originalMovement == null) return BadRequest("Original movement not found.");
// --- HIERARCHICAL CUSTODY CHECK ---
// Verify if the user who initiated the movement (LastUser) belongs to the Owner Department.
// *Note: Adjust 'DepartmentId' below to match your actual schema*
var senderUserId = originalMovement.LastUser;
var senderDept = await _centralDbContext.Users
.Where(u => u.Id == senderUserId)
.Select(u => u.departmentId)
.FirstOrDefaultAsync();
bool isOwnerDepartment = (item.DepartmentId == senderDept);
// ONLY restore the Items table quantity if the Owner initiated the cancelled movement
if (isOwnerDepartment)
{
// 1. Logic for Disposable (Variable)
if (item.Product?.Category == "Disposable")
{
item.Quantity += (originalMovement.Quantity ?? 1);
}
// 2. Logic for Part and Asset (Binary)
@ -1287,7 +1296,10 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
{
item.Quantity = 1; // Mark as back in stock/available
}
}
// We still save the item regardless, in case other status fields needed updating
// (though in this specific snippet it's just quantity being managed)
_centralDbContext.Items.Update(item);
await _centralDbContext.SaveChangesAsync();
@ -1979,51 +1991,73 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
[HttpPost("ReturnItemMovementUser")]
public async Task<IActionResult> ReturnItemMovementUser([FromBody] ItemMovementModel returnMovement)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (!ModelState.IsValid) return BadRequest(ModelState);
try
{
// --- CORE FIX: ISOLATE THE CURRENT LIFECYCLE ---
// 1. Find the last time the item cycle was "reset" (Returned or newly Registered)
var lastResetMovement = await _centralDbContext.ItemMovements
.Where(m => m.ItemId == returnMovement.ItemId && (m.Action == "StockIn" || m.Action == "Register"))
.OrderByDescending(m => m.Id)
.FirstOrDefaultAsync();
// Get the ID where the current cycle started (if 0, it means it has never been returned/registered)
int currentCycleStartId = lastResetMovement != null ? lastResetMovement.Id : 0;
var itemData = await _centralDbContext.Items
.Include(i => i.Product)
.FirstOrDefaultAsync(i => i.ItemID == returnMovement.ItemId);
// 2. Find the VERY FIRST movement AFTER the cycle started.
// This bypasses the middleman (User/Store who gave it to the station) and
// accurately grabs the Inventory Master who originally assigned it at the beginning of the chain.
var originalMasterMovement = await _centralDbContext.ItemMovements
.Where(m => m.ItemId == returnMovement.ItemId
&& m.Id > currentCycleStartId
&& (m.LastUser != null || m.LastStore != null))
.OrderBy(m => m.Id) // <-- Get the FIRST movement of the current chain
.FirstOrDefaultAsync();
if (itemData == null) return BadRequest("Item not found.");
if (originalMasterMovement != null)
if (itemData.Product?.Category == "Disposable")
{
// Set the destination back to the original Inventory Master
returnMovement.ToUser = originalMasterMovement.LastUser;
returnMovement.ToStore = originalMasterMovement.LastStore;
// Sum everything this specific user/store/station RECEIVED
IQueryable<ItemMovementModel> totalInQuery = _centralDbContext.ItemMovements
.Where(m => m.ItemId == returnMovement.ItemId && m.MovementComplete == true);
// Sum everything this specific user/store/station SENT OUT
IQueryable<ItemMovementModel> totalOutQuery = _centralDbContext.ItemMovements
.Where(m => m.ItemId == returnMovement.ItemId);
// Filter down to the specific custody level initiating the return
if (returnMovement.LastStation != null)
{
totalInQuery = totalInQuery.Where(m => m.ToStation == returnMovement.LastStation);
totalOutQuery = totalOutQuery.Where(m => m.LastStation == returnMovement.LastStation);
}
else if (returnMovement.LastUser != null)
{
totalInQuery = totalInQuery.Where(m => m.ToUser == returnMovement.LastUser);
totalOutQuery = totalOutQuery.Where(m => m.LastUser == returnMovement.LastUser);
}
else if (returnMovement.LastStore != null)
{
totalInQuery = totalInQuery.Where(m => m.ToStore == returnMovement.LastStore);
totalOutQuery = totalOutQuery.Where(m => m.LastStore == returnMovement.LastStore);
}
var totalIn = await totalInQuery.SumAsync(m => m.Quantity ?? 0);
var totalOut = await totalOutQuery.SumAsync(m => m.Quantity ?? 0);
// The true current custody balance for this borrower
int currentBalance = totalIn - totalOut;
// Override the frontend quantity with the calculated actual balance
returnMovement.Quantity = currentBalance > 0 ? currentBalance : 1;
}
else
{
// Fallback just in case history isn't found
returnMovement.ToUser = null;
returnMovement.ToStore = null;
// Parts and Assets are binary, so they are always 1
returnMovement.Quantity = 1;
}
// Set the destination to the Item's DepartmentId
// Tracing the item's department and saving in ToStore
returnMovement.ToStore = itemData.DepartmentId;
returnMovement.ToUser = null;
returnMovement.ToStation = null;
returnMovement.ToOther = "Return";
returnMovement.Action = "Stock Out";
if (!string.IsNullOrEmpty(returnMovement.ConsignmentNote))
{
var findUniqueCode = _centralDbContext.Items.Include(i => i.Product).FirstOrDefault(r => r.ItemID == returnMovement.ItemId);
// Fetch the user data for the file name based on the destination we just assigned
var findUniqueUser = _centralDbContext.Users.FirstOrDefault(r => r.Id == returnMovement.ToUser);
var bytes = Convert.FromBase64String(returnMovement.ConsignmentNote);
@ -2053,43 +2087,25 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
_centralDbContext.ItemMovements.Add(returnMovement);
await _centralDbContext.SaveChangesAsync();
itemData.MovementId = returnMovement.Id;
itemData.ItemStatus = 2;
_centralDbContext.Items.Update(itemData);
await _centralDbContext.SaveChangesAsync();
var updateItemIdMovement = await _centralDbContext.ItemMovements
.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();
}
}
return Json(new
{
updateItemIdMovement.Id,
updateItemIdMovement.ItemId,
updateItemIdMovement.LastStation,
updateItemIdMovement.LastStore,
updateItemIdMovement.LastUser,
updateItemIdMovement.ToOther,
updateItemIdMovement.sendDate,
updateItemIdMovement.Action,
updateItemIdMovement.Quantity,
updateItemIdMovement.Remark,
updateItemIdMovement.ConsignmentNote,
updateItemIdMovement.Date,
updateItemIdMovement.ToUser,
updateItemIdMovement.ToStore,
updateItemIdMovement.ToStation,
updateItemIdMovement.LatestStatus,
updateItemIdMovement.receiveDate,
updateItemIdMovement.MovementComplete
Id = updateItemIdMovement.Id,
ItemId = updateItemIdMovement.ItemId,
ToStore = updateItemIdMovement.ToStore,
ToUser = updateItemIdMovement.ToUser,
Action = updateItemIdMovement.Action,
ToOther = updateItemIdMovement.ToOther,
ConsignmentNote = updateItemIdMovement.ConsignmentNote,
MovementComplete = updateItemIdMovement.MovementComplete
});
}
catch (Exception ex)
{
@ -2097,6 +2113,84 @@ namespace PSTW_CentralSystem.Controllers.API.Inventory
}
}
[HttpPost("ReturnToStore")]
public async Task<IActionResult> ReturnToStore([FromBody] ItemMovementModel returnMovement)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
try
{
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Unauthorized("User not found.");
if (currentUser.departmentId == null)
{
return BadRequest("Your user profile is not assigned to a Department.");
}
// Assign the current user's department to ToStore
returnMovement.ToStore = currentUser.departmentId;
returnMovement.ToUser = null;
returnMovement.ToStation = null;
returnMovement.ToOther = "Return";
returnMovement.Action = "StockIn";
if (!string.IsNullOrEmpty(returnMovement.ConsignmentNote))
{
var bytes = Convert.FromBase64String(returnMovement.ConsignmentNote);
var itemData = await _centralDbContext.Items
.Include(i => i.Product)
.FirstOrDefaultAsync(r => r.ItemID == returnMovement.ItemId);
string safeUserName = string.Join("_", (currentUser.FullName ?? "Unknown").Split(Path.GetInvalidFileNameChars()));
string safeModelNo = string.Join("_", (itemData?.Product?.ModelNo ?? "NA").Split(Path.GetInvalidFileNameChars()));
string uniqueId = Guid.NewGuid().ToString().Substring(0, 8);
string ext = IsPdf(bytes) ? ".pdf" : ".jpg";
string relativePath = $"media/inventory/itemmovement/{safeUserName}_{safeModelNo}_{uniqueId}_Return{ext}";
string folderPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", relativePath);
if (!Directory.Exists(Path.GetDirectoryName(folderPath))) Directory.CreateDirectory(Path.GetDirectoryName(folderPath));
await System.IO.File.WriteAllBytesAsync(folderPath, bytes);
returnMovement.ConsignmentNote = "/" + relativePath;
}
_centralDbContext.ItemMovements.Add(returnMovement);
await _centralDbContext.SaveChangesAsync();
if (returnMovement.ItemId.HasValue)
{
var item = await _centralDbContext.Items.FindAsync(returnMovement.ItemId.Value);
if (item != null)
{
item.MovementId = returnMovement.Id;
item.ItemStatus = 2;
_centralDbContext.Items.Update(item);
await _centralDbContext.SaveChangesAsync();
}
}
return Json(new
{
id = returnMovement.Id,
itemId = returnMovement.ItemId,
toStore = returnMovement.ToStore,
toUser = returnMovement.ToUser,
lastUser = returnMovement.LastUser,
lastStore = returnMovement.LastStore,
action = returnMovement.Action,
toOther = returnMovement.ToOther,
remark = returnMovement.Remark,
consignmentNote = returnMovement.ConsignmentNote
});
}
catch (Exception ex)
{
return BadRequest($"Error: {ex.Message}");
}
}
[HttpPost("StationItemMovementUser")]
public async Task<IActionResult> StationItemMovementUser([FromBody] ItemMovementModel stationMovement)
{