RIS creation, approval and Cancel working well

This commit is contained in:
rowell_m_soriano 2026-06-15 16:41:50 +08:00
parent 6710f04bd7
commit 44862d01b5
71 changed files with 6275 additions and 609 deletions

View File

@ -1,4 +1,6 @@
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Entities.Inventory;
using System;
using System.Collections.Generic;
@ -11,11 +13,15 @@ namespace CPRNIMS.Domain.Contracts.Inventory
public interface IInventory
{
Task<List<Lot>> GetLotNo(InventoryDto itemDto);
Task<List<Lot>> GetLotNoById(InventoryDto itemDto);//
Task<List<Lot>> GetLotNoById(InventoryDto itemDto);
Task<List<LotQtyByItem>> GetLotQtyByItem(InventoryDto itemDto);
Task<List<Infrastructure.Entities.Inventory.Inventory>> GetInventoryByUserId(InventoryDto itemDto);
Task<List<RequestItemDetail>> GetRequestedItemByUserId(InventoryDto itemDto);
Task<List<ItemDetail>> GetInventoryById(InventoryDto itemDto);
Task<PagedResult<InventoryResponse>> GetInventory(InventoryRequest request, CancellationToken ct);
Task<List<InventoryByIdResponse>> GetInventoryById(InventoryRequest itemDto, CancellationToken ct);
Task<TransactContextDto?> GetTransactContextAsync(int inventoryId, CancellationToken ct);
Task<IEnumerable<DisciplineDto>> GetDisciplinesAsync(CancellationToken ct);
Task<Infrastructure.Entities.Inventory.Inventory> PostPutReqApproval(InventoryDto itemDto);
Task<RequestItem> PostPutReqItems(InventoryDto itemDto);
Task<Lot> PostPutLotNo(InventoryDto itemDto);

View File

@ -0,0 +1,19 @@
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Entities.Inventory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Contracts.Inventory
{
public interface IMRS
{
Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter);
Task<MRS?> GetByIdAsync(long mrsId);
Task<MRS> CreateAsync(CreateMRSRequest dto, string createdBy);
Task ApproveAsync(long mrsId, string approvedBy);
}
}

View File

@ -0,0 +1,22 @@
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Entities.Inventory;
using CPRNIMS.Infrastructure.Entities.Purchasing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Contracts.Inventory
{
public interface IRIS
{
Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct);
Task<RISResponse?> GetByIdAsync(long risId, CancellationToken ct);
Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct);
Task ApproveAsync(ApproveRISRequest request, string approvedBy, CancellationToken ct);
Task CancelAsync(CancelRISRequest request, CancellationToken ct);
}
}

View File

@ -1,8 +1,8 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Items;
using CPRNIMS.Infrastructure.Entities.Finance;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Entities.Inventory;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
@ -21,65 +21,203 @@ namespace CPRNIMS.Domain.Services.Inventory
{
_dbContext = dbContext;
}
public async Task<List<Infrastructure.Entities.Inventory.ItemDetail>> GetInventoryById(InventoryDto itemDto)
public async Task<TransactContextDto?> GetTransactContextAsync(int inventoryId, CancellationToken ct)
{
try
{
var allItems = await _dbContext.ItemDetails
.FromSqlRaw($"EXEC GetInventoryById @UserId = '{itemDto.UserId}',@InventoryId = '{itemDto.InventoryId}'")
.ToListAsync();
return allItems ?? new List<Infrastructure.Entities.Inventory.ItemDetail>();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
}
public async Task<List<Infrastructure.Entities.Inventory.Inventory>>
GetInventoryByUserId(InventoryDto itemDto)
{
try
{
if(itemDto.IsSorting == false)
var inv = await _dbContext.Inventories
.Where(i => i.InventoryId == inventoryId && i.IsActive)
.Select(i => new
{
itemDto.DateFrom=DateTime.Now;
itemDto.DateTo = DateTime.Now;
}
var allItems = await _dbContext.Inventories
.FromSqlRaw($"EXEC GetInventoryByUserId @UserId,@DateFrom,@DateTo,@IsSorting",
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@DateFrom", itemDto.DateFrom),
new SqlParameter("@DateTo", itemDto.DateTo),
new SqlParameter("@IsSorting", itemDto.IsSorting))
.ToListAsync();
i.InventoryId,
i.QtyOnHand,
i.QtyIn,
i.QtyOut,
return allItems ?? new List<Infrastructure.Entities.Inventory.Inventory>();
}
catch (SqlException ex)
LotNo = i.Lot != null ? i.Lot.LotName : null,
Department = i.User != null && i.User.Department != null
? i.User.Department.Department
: null,
i.ItemNo,
FirstDetail = i.InventTrans
.Where(t => t.IsActive)
.SelectMany(t => t.InventTransDetails)
.Where(d => d.PRDetails != null
&& d.PRDetails.PRs != null
&& d.IsActive)
.Select(d => new
{
d.PRDetails!.ItemName,
PRNo = d.PRDetails.PRs!.PRNo
})
.FirstOrDefault()
})
.FirstOrDefaultAsync(ct);
if (inv == null) return null;
// Computed property QtyAvailableToReturn can't be used in EF Where,
// so filter after projection using a raw expression in the query.
var openRIS = await _dbContext.RIS
.Where(r => r.InventoryId == inventoryId
&& r.Status == 1)
.Select(r => new RISReferenceDto
{
RISId = r.RISId,
RISNo = r.RISNo,
QtyIssued = r.QtyIssued,
TotalReturned = r.MaterialReturns
.Where(m => m.Status != 2)
.Sum(m => (int?)m.QtyReturned) ?? 0,
DisciplineName = r.Discipline.DisciplineName,
CreatedDate = r.CreatedDate
})
// Can't use the computed property here — EF won't translate it
// so we repeat the expression inline
.Where(r => r.QtyIssued - r.TotalReturned > 0)
.OrderByDescending(r => r.CreatedDate)
.ToListAsync(ct);
var disciplines = await GetDisciplinesAsync(ct);
return new TransactContextDto
{
ex.ToString();
throw;
InventoryId = inv.InventoryId,
ItemName = inv.FirstDetail?.ItemName ?? "—",
PRNo = inv.FirstDetail.PRNo,
ItemNo = inv.ItemNo,
LotNo = inv.LotNo,
Department = inv.Department,
QtyOnHand = inv.QtyOnHand,
QtyIn = inv.QtyIn,
QtyOut = inv.QtyOut,
Disciplines = disciplines,
OpenRISList = openRIS
};
}
public async Task<IEnumerable<DisciplineDto>> GetDisciplinesAsync(CancellationToken ct)
{
return await _dbContext.Disciplines
.OrderBy(d => d.DisciplineName)
.Select(d => new DisciplineDto
{
DisciplineId = d.DisciplineId,
DisciplineName = d.DisciplineName
})
.ToListAsync(ct);
}
public async Task<List<ItemDetail>> GetInventoryById(InventoryDto itemDto)
{
var allItems = await _dbContext.ItemDetails
.FromSqlRaw($"EXEC GetInventoryById @UserId = '{itemDto.UserId}',@InventoryId = '{itemDto.InventoryId}'")
.ToListAsync();
return allItems ?? new List<Infrastructure.Entities.Inventory.ItemDetail>();
}
public async Task<PagedResult<InventoryResponse>> GetInventory(InventoryRequest request, CancellationToken ct)
{
var parameters = new[]
{
new SqlParameter("@UserId", request.UserId),
new SqlParameter("@SearchPRNo", request.SearchPRNo ?? ""),
new SqlParameter("@SearchItemNo", request.SearchItemNo ?? ""),
new SqlParameter("@SearchItemName", request.SearchItemName ?? ""),
new SqlParameter("@SearchDept", request.SearchDept ?? ""),
new SqlParameter("@SearchProjectCode",request.SearchProjectCode ?? ""),
new SqlParameter("@PageNumber", request.PageNumber),
new SqlParameter("@PageSize", request.PageSize)
};
var departmentList = new List<string>();
int totalCount = 0;
var items = new List<InventoryResponse>();
await using var conn = _dbContext.Database.GetDbConnection();
await conn.OpenAsync(ct);
using var cmd = conn.CreateCommand();
cmd.CommandText = @"EXEC GetInventory @UserId, @SearchPRNo, @SearchItemNo, @SearchItemName, @SearchDept,@SearchProjectCode, @PageNumber, @PageSize";
foreach (var p in parameters) cmd.Parameters.Add(p);
cmd.CommandTimeout = 60;
using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
departmentList.Add(reader.GetString(0));
await reader.NextResultAsync(ct);
if (await reader.ReadAsync(ct))
totalCount = reader.GetInt32(0);
await reader.NextResultAsync(ct);
while (await reader.ReadAsync(ct))
{
items.Add(new InventoryResponse
{
InventoryId = reader.GetInt32(reader.GetOrdinal("InventoryId")),
QtyIn = reader["QtyIn"] as decimal? ?? 0,
QtyOut = reader["QtyOut"] as decimal? ?? 0,
QtyOnHand = reader["QtyOnHand"] as decimal? ?? 0,
LotNo = reader["LotNo"]?.ToString(),
PRNo = Convert.ToInt64(reader["PRNo"]),
UserId = reader["UserId"]?.ToString(),
ItemName = reader["ItemName"]?.ToString(),
ItemNo = Convert.ToInt64(reader["ItemNo"]),
ItemDescription = reader["ItemDescription"]?.ToString(),
ItemCategoryName = reader["ItemCategoryName"]?.ToString(),
Department = reader["Department"]?.ToString(),
ProjectCode = reader["ProjectCode"]?.ToString(),
CreatedDate = reader["CreatedDate"] == DBNull.Value
? DateTime.MinValue
: Convert.ToDateTime(reader["CreatedDate"])
});
}
await conn.CloseAsync();
return new PagedResult<InventoryResponse>
{
Data = items,
TotalCount = totalCount,
PageNumber = request.PageNumber,
PageSize = request.PageSize,
DepartmentList = departmentList
};
}
public async Task<List<InventoryByIdResponse>> GetInventoryById(InventoryRequest itemDto,CancellationToken ct)
{
var allItems = await _dbContext.InventoryByIdResponses
.FromSqlRaw($"EXEC GetInventoryById @InventoryId",
new SqlParameter("@InventoryId", itemDto.InventoryId))
.ToListAsync(ct);
return allItems ?? new List<InventoryByIdResponse>();
}
public async Task<List<Infrastructure.Entities.Inventory.Inventory>>GetInventoryByUserId(InventoryDto itemDto)
{
if (itemDto.IsSorting == false)
{
itemDto.DateFrom = DateTime.Now;
itemDto.DateTo = DateTime.Now;
}
var allItems = await _dbContext.Inventories
.FromSqlRaw($"EXEC GetInventoryByUserId @UserId,@DateFrom,@DateTo,@IsSorting",
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@DateFrom", itemDto.DateFrom),
new SqlParameter("@DateTo", itemDto.DateTo),
new SqlParameter("@IsSorting", itemDto.IsSorting))
.ToListAsync();
return allItems ?? new List<Infrastructure.Entities.Inventory.Inventory>();
}
public async Task<List<Lot>> GetLotNoById(InventoryDto itemDto)
{
try
{
var allItems = await _dbContext.Lots
.FromSqlRaw($"EXEC GetLotById @UserId = '{itemDto.UserId}'")
.ToListAsync();
var allItems = await _dbContext.Lots
.FromSqlRaw($"EXEC GetLotById @UserId = '{itemDto.UserId}'")
.ToListAsync();
return allItems ?? new List<Lot>();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
return allItems ?? new List<Lot>();
}
public async Task<List<Lot>> GetLotNo(InventoryDto itemDto)
@ -94,125 +232,77 @@ namespace CPRNIMS.Domain.Services.Inventory
public async Task<Lot> PostPutLotNo(InventoryDto itemDto)
{
try
{
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutLotNo @UserId, @LotId, @LotTypeId,@LotName",
new SqlParameter("@LotId", itemDto.LotId != null ? itemDto.LotId : 0L),
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@LotTypeId", itemDto.LotTypeId),
new SqlParameter("@LotName", itemDto.LotName));
return new Lot();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutLotNo @UserId, @LotId, @LotTypeId,@LotName",
new SqlParameter("@LotId", itemDto.LotId != null ? itemDto.LotId : 0L),
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@LotTypeId", itemDto.LotTypeId),
new SqlParameter("@LotName", itemDto.LotName));
return new Lot();
}
public async Task<Infrastructure.Entities.Inventory.Inventory> PostPutReqApproval(InventoryDto itemDto)
{
try
{
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutReqApproval @UserId, @ItemNo, @Status, @Remarks",
new SqlParameter("@ItemNo", itemDto.InventoryId != null ? itemDto.InventoryId : 0L),
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@Status", itemDto.Status),
new SqlParameter("@Remarks", itemDto.Remarks ?? "N/A"));
return new Infrastructure.Entities.Inventory.Inventory();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutReqApproval @UserId, @ItemNo, @Status, @Remarks",
new SqlParameter("@ItemNo", itemDto.InventoryId != null ? itemDto.InventoryId : 0L),
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@Status", itemDto.Status),
new SqlParameter("@Remarks", itemDto.Remarks ?? "N/A"));
return new Infrastructure.Entities.Inventory.Inventory();
}
public async Task<ItemDetail> PostPutLotBin(InventoryDto itemDto)
{
try
{
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutLotBin @UserId, @LotId, @InventoryId",
new SqlParameter("@InventoryId", itemDto.InventoryId),
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@LotId", itemDto.LotId));
return new ItemDetail();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutLotBin @UserId, @LotId, @InventoryId",
new SqlParameter("@InventoryId", itemDto.InventoryId),
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@LotId", itemDto.LotId));
return new ItemDetail();
}
public async Task<RequestItem> PostPutReqItems(InventoryDto itemDto)
{
try
if (itemDto.QtyReceived == null || itemDto.QtyReceived == 0)
{
if(itemDto.QtyReceived == null || itemDto.QtyReceived == 0)
{
itemDto.QtyReceived = 0;
}
if(itemDto.LotId == null || itemDto.LotId == 0)
{
itemDto.LotId = 0;
}
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutReqItems @UserId, @RequestItemId, @ItemNo, @QtyRequest,@Status,@IsApproved,@QtyReceived,@LotId",
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@ItemNo", itemDto.ItemNo),
new SqlParameter("@RequestItemId", itemDto.RequestItemId != null ? itemDto.RequestItemId : 0L),
new SqlParameter("@QtyRequest", itemDto.QtyRequest),
new SqlParameter("@Status", itemDto.Status),
new SqlParameter("@IsApproved", itemDto.IsApproved),
new SqlParameter("@QtyReceived", itemDto.QtyReceived),
new SqlParameter("@LotId", itemDto.LotId));
return new RequestItem();
itemDto.QtyReceived = 0;
}
catch (SqlException ex)
if (itemDto.LotId == null || itemDto.LotId == 0)
{
ex.ToString();
throw;
itemDto.LotId = 0;
}
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutReqItems @UserId, @RequestItemId, @ItemNo, @QtyRequest,@Status,@IsApproved,@QtyReceived,@LotId",
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@ItemNo", itemDto.ItemNo),
new SqlParameter("@RequestItemId", itemDto.RequestItemId != null ? itemDto.RequestItemId : 0L),
new SqlParameter("@QtyRequest", itemDto.QtyRequest),
new SqlParameter("@Status", itemDto.Status),
new SqlParameter("@IsApproved", itemDto.IsApproved),
new SqlParameter("@QtyReceived", itemDto.QtyReceived),
new SqlParameter("@LotId", itemDto.LotId));
return new RequestItem();
}
public async Task<List<RequestItemDetail>> GetRequestedItemByUserId(InventoryDto itemDto)
{
try
{
var allItems = await _dbContext.RequestItemDetails
var allItems = await _dbContext.RequestItemDetails
.FromSqlRaw($"EXEC GetRequestedItemByUserId @UserId = '{itemDto.UserId}', " +
$"@RequestItemId = '{itemDto.RequestItemId}',@WithoutStocks = '{itemDto.WithoutStocks}'")
.ToListAsync();
return allItems ?? new List<RequestItemDetail>();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
return allItems ?? new List<RequestItemDetail>();
}
public async Task<List<LotQtyByItem>> GetLotQtyByItem(InventoryDto itemDto)
{
try
{
var allItems = await _dbContext.LotQtyByItems
.FromSqlRaw($"EXEC GetLotQtyByItem @UserId = '{itemDto.UserId}', " +
$"@ItemNo = '{itemDto.ItemNo}',@LotId = '{itemDto.LotId}'")
.ToListAsync();
var allItems = await _dbContext.LotQtyByItems
.FromSqlRaw($"EXEC GetLotQtyByItem @UserId = '{itemDto.UserId}', " +
$"@ItemNo = '{itemDto.ItemNo}',@LotId = '{itemDto.LotId}'")
.ToListAsync();
return allItems ?? new List<LotQtyByItem>();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
return allItems ?? new List<LotQtyByItem>();
}
}
}

View File

@ -0,0 +1,155 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Entities.Inventory;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Services.Inventory
{
public class MRS : IMRS
{
private readonly NonInventoryDbContext _db;
public MRS(NonInventoryDbContext db) => _db = db;
public async Task ApproveAsync(long mrsId, string approvedBy)
{
var rms = await _db.MRS.FindAsync(mrsId)
?? throw new InvalidOperationException("MRS not found.");
if (rms.Status != 0)
throw new InvalidOperationException("Only Draft MRS records can be approved.");
rms.Status = 1; // Approved
rms.ApprovedBy = approvedBy;
rms.ApprovedDate = DateTime.Now;
await _db.SaveChangesAsync();
}
public async Task<Infrastructure.Entities.Inventory.MRS> CreateAsync(CreateMRSRequest dto, string createdBy)
{
var ris = await _db.RIS
.Include(r => r.Inventory)
.FirstOrDefaultAsync(r => r.RISId == dto.RISId)
?? throw new InvalidOperationException("Referenced RIS not found.");
if (dto.QtyReturned > ris.QtyIssued)
throw new InvalidOperationException(
$"Cannot return more than issued. Issued: {ris.QtyIssued}.");
var mrsNo = await GenerateMRSNoAsync();
var mrs = new Infrastructure.Entities.Inventory.MRS
{
MRSNo = mrsNo,
RISId = dto.RISId,
InventoryId = ris.InventoryId,
ReturnedBy = dto.ReturnedBy,
QtyReturned = dto.QtyReturned,
Condition = dto.Condition,
Remarks = dto.Remarks,
Status = 0,
CreatedBy = createdBy,
CreatedDate = DateTime.Now
};
_db.MRS.Add(mrs);
var inventory = ris.Inventory;
inventory.QtyOut = Math.Max(0m, inventory.QtyOut - dto.QtyReturned);
inventory.QtyOnHand = inventory.QtyIn - inventory.QtyOut;
var trans = await _db.InventTrans
.FirstOrDefaultAsync(t => t.InventoryId == ris.InventoryId && t.IsActive == true)!;
_db.InventTransDetails.Add(new InventTransDetail
{
InventTransId = trans.InventTransId,
TransTypeId = 6,
QtyIn = dto.QtyReturned,
CreatedDate = DateTime.Now,
Remarks = $"MRS: {mrsNo} — return from MRS: {ris.RISNo}",
IsActive = true
});
await _db.SaveChangesAsync();
return mrs;
}
public async Task<Infrastructure.Entities.Inventory.MRS?> GetByIdAsync(long mrsId)
=> await _db.MRS
.Include(r => r.Inventory)
.Include(r => r.RIS)
.FirstOrDefaultAsync(r => r.RISId == mrsId);
public async Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter)
{
var q = _db.MRS
.Include(m => m.RIS)
.Include(m => m.Inventory)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(filter.SearchMRSNo))
q = q.Where(m => m.MRSNo.Contains(filter.SearchMRSNo));
if (filter.RISId.HasValue)
q = q.Where(m => m.RISId == filter.RISId.Value);
if (filter.Status.HasValue)
q = q.Where(m => m.Status == filter.Status.Value);
if (filter.DateFrom.HasValue)
q = q.Where(m => m.CreatedDate >= filter.DateFrom.Value);
if (filter.DateTo.HasValue)
q = q.Where(m => m.CreatedDate <= filter.DateTo.Value.AddDays(1));
var total = await q.CountAsync();
var data = await q
.OrderByDescending(m => m.CreatedDate)
.Skip((filter.Page - 1) * filter.PageSize)
.Take(filter.PageSize)
.Select(m => new MRSResponse
{
MRSId = m.MRSId,
MRSNo = m.MRSNo,
RISId = m.RISId,
RISNo = m.RIS.RISNo,
InventoryId = m.InventoryId,
ItemName = m.Inventory.InventTrans
.SelectMany(t => t.InventTransDetails)
.Select(d => d.PRDetails != null ? d.PRDetails.ItemName : "—")
.FirstOrDefault() ?? "—",
ReturnedBy = m.ReturnedBy,
QtyReturned = m.QtyReturned,
Condition = m.Condition,
Remarks = m.Remarks,
Status = m.Status,
CreatedBy = m.CreatedBy,
CreatedDate = m.CreatedDate,
ApprovedBy = m.ApprovedBy,
ApprovedDate = m.ApprovedDate
})
.ToListAsync();
return new MRSPagedResult { Data = data, RecordsTotal = total };
}
private async Task<string> GenerateMRSNoAsync()
{
var year = DateTime.Now.Year;
var month = DateTime.Now.Month.ToString("D2");
var count = await _db.MRS.CountAsync(m => m.CreatedDate.Year == year) + 1;
return $"MRS-{year}{month}-{count:D4}";
}
}
}

View File

@ -0,0 +1,307 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Domain.UIServices.Inventory;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Entities.Inventory;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Services.Inventory
{
public class RIS : IRIS
{
private readonly NonInventoryDbContext _db;
public RIS(NonInventoryDbContext db) => _db = db;
public async Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct)
{
var strategy = _db.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
try
{
var inventory = await _db.Inventories
.FirstOrDefaultAsync(i => i.InventoryId == dto.InventoryId, ct)
?? throw new InvalidOperationException("Inventory record not found.");
if (inventory.QtyOnHand < dto.QtyIssued)
throw new InvalidOperationException(
$"Insufficient stock. On hand: {inventory.QtyOnHand}, requested: {dto.QtyIssued}.");
var risNo = await GenerateRISNoAsync(ct);
var ris = new Infrastructure.Entities.Inventory.RIS
{
RISNo = risNo,
InventoryId = dto.InventoryId,
PRDetailId = dto.PRDetailId,
IssuedTo = dto.IssuedTo,
DisciplineId = dto.DisciplineId,
QtyIssued = dto.QtyIssued,
Remarks = dto.Remarks,
Status = 0,
CreatedBy = createdBy,
CreatedDate = DateTime.Now
};
_db.RIS.Add(ris);
await _db.SaveChangesAsync(ct);
var trans = await _db.InventTrans
.Where(t => t.InventoryId == dto.InventoryId && t.IsActive == true)
.FirstOrDefaultAsync(ct)
?? throw new InvalidOperationException(
"No active InventTrans found for this inventory record.");
_db.InventTransDetails.Add(new InventTransDetail
{
InventTransId = trans.InventTransId,
TransTypeId = 5,
PRDetailId = dto.PRDetailId,
QtyOut = dto.QtyIssued,
CreatedDate = DateTime.Now,
Remarks = $"RIS: {risNo}",
IsActive = true
});
inventory.QtyOut = Math.Max(0m, inventory.QtyOut) + dto.QtyIssued;
inventory.QtyOnHand = Math.Max(0m, inventory.QtyIn) - (decimal)inventory.QtyOut;
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return ris;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
});
}
public async Task ApproveAsync(ApproveRISRequest request, string approvedBy, CancellationToken ct)
{
var ris = await _db.RIS.FindAsync(request.RISId, ct)
?? throw new InvalidOperationException("RIS not found.");
if (ris.Status != 0)
throw new InvalidOperationException("Only Draft RIS records can be approved.");
ris.Status = 1; // Approved
ris.ApprovedBy = approvedBy;
ris.ApprovedDate = DateTime.Now;
await _db.SaveChangesAsync(ct);
}
public async Task CancelAsync(CancelRISRequest request, CancellationToken ct)
{
var strategy = _db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
try
{
var ris = await _db.RIS
.Include(r => r.Inventory)
.FirstOrDefaultAsync(r => r.RISId == request.RISId, ct)
?? throw new InvalidOperationException("RIS not found.");
if (ris.Status == 2)
throw new InvalidOperationException("RIS is already cancelled.");
ris.Inventory.QtyOut = Math.Max(0m, ris.Inventory.QtyOut - ris.QtyIssued);
ris.Inventory.QtyOnHand = ris.Inventory.QtyIn - ris.Inventory.QtyOut;
ris.Reason = request.Reason;
ris.Status = 2; // Cancelled
var trans = await _db.InventTrans
.Where(t => t.InventoryId == ris.InventoryId && t.IsActive == true)
.FirstOrDefaultAsync(ct)
?? throw new InvalidOperationException(
"No active InventTrans found for this inventory record.");
_db.InventTransDetails.Add(new InventTransDetail
{
InventTransId = trans.InventTransId,
TransTypeId = 5,
PRDetailId = ris.PRDetailId,
QtyIn = ris.QtyIssued,//Return the issued Qty
CreatedDate = DateTime.Now,
Remarks = request.Reason,
IsActive = true
});
//var inventory = await _db.Inventories
// .FirstOrDefaultAsync(i => i.InventoryId == ris.InventoryId, ct)
// ?? throw new InvalidOperationException("Inventory record not found.");
//if (inventory.QtyOnHand < ris.QtyIssued)
// throw new InvalidOperationException(
// $"Insufficient stock. On hand: {inventory.QtyOnHand}, requested: {ris.QtyIssued}.");
////restore the QtyOnHand using ris.QtyIssued
//inventory.QtyOnHand = Math.Max(0m, inventory.QtyOnHand) + ris.QtyIssued;
////reduce the QtyOut using ris.QtyIssued as we cancel the return isuance slip
//inventory.QtyOut = Math.Max(0m, inventory.QtyOut) - ris.QtyIssued;
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return ris;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
});
}
private async Task<string> GenerateRISNoAsync(CancellationToken ct)
{
var year = DateTime.Now.Year;
var month = DateTime.Now.Month.ToString("D2");
var count = await _db.RIS
.CountAsync(r => r.CreatedDate.Year == year,ct) + 1;
return $"RIS-{year}{month}-{count:D4}"; // e.g. RIS-202606-0001
}
public async Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct)
{
var q = _db.RIS
.Include(r => r.Discipline)
.Include(r => r.Inventory)
.Include(r => r.MaterialReturns)
.AsQueryable();
// Status filter (default to Draft=0 if null)
if (filter.Status.HasValue)
q = q.Where(r => r.Status == filter.Status.Value);
else
q = q.Where(r => r.Status == 0);
// RIS No
if (!string.IsNullOrWhiteSpace(filter.SearchRISNo))
q = q.Where(r => r.RISNo.Contains(filter.SearchRISNo));
// Item Name
if (!string.IsNullOrWhiteSpace(filter.SearchItemName))
q = q.Where(r => r.Inventory.InventTrans
.SelectMany(t => t.InventTransDetails)
.Any(d => d.PRDetails != null &&
d.PRDetails.ItemName.Contains(filter.SearchItemName)));
// Issued To
if (!string.IsNullOrWhiteSpace(filter.SearchIssuedTo))
q = q.Where(r => r.IssuedTo.Contains(filter.SearchIssuedTo));
// Discipline
if (!string.IsNullOrWhiteSpace(filter.Discipline))
q = q.Where(r => r.Discipline.DisciplineName == filter.Discipline);
// Date range (if you ever add date filters)
if (filter.DateFrom.HasValue)
q = q.Where(r => r.CreatedDate >= filter.DateFrom.Value);
if (filter.DateTo.HasValue)
q = q.Where(r => r.CreatedDate <= filter.DateTo.Value.AddDays(1));
var total = await q.CountAsync(ct);
var data = await q
.OrderByDescending(r => r.CreatedDate)
.Skip((filter.PageNumber - 1) * filter.PageSize)
.Take(filter.PageSize)
.Select(r => new RISResponse
{
RISId = r.RISId,
RISNo = r.RISNo,
InventoryId = r.InventoryId,
ItemName = r.Inventory.InventTrans
.SelectMany(t => t.InventTransDetails)
.Select(d => d.PRDetails != null ? d.PRDetails.ItemName : "—")
.FirstOrDefault() ?? "—",
ItemNo = r.Inventory.ItemNo,
LotNo = r.Inventory.Lot != null ? r.Inventory.Lot.LotName : null,
IssuedTo = r.IssuedTo,
DisciplineName = r.Discipline.DisciplineName,
DisciplineId = r.DisciplineId,
QtyIssued = r.QtyIssued,
Remarks = r.Remarks,
Status = r.Status,
CreatedBy = r.CreatedBy,
CreatedDate = r.CreatedDate,
ApprovedBy = r.ApprovedBy,
ApprovedDate = r.ApprovedDate,
MRSCount = r.MaterialReturns.Count(m => m.Status != 2),
TotalReturned = r.MaterialReturns
.Where(m => m.Status != 2)
.Sum(m => m.QtyReturned)
})
.ToListAsync(ct);
// Full department list (never filtered)
var departments = await _db.Departments
.Select(d => d.Department)
.OrderBy(d => d)
.ToListAsync(ct);
var disciplines = await GetDisciplinesAsync(ct);
return new RISPagedResult
{
Data = data,
RecordsTotal = total,
DepartmentList = departments,
DisciplineList = disciplines
};
}
public async Task<IEnumerable<DisciplineDto>> GetDisciplinesAsync(CancellationToken ct)
{
return await _db.Disciplines
.OrderBy(d => d.DisciplineName)
.Select(d => new DisciplineDto
{
DisciplineId = d.DisciplineId,
DisciplineName = d.DisciplineName
})
.ToListAsync(ct);
}
public async Task<RISResponse?> GetByIdAsync(long risId, CancellationToken ct)
{
return await _db.RIS
.Where(r => r.RISId == risId)
.Select(r => new RISResponse
{
RISId = r.RISId,
RISNo = r.RISNo,
InventoryId = r.InventoryId,
IssuedTo = r.IssuedTo,
DisciplineName = r.Discipline.DisciplineName,
DisciplineId = r.DisciplineId,
QtyIssued = r.QtyIssued,
Remarks = r.Remarks,
Status = r.Status,
CreatedBy = r.CreatedBy,
CreatedDate = r.CreatedDate,
ApprovedBy = r.ApprovedBy,
ApprovedDate = r.ApprovedDate,
MRSCount = r.MaterialReturns.Count(m => m.Status != 2),
TotalReturned = r.MaterialReturns
.Where(m => m.Status != 2)
.Sum(m => m.QtyReturned)
})
.FirstOrDefaultAsync(ct);
}
}
}

View File

@ -1,4 +1,6 @@
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Models.Account;
using CPRNIMS.Infrastructure.ViewModel.Inventory;
using System;
@ -11,12 +13,15 @@ namespace CPRNIMS.Domain.UIContracts.Inventory
{
public interface IInventory
{
Task<TransactContextDto?> GetTransactContextAsync(int inventoryId);
Task<List<InventoryVM>> GetInventoryByUserId(User user, InventoryVM viewModel);
Task<List<InventoryVM>> GetRequestedItemByUserId(User user, InventoryVM viewModel);
Task<List<InventoryVM>> GetInventoryById(User user, InventoryVM viewModel);
Task<List<InventoryVM>> GetLotNo(User user, InventoryVM viewModel);
Task<List<InventoryVM>> GetLotQtyByItem(User user, InventoryVM viewModel);
Task<List<InventoryVM>> GetLotNoById(User user, InventoryVM viewModel);
Task<PagedResult<InventoryResponse>> GetInventory(User user, InventoryRequest request);
Task<InventoryVM> PostPutLotNo(User user, InventoryVM viewModel);
Task<InventoryVM> PostPutLotBin(User user, InventoryVM viewModel);
Task<InventoryVM> PostPutReqApproval(User user, InventoryVM viewModel);

View File

@ -0,0 +1,20 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.ViewModel.Inventory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.UIContracts.Inventory
{
public interface IRIS
{
Task<bool> ApproveRIS(ApproveRISRequest request, CancellationToken ct);
Task<bool> CancelRIS(CancelRISRequest request, CancellationToken ct);
Task<RISResponse> CreateRIS(CreateRISRequest request, CancellationToken ct);
Task<RISPagedResponse> GetRISById(int risId, CancellationToken ct);
Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct);
}
}

View File

@ -1,9 +1,11 @@
using CPRNIMS.Domain.UIContracts.Common;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.Models.Account;
using CPRNIMS.Infrastructure.Models.Common;
using CPRNIMS.Infrastructure.ViewModel.Finance;
using CPRNIMS.Infrastructure.ViewModel.Inventory;
using Microsoft.Extensions.Configuration;
using System;
@ -123,6 +125,73 @@ namespace CPRNIMS.Domain.UIServices.Inventory
throw;
}
}
public async Task<TransactContextDto?> GetTransactContextAsync(int inventoryId)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
return null;
var apiEndpoint =
_configuration["LLI:NonInvent:InventoryMgmt:GetTransactContext"] ?? "api/InventoryMgmt/GetTransactContext";
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync( $"{apiEndpoint}?inventoryId={inventoryId}");
if (!response.IsSuccessStatusCode)
return null;
var jsonResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TransactContextDto>(jsonResponse,
new JsonSerializerOptions{PropertyNameCaseInsensitive = true});
}
public async Task<Infrastructure.Dto.Inventory.PagedResult<InventoryResponse>> SendGetPageListApiRequest
(User user, InventoryRequest request, string apiEndpoint)
{
var token = await _tokenHelper.GetValidTokenAsync();
try
{
if (string.IsNullOrEmpty(token))
{
return null;
}
request.UserId = user.UserId;
var jsonContent = JsonSerializer.Serialize(request);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
using (var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token))
{
HttpResponseMessage response;
response = await httpClient.PostAsync(apiEndpoint, content);
var jsonResponse = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var result = JsonSerializer.Deserialize<PagedResult<InventoryResponse>>(jsonResponse, options);
return result ?? new PagedResult<InventoryResponse>();
}
else
{
return new PagedResult<InventoryResponse>
{
IsSuccess = false,
ErrorMessage = $"API returned {response.StatusCode}"
};
}
}
}
catch (Exception ex)
{
throw;
}
}
#endregion
#region Get
public async Task<List<InventoryVM>> GetInventoryById(User user, InventoryVM viewModel)
@ -141,7 +210,11 @@ namespace CPRNIMS.Domain.UIServices.Inventory
return await SendGetApiRequest(user, viewModel,
_configuration["LLI:NonInvent:InventoryMgmt:GetLotNoById"]);
}
public async Task<Infrastructure.Dto.Inventory.PagedResult<InventoryResponse>> GetInventory(User user, InventoryRequest reqquest)
{
return await SendGetPageListApiRequest(user, reqquest,
_configuration["LLI:NonInvent:InventoryMgmt:GetInventory"]);
}
public async Task<List<InventoryVM>> GetInventoryByUserId(User user, InventoryVM viewModel)
{
return await SendGetApiRequest(user, viewModel,
@ -153,7 +226,11 @@ namespace CPRNIMS.Domain.UIServices.Inventory
return await SendGetApiRequest(user, viewModel,
_configuration["LLI:NonInvent:InventoryMgmt:GetRequestedItemByUserId"]);
}
public async Task<List<InventoryVM>> GetLotQtyByItem(User user, InventoryVM viewModel)
{
return await SendGetApiRequest(user, viewModel,
_configuration["LLI:NonInvent:InventoryMgmt:GetLotQtyByItem"]);
}
#endregion
#region Post Put
public async Task<InventoryVM> PostPutReqApproval(User user, InventoryVM viewModel)
@ -177,12 +254,6 @@ namespace CPRNIMS.Domain.UIServices.Inventory
return await SendPostApiRequest(user, viewModel,
_configuration["LLI:NonInvent:InventoryMgmt:PostPutReqItems"]);
}
public async Task<List<InventoryVM>> GetLotQtyByItem(User user, InventoryVM viewModel)
{
return await SendGetApiRequest(user, viewModel,
_configuration["LLI:NonInvent:InventoryMgmt:GetLotQtyByItem"]);
}
#endregion
}
}

View File

@ -0,0 +1,170 @@
using CPRNIMS.Domain.UIContracts.Common;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Helper;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.UIServices.Inventory
{
public class RIS : IRIS
{
private readonly IConfiguration _configuration;
private readonly TokenHelper _tokenHelper;
private readonly IApiConfigurationService _apiConfigurationService;
public RIS(IConfiguration configuration, TokenHelper tokenHelper,
IApiConfigurationService apiConfigurationService)
{
_configuration = configuration;
_tokenHelper = tokenHelper;
_apiConfigurationService = apiConfigurationService;
}
#region Get
public async Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct)
{
try
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRIS"]
?? throw new InvalidOperationException("GetRIS endpoint is not configured.");
// ── Build query string — GET request, no body ──
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"pageNumber={request.PageNumber}");
qs.Append($"&pageSize={request.PageSize}");
if (!string.IsNullOrWhiteSpace(request.SearchRISNo))
qs.Append($"&searchRISNo={Uri.EscapeDataString(request.SearchRISNo)}");
if (!string.IsNullOrWhiteSpace(request.SearchItemName))
qs.Append($"&searchItemName={Uri.EscapeDataString(request.SearchItemName)}");
if (!string.IsNullOrWhiteSpace(request.SearchIssuedTo))
qs.Append($"&searchIssuedTo={Uri.EscapeDataString(request.SearchIssuedTo)}");
if (!string.IsNullOrWhiteSpace(request.Discipline))
qs.Append($"&discipline={Uri.EscapeDataString(request.Discipline)}");
if (request.Status.HasValue)
qs.Append($"&status={request.Status.Value}");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
// ── GET, no content body ──
var response = await httpClient.GetAsync(qs.ToString(), ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[GetRISPaged] Error {(int)response.StatusCode}: {json}");
return new RISPagedResponse { Data = [], RecordsTotal = 0 };
}
var result = JsonSerializer.Deserialize<RISPagedResponse>(json, _jsonOptions);
return result ?? new RISPagedResponse();
}
catch (Exception ex)
{
ex.ToString();
throw;
}
}
#endregion
#region Post Put
public async Task<bool> ApproveRIS(ApproveRISRequest request, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:ApproveRIS"]
?? throw new InvalidOperationException("ApproveRIS endpoint is not configured.");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent(JsonSerializer.Serialize(request),Encoding.UTF8,"application/json");
var response = await httpClient.PutAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
return false;
}
return true;
}
public async Task<bool> CancelRIS(CancelRISRequest request, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:CancelRIS"]
?? throw new InvalidOperationException("CancelRIS endpoint is not configured.");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent(
JsonSerializer.Serialize(request),Encoding.UTF8,"application/json");
var response = await httpClient.PutAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
return false;
}
return true;
}
public async Task<RISResponse> CreateRIS(CreateRISRequest request, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:CreateRIS"] ??
throw new InvalidOperationException("CreateRIS endpoint is not configured.");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent(
JsonSerializer.Serialize(request),
Encoding.UTF8,
"application/json");
var response = await httpClient.PostAsync(endpoint, content,ct);
if (!response.IsSuccessStatusCode)
{
return new RISResponse();
}
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ApiResponse<RISResponse>>(json, _jsonOptions);
return result?.data ?? new RISResponse();
}
public Task<RISPagedResponse> GetRISById(int risId, CancellationToken ct)
{
throw new NotImplementedException();
}
private static readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
#endregion
}
}

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Entities.Account;
using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Entities.Common;
@ -22,9 +23,7 @@ namespace CPRNIMS.Infrastructure.Database
public class NonInventoryDbContext : IdentityDbContext<ApplicationUser>
{
public NonInventoryDbContext(DbContextOptions<NonInventoryDbContext> options) : base(options) { }
public virtual DbSet<ItemCode> ItemCodes { get; set; }
public virtual DbSet<ItemList> ItemList { get; set; }
public virtual DbSet<Item> Items { get; set; }
#region Common
public DbSet<Departments> Departments { get; set; }
public DbSet<IdentityRole> IdentityRoles { get; set; }
public DbSet<AuthorizeRoles> AuthorizeRoles { get; set; }
@ -32,39 +31,51 @@ namespace CPRNIMS.Infrastructure.Database
public DbSet<IdentityUserRole<string>> IdentityUserRoles { get; set; }
public DbSet<ForgotPassword> ForgotPasswords { get; set; }
public virtual DbSet<Otps> Otps { get; set; }
public virtual DbSet<ErrorMessage> ErrorMessages { get; set; }
public virtual DbSet<ControllerAccess> ControllerAccess { get; set; }
public virtual DbSet<SMTPCredential> SMTPCredentials { get; set; }
#endregion
#region Item
public virtual DbSet<ItemCode> ItemCodes { get; set; }
public virtual DbSet<ItemList> ItemList { get; set; }
public virtual DbSet<Item> Items { get; set; }
public DbSet<Attachment> Attachments { get; set; }
public virtual DbSet<AttachmentExtension> AttachmentExtensions { get; set; }
public virtual DbSet<AttachmentFileType> AttachmentFileTypes { get; set; }
public virtual DbSet<ItemAttachement> ItemAttachements { get; set; }
public virtual DbSet<ErrorMessage> ErrorMessages { get; set; }
public virtual DbSet<ControllerAccess> ControllerAccess { get; set; }
public virtual DbSet<ItemCategory> ItemCategories { get; set; }
public virtual DbSet<UnitOfMessure> UnitOfMessures { get; set; }
public virtual DbSet<ItemColor> ItemColors { get; set; }
public virtual DbSet<ItemLocalization> ItemLocalizations { get; set; }
public virtual DbSet<ItemCart> ItemCarts { get; set; }
public virtual DbSet<ItemApproval> ItemApprovals { get; set; }
public virtual DbSet<Entities.Inventory.ItemDetail> ItemDetails { get; set; }
#endregion
#region PR
public virtual DbSet<PR> PRs { get; set; }
public virtual DbSet<Approved> Approved { get; set; }
public virtual DbSet<ApprovedPR> ApprovedPrs { get; set; }
public virtual DbSet<DeletedPR> DeletedPRs { get; set; }
public virtual DbSet<SMTPCredential> SMTPCredentials { get; set; }
public virtual DbSet<PRDetails> PRDetails { get; set; }
public virtual DbSet<PRItemList> PRItemLists { get; set; }
public DbSet<PRAttachments> PRAttachments { get; set; }
public DbSet<ProjectCodes> ProjectCodes { get; set; }
public virtual DbSet<Entities.Purchasing.PRList> PRLists { get; set; }
public virtual DbSet<Entities.Canvass.PRList> PRItemList { get; set; }
public virtual DbSet<PRWOCanvass> PRWOCanvasses { get; set; }
public virtual DbSet<Entities.PO.ItemDetail> PRItemDetails { get; set; }
public virtual DbSet<DetailedPRTracking> DetailedPRTrackings { get; set; }
public virtual DbSet<PRTracking> PRTrackings { get; set; }
public virtual DbSet<Dashboard> Dashboards { get; set; }
#endregion
#region Canvassing
public virtual DbSet<NotificationById> NotificationByIds { get; set; }
public virtual DbSet<AlternativeOffer> AlternativeOffers { get; set; }
public virtual DbSet<AlternativeOfferDetails> AlternativeOfferDetails { get; set; }
public virtual DbSet<MyPRWOCanvass> MyPRWOCanvass { get; set; }
public virtual DbSet<Entities.Purchasing.PRList> PRLists { get; set; }
public virtual DbSet<Entities.Canvass.PRList> PRItemList { get; set; }
public virtual DbSet<ItemApproval> ItemApprovals { get; set; }
public virtual DbSet<ForReceiving> ForReceivings { get; set; }
public virtual DbSet<Infrastructure.Entities.Receiving.RRReport> RRReports { get; set; }
public virtual DbSet<ReceivingDetail> ReceivingDetails { get; set; }
public virtual DbSet<RRDetail> RRDetails { get; set; }
public virtual DbSet<ForRR> ForRRs { get; set; }
public virtual DbSet<RR> RRs { get; set; }
public virtual DbSet<Canvass> Canvasses { get; set; }
public DbSet<SupplierForCanvass> SupplierForCanvass { get; set; }
public DbSet<SupplierResponseDto> SupplierResponses { get; set; }
@ -87,6 +98,9 @@ namespace CPRNIMS.Infrastructure.Database
public DbSet<ForAISearchingTagging> ForAISearchingTaggings { get; set; }
public virtual DbSet<CanvassGroupByPRNo> CanvassGroupByPRNos { get; set; }
public virtual DbSet<ForCanvass> ForCanvasses { get; set; }
#endregion
#region PO
public virtual DbSet<ForPO> ForPOs { get; set; }
public virtual DbSet<CreatedPO> CreatedPOs { get; set; }
public virtual DbSet<CustomPO> CustomPOs { get; set; }
@ -94,6 +108,7 @@ namespace CPRNIMS.Infrastructure.Database
public virtual DbSet<ForPOApproval> ForPOApprovals { get; set; }
public virtual DbSet<ApprovedPO> ApprovedPOs { get; set; }
public virtual DbSet<PurchaseOrder> PurchaseOrders { get; set; }
public virtual DbSet<ForPayment> ForPayments { get; set; }
public virtual DbSet<PO> POs { get; set; }
public DbSet<PODetails> PODetails { get; set; }
public virtual DbSet<PRPOSummaryCount> PRPOSummaryCounts { get; set; }
@ -103,29 +118,42 @@ namespace CPRNIMS.Infrastructure.Database
public virtual DbSet<PortOfDischarges> PortOfDischarges { get; set; }
public virtual DbSet<DocRequired> DocRequireds { get; set; }
public virtual DbSet<BiddingApproval> BiddingApprovals { get; set; }
public virtual DbSet<Entities.Receiving.RRDetail> RRDetailss { get; set; }
public virtual DbSet<RRSeries> RRSeries { get; set; }
public virtual DbSet<PRWOCanvass> PRWOCanvasses { get; set; }
public virtual DbSet<ForPayment> ForPayments { get; set; }
public virtual DbSet<Inventory> Inventories { get; set; }
public virtual DbSet<Entities.Inventory.ItemDetail> ItemDetails { get; set; }
public virtual DbSet<Entities.PO.ItemDetail> PRItemDetails { get; set; }
public virtual DbSet<Lot> Lots { get; set; }
public virtual DbSet<LotQtyByItem> LotQtyByItems { get; set; }
public virtual DbSet<RequestItem> RequestItems { get; set; }
public virtual DbSet<RequestItemDetail> RequestItemDetails { get; set; }
public virtual DbSet<NotifUserKey> NotifUserKeys { get; set; }
public virtual DbSet<ItemListForPO> ItemListForPOs { get; set; }
public virtual DbSet<CreatedPOPerSupId> CreatedPOPerSupIds { get; set; }
public virtual DbSet<PRTracking> PRTrackings { get; set; }
public virtual DbSet<Dashboard> Dashboards { get; set; }
public DbSet<IncomingShipment> IncomingShipments { get; set; }
public virtual DbSet<IncomingShipmentDto> IncomingShipmentDtos { get; set; }
public virtual DbSet<PaymentTerm> PaymentTerms { get; set; }
public virtual DbSet<Incoterm> Incoterms { get; set; }
public virtual DbSet<CentralPONo> CentralPONos { get; set; }
public virtual DbSet<DetailedPRTracking> DetailedPRTrackings { get; set; }
public DbSet<IncomingShipment> IncomingShipments { get; set; }
public virtual DbSet<IncomingShipmentDto> IncomingShipmentDtos { get; set; }
#endregion
#region Inventory
public virtual DbSet<Inventory> Inventories { get; set; }
public virtual DbSet<Lot> Lots { get; set; }
public virtual DbSet<LotType> LotTypes { get; set; }
public virtual DbSet<LotQtyByItem> LotQtyByItems { get; set; }
public virtual DbSet<RequestItem> RequestItems { get; set; }
public virtual DbSet<RequestItemDetail> RequestItemDetails { get; set; }
public virtual DbSet<RIS> RIS { get; set; }
public virtual DbSet<MRS> MRS { get; set; }
public virtual DbSet<Discipline> Disciplines { get; set; }
public virtual DbSet<InventTrans> InventTrans { get; set; }
public virtual DbSet<InventTransDetail> InventTransDetails { get; set; }
public DbSet<InventoryByIdResponse> InventoryByIdResponses { get; set; }
#endregion
#region RR
public virtual DbSet<ForReceiving> ForReceivings { get; set; }
public virtual DbSet<Infrastructure.Entities.Receiving.RRReport> RRReports { get; set; }
public virtual DbSet<ReceivingDetail> ReceivingDetails { get; set; }
public virtual DbSet<RRDetail> RRDetails { get; set; }
public virtual DbSet<ForRR> ForRRs { get; set; }
public virtual DbSet<RR> RRs { get; set; }
public virtual DbSet<Entities.Receiving.RRDetail> RRDetailss { get; set; }
public virtual DbSet<RRSeries> RRSeries { get; set; }
#endregion
#region Automation Part
public virtual DbSet<AllForCanvass> AllForCanvasses { get; set; }
@ -236,6 +264,52 @@ namespace CPRNIMS.Infrastructure.Database
{
b.ToTable("UserRoles");
});
modelBuilder.Entity<Inventory>()
.HasOne(i => i.Lot)
.WithMany()
.HasForeignKey(i => i.LotId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<InventTrans>(e =>
{
e.HasMany(r => r.InventTransDetails)
.WithOne()
.HasForeignKey(t => t.InventTransId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<InventTransDetail>(e =>
{
e.HasOne(i => i.PRDetails)
.WithMany()
.HasForeignKey(t => t.PRDetailId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<PRDetails>(e =>
{
e.HasOne(i => i.PRs)
.WithMany()
.HasForeignKey(t => t.PRId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<RIS>(e => {
e.HasOne(r => r.Inventory)
.WithMany()
.HasForeignKey(r => r.InventoryId)
.OnDelete(DeleteBehavior.Restrict);
e.HasOne(r => r.Discipline)
.WithMany()
.HasForeignKey(r => r.DisciplineId);
});
modelBuilder.Entity<MRS>(e => {
e.HasOne(m => m.RIS)
.WithMany(r => r.MaterialReturns)
.HasForeignKey(m => m.RISId)
.OnDelete(DeleteBehavior.Restrict);
});
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Common
{
public class ApiResponse<T>
{
public bool success { get; set; }
public string? message { get; set; }
public T? data { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class DisciplineDto
{
public byte DisciplineId { get; set; }
public string DisciplineName { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class MRSFilterDto
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 12;
public string? SearchMRSNo { get; set; }
public long? RISId { get; set; }
public short? Status { get; set; }
public DateTime? DateFrom { get; set; }
public DateTime? DateTo { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class MRSPagedResult
{
public IEnumerable<MRSResponse> Data { get; set; } = [];
public int RecordsTotal { get; set; }
public List<string> DepartmentList { get; set; } = new List<string>();
public List<string> DisciplineList { get; set; } = new List<string>();
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class PagedResult<T>
{
public List<T> Data { get; set; } = new List<T>();
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public List<string> DepartmentList { get; set; } = new List<string>();
public List<string> CategoryList { get; set; } = new List<string>();
public bool IsSuccess { get; set; } = true;
public string? ErrorMessage { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class RISFilterDto
{
public string? SearchRISNo { get; set; }
public string? SearchItemName { get; set; }
public string? SearchIssuedTo { get; set; }
public string? Discipline { get; set; }
public short? Status { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 12;
public DateTime? DateFrom { get; set; }
public DateTime? DateTo { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class RISPagedResult
{
public IEnumerable<RISResponse> Data { get; set; } = [];
public int RecordsTotal { get; set; }
public IEnumerable<string> DepartmentList { get; set; } = [];
public IEnumerable<DisciplineDto> DisciplineList { get; set; } = [];
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class RISReferenceDto
{
public long RISId { get; set; }
public string RISNo { get; set; } = string.Empty;
public decimal QtyIssued { get; set; }
public decimal TotalReturned { get; set; }
public decimal QtyAvailableToReturn => QtyIssued - TotalReturned;
public string DisciplineName { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
{
public class ApproveRISRequest
{
public long RISId { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
{
public class CancelRISRequest
{
public long RISId { get; set; }
public string Reason { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
{
public class CreateMRSRequest
{
public long RISId { get; set; }
public decimal QtyReturned { get; set; }
public string ReturnedBy { get; set; } = string.Empty;
public string? Condition { get; set; }
public string? Remarks { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
{
public class CreateRISRequest
{
public int InventoryId { get; set; }
public long PRDetailId { get; set; }
public string IssuedTo { get; set; } = string.Empty;
public byte DisciplineId { get; set; }
public decimal QtyIssued { get; set; }
public string? Remarks { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
{
public class InventoryRequest
{
public string? SearchPRNo { get; set; }
public string? SearchItemName { get; set; }
public string? SearchDept { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public byte MessCode { get; set; }
public string? SearchProjectCode { get; set; }
public string? SearchItemNo { get; set; }
public string? UserId { get; set; }
public int InventoryId { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
{
public class RISPagedRequest
{
public string? SearchRISNo { get; set; }
public string? SearchItemName { get; set; }
public string? SearchIssuedTo { get; set; }
public string? Discipline { get; set; }
public short? Status { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 12;
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
{
public class InventoryByIdResponse
{
[Key]
public int InventoryId { get; set; }
public decimal QtyIn { get; set; }
public decimal QtyOut { get; set; }
public decimal QtyOnHand { get; set; }
public string? LotNo { get; set; }
public string? ProjectCode { get; set; }
public string? UserId { get; set; }
public string? ItemName { get; set; }
public string? ItemDescription { get; set; }
public string? ItemCategoryName { get; set; }
public string? Department { get; set; }
public DateTime CreatedDate { get; set; }
public long ItemNo { get; set; }
public long PRNo { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
{
public class InventoryResponse
{
[Key]
public int InventoryId { get; set; }
public decimal QtyIn { get; set; }
public decimal QtyOut { get; set; }
public decimal QtyOnHand { get; set; }
public string? LotNo { get; set; }
public string? ProjectCode { get; set; }
public string? UserId { get; set; }
public string? ItemName { get; set; }
public string? ItemDescription { get; set; }
public string? ItemCategoryName { get; set; }
public string? Department { get; set; }
public DateTime CreatedDate { get; set; }
public long ItemNo { get; set; }
public long PRNo { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
{
public class MRSResponse
{
public long MRSId { get; set; }
public string MRSNo { get; set; } = string.Empty;
public long RISId { get; set; }
public string RISNo { get; set; } = string.Empty;
public int InventoryId { get; set; }
public string ItemName { get; set; } = string.Empty;
public string ReturnedBy { get; set; } = string.Empty;
public decimal QtyReturned { get; set; }
public string? Condition { get; set; }
public string? Remarks { get; set; }
public short Status { get; set; }
public string StatusLabel => Status switch { 0 => "Draft", 1 => "Approved", 2 => "Cancelled", _ => "Unknown" };
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string? ApprovedBy { get; set; }
public DateTime? ApprovedDate { get; set; }
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
{
public class RISPagedResponse
{
public List<RISListItem> Data { get; set; } = [];
public int RecordsTotal { get; set; }
public List<string> DepartmentList { get; set; } = [];
public List<DisciplineItem> DisciplineList { get; set; } = [];
}
public class RISListItem
{
public long RISId { get; set; }
public string RISNo { get; set; } = string.Empty;
public int InventoryId { get; set; }
public string? ItemName { get; set; }
public long ItemNo { get; set; }
public string? IssuedTo { get; set; }
public string? DisciplineName { get; set; }
public decimal QtyIssued { get; set; }
public decimal TotalReturned { get; set; }
public string? Remarks { get; set; }
public short Status { get; set; }
public string? StatusLabel { get; set; }
public string? CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string? ApprovedBy { get; set; }
public DateTime? ApprovedDate { get; set; }
public decimal MRSCount { get; set; }
}
public class DisciplineItem
{
public byte DisciplineId { get; set; }
public string DisciplineName { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
{
public class RISResponse
{
public long RISId { get; set; }
public string RISNo { get; set; } = string.Empty;
public int InventoryId { get; set; }
public string ItemName { get; set; } = string.Empty;
public long ItemNo { get; set; }
public string? LotNo { get; set; }
public string IssuedTo { get; set; } = string.Empty;
public string DisciplineName { get; set; } = string.Empty;
public string? Message { get; set; }
public byte DisciplineId { get; set; }
public decimal QtyIssued { get; set; }
public string? Remarks { get; set; }
public short Status { get; set; }
public string StatusLabel => Status switch { 0 => "Draft", 1 => "Approved", 2 => "Cancelled", _ => "Unknown" };
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string? ApprovedBy { get; set; }
public DateTime? ApprovedDate { get; set; }
public decimal MRSCount { get; set; }
public decimal TotalReturned { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class TransactContextDto
{
public int InventoryId { get; set; }
public string ItemName { get; set; } = string.Empty;
public long ItemNo { get; set; }
public long PRNo { get; set; }
public string? LotNo { get; set; }
public string? Department { get; set; }
public decimal QtyOnHand { get; set; }
public decimal QtyIn { get; set; }
public decimal QtyOut { get; set; }
public IEnumerable<DisciplineDto> Disciplines { get; set; } = [];
public IEnumerable<RISReferenceDto> OpenRISList { get; set; } = [];
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Inventory
{
public class Discipline
{
[Key]
public byte DisciplineId { get; set; }
[Required, MaxLength(100)]
public string DisciplineName { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Inventory
{
[Table("InventTrans")]
public class InventTrans
{
[Key]
public int InventTransId { get; set; }
public int InventoryId { get; set; }
public long RRNo { get; set; }
public string CreatedBy { get; set; }=string.Empty;
public bool IsActive { get; set; }
public ICollection<InventTransDetail> InventTransDetails { get; set; } = [];
}
}

View File

@ -0,0 +1,28 @@
using CPRNIMS.Infrastructure.Entities.Purchasing;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Inventory
{
[Table("InventTransDetail")]
public class InventTransDetail
{
[Key]
public long InventTransDetailId { get; set; }
public int InventTransId { get; set; }
public byte TransTypeId { get; set; }
public long RRDetailId { get; set; }
public decimal QtyIn { get; set; }
public decimal QtyOut { get; set; }
public DateTime CreatedDate { get; set; }
public string? Remarks { get; set; }
public bool IsActive { get; set; }
public long PRDetailId { get; set; }
public PRDetails? PRDetails { get; set; }
}
}

View File

@ -1,12 +1,15 @@
using System;
using CPRNIMS.Infrastructure.Entities.Account;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Inventory
{
[Table("Inventory")]
public class Inventory
{
[Key]
@ -14,15 +17,12 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public decimal QtyIn { get; set; }
public decimal QtyOut { get; set; }
public decimal QtyOnHand { get; set; }
public decimal RemainingQty { get; set; }
public string? LotNo { get; set; }
public string? UserId { get; set; }
public string? ItemName { get; set; }
public string? ItemDescription { get; set; }
public string? ItemCategoryName { get; set; }
public string? Department { get; set; }
public DateTime CreatedDate { get; set; }
public bool IsActive { get; set; }
public long ItemNo { get; set; }
public int LotId { get; set; }
public Lot? Lot { get; set; }
public ICollection<InventTrans> InventTrans { get; set; } = [];
public ApplicationUser? User { get; set; }
}
}

View File

@ -17,8 +17,8 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public int LotId { get; set; }
public int InventoryId { get; set; }
public string? LotName { get; set; }
public string? LotTypeName { get; set; }
public byte LotTypeId { get; set; }
public string? UserId { get; set; }
public LotType? LotType { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Inventory
{
[Table("LotType")]
public class LotType
{
[Key]
public int LotTypeId { get; set; }
public string? LotTypeName { get; set; }
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Inventory
{
[Table("MRS")]
public class MRS
{
[Key]
public long MRSId { get; set; }
[Required, MaxLength(50)]
public string MRSNo { get; set; } = string.Empty;
public long RISId { get; set; }
public int InventoryId { get; set; }
[Required, MaxLength(450)]
public string ReturnedBy { get; set; } = string.Empty;
public decimal QtyReturned { get; set; }
[MaxLength(100)]
public string? Condition { get; set; }
[MaxLength(500)]
public string? Remarks { get; set; }
public short Status { get; set; } = 0;
[Required, MaxLength(450)]
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; } = DateTime.Now;
public string? ApprovedBy { get; set; }
public DateTime? ApprovedDate { get; set; }
public string? CanceledBy { get; set; }
public DateTime? CanceledDate { get; set; }
public string? Reason { get; set; }
public RIS RIS { get; set; } = null!;
public Inventory Inventory { get; set; } = null!;
}
}

View File

@ -0,0 +1,48 @@
using CPRNIMS.Infrastructure.Entities.Purchasing;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Inventory
{
[Table("RIS")]
public class RIS
{
[Key]
public long RISId { get; set; }
[Required, MaxLength(50)]
public string RISNo { get; set; } = string.Empty;
public int InventoryId { get; set; }
public long PRDetailId { get; set; }
[Required, MaxLength(450)]
public string IssuedTo { get; set; } = string.Empty;
public byte DisciplineId { get; set; }
public decimal QtyIssued { get; set; }
[MaxLength(500)]
public string? Remarks { get; set; }
public short Status { get; set; } = 0;
[Required, MaxLength(450)]
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; } = DateTime.Now;
public string? ApprovedBy { get; set; }
public DateTime? ApprovedDate { get; set; }
public string? CanceledBy { get; set; }
public DateTime? CanceledDate { get; set; }
public string? Reason { get; set; }
public Inventory Inventory { get; set; } = null!;
public PRDetails PRDetail { get; set; } = null!;
public Discipline Discipline { get; set; } = null!;
public ICollection<MRS> MaterialReturns { get; set; } = [];
}
}

View File

@ -15,6 +15,7 @@ namespace CPRNIMS.Infrastructure.Entities.Purchasing
{
[Key]
public long PRDetailsId { get; set; }
public long PRId { get; set; }
public string ItemName { get; set; }=string.Empty;
public string ItemDescription { get; set; } = string.Empty;
public short Status { get; set; }
@ -22,5 +23,6 @@ namespace CPRNIMS.Infrastructure.Entities.Purchasing
public long ItemNo { get; set; }
public decimal Qty { get; set; }
public bool IsSearched { get; set; }
public PR? PRs { get; set; }
}
}

View File

@ -10,15 +10,11 @@ namespace CPRNIMS.Infrastructure.Models
{
public class RegisterModel : ApplicationUser
{
[EmailAddress]
[Required(ErrorMessage = "Email is required")]
public string Email { get; set; }
public string Role { get; set; }
public string? Role { get; set; }
[Required(ErrorMessage = "Password is required")]
public string? Password { get; set; }
//public string? UserId { get; set; }
[Required(ErrorMessage = "ClaimType is required")]
public string? ClaimType { get; set; }
[Required(ErrorMessage = "ClaimValue is required")]

View File

@ -13,7 +13,5 @@ namespace CPRNIMS.Infrastructure.ViewModel.Account
public int Id { get; set; }
public string? Message { get; set; }
public string? Status { get; set; }
public string? Token { get; internal set; }
}
}

View File

@ -70,5 +70,12 @@ namespace CPRNIMS.Infrastructure.ViewModel.Inventory
public DateTime DateTo { get; set; }
public bool IsSorting { get; set; }
public string? URL { get; set; }
public string SearchPRNo { get; set; } = "";
public string SearchItemName { get; set; } = "";
public string SearchDept { get; set; } = "";
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public byte MessCode { get; set; }
public string? SearchProjectCode { get; set; }
}
}

View File

@ -147,6 +147,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Sql\Phase 5\" />
<Folder Include="wwwroot\Content\Uploads\PRAttachment\" />
</ItemGroup>

View File

@ -152,6 +152,8 @@ namespace CPRNIMS.WebApi.Common
services.AddScoped<IItem, Domain.Services.Items.Item>();
services.AddScoped<IPRequest, Domain.Services.PR.PRequest>();
services.AddScoped<ICanvass, Canvass>();
services.AddScoped<IRIS, RIS>();
services.AddScoped<IMRS, MRS>();
#region Automation using LLM
services.AddHttpClient<SupplierSearchService>();

View File

@ -1,6 +1,8 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Domain.Services;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.PR;
using CPRNIMS.WebApi.Controllers.Base;
using Microsoft.AspNetCore.Mvc;
@ -10,16 +12,23 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
public class InventoryMgmtController : BaseController
{
private readonly IInventory _inventory;
public InventoryMgmtController(ErrorMessageService errorMessageService,
IWebHostEnvironment webHostEnvironment, IConfiguration configuration,
IInventory inventory) :
base(errorMessageService, webHostEnvironment,configuration)
{
_inventory = inventory;
}
=> _inventory = inventory;
#region Get
[HttpGet("GetTransactContext")]
public async Task<IActionResult> GetTransactContext([FromQuery] int inventoryId, CancellationToken ct)
{
var ctx = await _inventory.GetTransactContextAsync(inventoryId, ct);
if (ctx == null)
return NotFound(new { success = false, message = "Inventory record not found." });
return Ok(ctx);
}
[HttpPost("GetInventoryByUserId")]
public async Task<IActionResult> GetInventoryByUserId(InventoryDto itemCodeDto)
{
@ -28,83 +37,45 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
[HttpPost("GetRequestedItemByUserId")]
public async Task<IActionResult> GetRequestedItemByUserId(InventoryDto itemCodeDto)
{
try
{
var allPR = await _inventory.GetRequestedItemByUserId(itemCodeDto);
var allPR = await _inventory.GetRequestedItemByUserId(itemCodeDto);
return Ok(allPR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "GetRequestedItemByUserId", "WebApi");
throw;
}
return Ok(allPR);
}
//GetRequestedItemByUserId
[HttpPost("GetInventoryById")]
public async Task<IActionResult> GetInventoryById(InventoryDto itemCodeDto)
[HttpPost("GetInventory")]
public async Task<IActionResult> GetInventory(InventoryRequest request, CancellationToken ct)
{
try
{
var allPR = await _inventory.GetInventoryById(itemCodeDto);
return Ok(allPR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "GetInventoryById", "WebApi");
throw;
}
var result = await _inventory.GetInventory(request, ct);
return Ok(result);
}
[HttpPost("GetInventoryById")]
public async Task<IActionResult> GetInventoryById(InventoryRequest request, CancellationToken ct)
{
var result = await _inventory.GetInventoryById(request, ct);
return Ok(result);
}
[HttpPost("GetLotNo")]
public async Task<IActionResult> GetLotNo(InventoryDto itemCodeDto)
{
try
{
var allPR = await _inventory.GetLotNo(itemCodeDto);
var allPR = await _inventory.GetLotNo(itemCodeDto);
return Ok(allPR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "GetInventoryById", "WebApi");
throw;
}
return Ok(allPR);
}
[HttpPost("GetLotNoById")]
public async Task<IActionResult> GetLotNoById(InventoryDto itemCodeDto)
{
try
{
var allPR = await _inventory.GetLotNoById(itemCodeDto);
var allPR = await _inventory.GetLotNoById(itemCodeDto);
return Ok(allPR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "GetInventoryById", "WebApi");
throw;
}
return Ok(allPR);
}
[HttpPost("GetLotQtyByItem")]
public async Task<IActionResult> GetLotQtyByItem(InventoryDto itemCodeDto)
{
try
{
var allPR = await _inventory.GetLotQtyByItem(itemCodeDto);
var allPR = await _inventory.GetLotQtyByItem(itemCodeDto);
return Ok(allPR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "GetLotQtyByItem", "WebApi");
throw;
}
return Ok(allPR);
}
#endregion
@ -112,66 +83,28 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
[HttpPost("PostPutReqItems")]
public async Task<IActionResult> PostPutReqItems(InventoryDto InventoryDto)
{
try
{
var pR = await _inventory.PostPutReqItems(InventoryDto);
var pR = await _inventory.PostPutReqItems(InventoryDto);
return Ok(pR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "PostPutReqItems", "WebApi");
throw;
}
return Ok(pR);
}
[HttpPost("PostPutReqApproval")]
public async Task<IActionResult> PostPutReqApproval(InventoryDto InventoryDto)
{
try
{
var pR = await _inventory.PostPutReqApproval(InventoryDto);
var pR = await _inventory.PostPutReqApproval(InventoryDto);
return Ok(pR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "PostPutReqApproval", "WebApi");
throw;
}
return Ok(pR);
}
[HttpPost("PostPutLotNo")]
public async Task<IActionResult> PostPutLotNo(InventoryDto InventoryDto)
{
try
{
var pR = await _inventory.PostPutLotNo(InventoryDto);
return Ok(pR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "PostPutReqApproval", "WebApi");
throw;
}
var pR = await _inventory.PostPutLotNo(InventoryDto);
return Ok(pR);
}
[HttpPost("PostPutLotBin")]
public async Task<IActionResult> PostPutLotBin(InventoryDto InventoryDto)
{
try
{
var pR = await _inventory.PostPutLotBin(InventoryDto);
return Ok(pR);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(message + "PostPutLotBin", "WebApi");
throw;
}
var pR = await _inventory.PostPutLotBin(InventoryDto);
return Ok(pR);
}
#endregion
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApi.Controllers.Inventory
{
public class MRSMgmtController : Controller
{
public IActionResult Index()
{
return View();
}
}
}

View File

@ -0,0 +1,76 @@
using Azure.Core;
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.WebApi.Security;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace CPRNIMS.WebApi.Controllers.Inventory
{
[Route("api/[controller]")]
[ApiController]
public class RISMgmtController : ControllerBase
{
private readonly IRIS _risRepo;
public RISMgmtController(IRIS risRepo) {
_risRepo = risRepo;
}
[HttpGet("GetRIS")]
public async Task<IActionResult> GetRIS([FromQuery] RISFilterDto filter, CancellationToken ct)
{
var result = await _risRepo.GetPagedAsync(filter,ct);
return Ok(result);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetRISById(long id, CancellationToken ct)
{
return Ok(await _risRepo.GetByIdAsync(id, ct));
}
[HttpPost]
public async Task<IActionResult> CreateRIS([FromBody] CreateRISRequest request,CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
if (!ModelState.IsValid)
return BadRequest(new
{
success = false,
message = "Validation failed.",
errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
});
var ris = await _risRepo.CreateAsync(request, currentUser.UserName,ct);
return Ok(new
{
success = true,
message = $"RIS {ris.RISNo} created successfully.",
data= ris
});
}
[HttpPut("ApproveRIS")]
public async Task<IActionResult> ApproveRIS([FromBody] ApproveRISRequest request, CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if(currentUser == null)
return BadRequest();
await _risRepo.ApproveAsync(request, currentUser.UserName, ct);
return Ok(new { success = true, message = "RIS approved." });
}
[HttpPut("CancelRIS")]
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request, CancellationToken ct)
{
await _risRepo.CancelAsync(request, ct);
return Ok(new { success = true, message = "RIS cancelled and inventory restored." });
}
}
}

View File

@ -0,0 +1,117 @@
USE [CPRNIMS]
GO
/****** Object: StoredProcedure [dbo].[GetInventory] Script Date: 6/11/2026 9:29:29 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[GetInventory]
(
@UserId VARCHAR(450)='89da2977-c70f-4df9-94d4-9a610aa999ea',
@SearchPRNo VARCHAR(50) = '',
@SearchItemNo VARCHAR(50) = '',
@SearchItemName VARCHAR(100) = '',
@SearchDept VARCHAR(200) = '',
@SearchProjectCode VARCHAR(200) = '',
@PageNumber INT = 1,
@PageSize INT = 10
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Offset INT = (@PageNumber - 1) * @PageSize;
-- ── 1. Full department list — unaffected by any filter ──────────────
SELECT DISTINCT D.Department
FROM dbo.Departments D
INNER JOIN dbo.Users U
ON U.DepartmentId = D.DepartmentId
INNER JOIN dbo.Inventory IV
ON IV.UserId = U.Id
ORDER BY D.Department;
-- ── 2. Filtered temp table ───────────────────────────────────────────
IF OBJECT_ID('tempdb..#Inventory') IS NOT NULL
DROP TABLE #Inventory;
SELECT
IV.InventoryId
, IV.ItemNo
, IV.QtyOnHand
, IV.QtyIn
, IV.QtyOut
, L.LotName AS LotNo
, IV.UserId
, PR.PRNo
, PRD.ItemName
, D.Department
, PRD.ItemDescription
, ICAT.ItemCategoryName
, RRD.RemainingQty
, ITD.CreatedDate
,COALESCE(PCD.ProjectCode,'N/A') ProjectCode
INTO #Inventory
FROM dbo.Inventory IV
INNER JOIN dbo.InventTrans IT
ON IV.InventoryId = IT.InventoryId
INNER JOIN dbo.InventTransDetail ITD
ON IT.InventTransId = ITD.InventTransId
INNER JOIN dbo.PRDetails PRD
ON ITD.PRDetailId = PRD.PRDetailsId
AND PRD.IsActive = 1
INNER JOIN dbo.RRDetails RRD
ON PRD.PRDetailsId = RRD.PRDetailId
AND RRD.IsActive = 1
INNER JOIN dbo.ItemCategories ICAT
ON PRD.ItemCategoryId = ICAT.ItemCategoryId
INNER JOIN dbo.Lot L
ON IV.LotId = L.LotId
INNER JOIN dbo.LotType LT
ON L.LotTypeId = LT.LotTypeId
INNER JOIN dbo.PR PR
ON PR.UserId = IV.UserId
AND PR.IsActive = 1
INNER JOIN dbo.ProjectCodes PC
ON PR.ProjectCodeId = PC.ProjectCodeId
INNER JOIN dbo.Users U
ON IV.UserId = U.Id
INNER JOIN dbo.Departments D
ON U.DepartmentId = D.DepartmentId
INNER JOIN dbo.ProjectCodes PCD
ON PR.ProjectCodeId = PCD.ProjectCodeId
WHERE ITD.TransTypeId=2 AND IV.IsActive=1 AND ITD.IsActive=1 AND
(@SearchPRNo = '' OR PR.PRNo = @SearchPRNo)
AND (@SearchItemNo = '' OR PRD.ItemNo = @SearchItemNo)
AND (@SearchItemName = '' OR PRD.ItemName LIKE '%' + @SearchItemName + '%')
AND (@SearchDept = '' OR D.Department LIKE '%' + @SearchDept + '%')
AND (@SearchProjectCode = '' OR PC.ProjectCode LIKE '%' + @SearchProjectCode + '%')
GROUP BY
IV.InventoryId
, IV.ItemNo
, IV.QtyOnHand
, IV.QtyIn
, IV.QtyOut
, L.LotName
, IV.UserId
, PR.PRNo
, PRD.ItemName
, D.Department
, PRD.ItemDescription
, ICAT.ItemCategoryName
, RRD.RemainingQty
, ITD.CreatedDate
,PCD.ProjectCode;
-- ── 3. Total count of filtered results ──────────────────────────────
SELECT COUNT(*) AS TotalCount
FROM #Inventory;
-- ── 4. Paged filtered results ────────────────────────────────────────
SELECT *
FROM #Inventory
ORDER BY CreatedDate DESC
OFFSET @Offset ROWS
FETCH NEXT @PageSize ROWS ONLY;
DROP TABLE #Inventory;
END

View File

@ -0,0 +1,61 @@
-- ── Disciplines lookup (Trade / Matrix / Structural / Architectural) ──
CREATE TABLE [dbo].[Disciplines] (
[DisciplineId] TINYINT IDENTITY(1,1) PRIMARY KEY NOT NULL,
[DisciplineName] VARCHAR(100) NOT NULL
);
INSERT INTO [dbo].[Disciplines] ([DisciplineName]) VALUES
('Trade'),
('Matrix'),
('Structural'),
('Architectural');
-- ── RIS: Return Issuance Slip ─────────────────────────────────────
CREATE TABLE [dbo].[RIS] (
[RISId] BIGINT IDENTITY(1,1) PRIMARY KEY NOT NULL,
[RISNo] VARCHAR(50) NOT NULL, -- generated slip number
[InventoryId] INT NOT NULL,
[PRDetailId] BIGINT NOT NULL,
[IssuedTo] VARCHAR(450) NOT NULL, -- UserId receiving items
[DisciplineId] TINYINT NOT NULL, -- Trade/Matrix/Structural/Architectural
[QtyIssued] DECIMAL(14,2) NOT NULL,
[Remarks] VARCHAR(500) NULL,
[Status] SMALLINT NOT NULL DEFAULT 0, -- 0=Draft,1=Approved,2=Cancelled
[CreatedBy] VARCHAR(450) NOT NULL,
[CreatedDate] DATETIME NOT NULL DEFAULT GETDATE(),
[ApprovedBy] VARCHAR(450) NULL,
[ApprovedDate] DATETIME NULL,
CanceledBy VARCHAR(50) NULL,
CanceledDate DATETIME NULL,
Reason VARCHAR(150) NULL,
FOREIGN KEY ([InventoryId]) REFERENCES [Inventory]([InventoryId]),
FOREIGN KEY ([PRDetailId]) REFERENCES [PRDetails]([PRDetailsId]),
FOREIGN KEY ([DisciplineId]) REFERENCES [Disciplines]([DisciplineId])
);
-- ── MRS: Material Return Slip ─────────────────────────────────────
CREATE TABLE [dbo].[MRS] (
[MRSId] BIGINT IDENTITY(1,1) PRIMARY KEY NOT NULL,
[MRSNo] VARCHAR(50) NOT NULL,
[RISId] BIGINT NOT NULL, -- must reference an original issuance
[InventoryId] INT NOT NULL,
[ReturnedBy] VARCHAR(450) NOT NULL,
[QtyReturned] DECIMAL(14,2) NOT NULL,
[Condition] VARCHAR(100) NULL, -- Good / Damaged / Partial
[Remarks] VARCHAR(500) NULL,
[Status] SMALLINT NOT NULL DEFAULT 0,
[CreatedBy] VARCHAR(450) NOT NULL,
[CreatedDate] DATETIME NOT NULL DEFAULT GETDATE(),
[ApprovedBy] VARCHAR(450) NULL,
[ApprovedDate] DATETIME NULL,
CanceledBy VARCHAR(50) NULL,
CanceledDate DATETIME NULL,
Reason VARCHAR(150) NULL,
FOREIGN KEY ([RISId]) REFERENCES [RIS]([RISId]),
FOREIGN KEY ([InventoryId]) REFERENCES [Inventory]([InventoryId])
);
INSERT INTO [dbo].[TransTypes]
(TransTypeName)
VALUES
('RIS'),('MRS')

View File

@ -87,6 +87,7 @@ namespace CPRNIMS.WebApps.Common
builder.Services.AddTransient<IAccount, Account>();
builder.Services.AddTransient<ICaptchaService, CaptchaService>();
builder.Services.AddScoped<ErrorLogHelper>();
builder.Services.AddScoped<IRIS, RIS>();
}
private static void AddSessionAndAuthentication(WebApplicationBuilder builder)

View File

@ -1,13 +1,12 @@
using CPRNIMS.Domain.UIContracts.Account;
using Azure.Core;
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.ViewModel.Account;
using CPRNIMS.Infrastructure.ViewModel.Finance;
using CPRNIMS.Infrastructure.ViewModel.Inventory;
using CPRNIMS.WebApps.Controllers.Base;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CPRNIMS.WebApps.Controllers.Inventory
{
@ -23,61 +22,30 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
_inventory = inventory;
}
#region Get
public async Task<IActionResult> GetTransactContext(int inventoryId)
{
var response = await _inventory.GetTransactContextAsync(inventoryId);
return GetResponse(response);
}
public async Task<IActionResult> GetLotQtyByItem(InventoryVM viewModels)
{
try
{
response = await _inventory.GetLotQtyByItem(GetUser(), viewModels);
return GetResponse(response);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
}
response = await _inventory.GetLotQtyByItem(GetUser(), viewModels);
return GetResponse(response);
}
public async Task<IActionResult> GetLotNo(InventoryVM viewModels)
{
try
{
response = await _inventory.GetLotNo(GetUser(), viewModels);
return GetResponse(response);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
}
response = await _inventory.GetLotNo(GetUser(), viewModels);
return GetResponse(response);
}
public async Task<IActionResult> GetLotNoById(InventoryVM viewModels)
{
try
{
response = await _inventory.GetLotNoById(GetUser(), viewModels);
return GetResponse(response);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
}
response = await _inventory.GetLotNoById(GetUser(), viewModels);
return GetResponse(response);
}
public async Task<IActionResult> GetInventoryById(InventoryVM viewModels)
{
try
{
response = await _inventory.GetInventoryById(GetUser(), viewModels);
return GetResponse(response);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
}
response = await _inventory.GetInventoryById(GetUser(), viewModels);
return GetResponse(response);
}
public async Task<IActionResult> GetInventoryByUserId(InventoryVM viewModels)
{
@ -86,99 +54,88 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
}
public async Task<IActionResult> GetRequestedItemByUserId(InventoryVM viewModels)
{
try
response = await _inventory.GetRequestedItemByUserId(GetUser(), viewModels);
return GetResponse(response);
}
[HttpGet]
public async Task<IActionResult> GetInventory(string searchPRNo = "", string searchItemNo = "",string searchItemName = "",
string searchDept = "",string searchProjectCode="", int pageNumber = 1, int pageSize = 10)
{
var request = new InventoryRequest
{
response = await _inventory.GetRequestedItemByUserId(GetUser(), viewModels);
return GetResponse(response);
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
SearchPRNo = searchPRNo,
SearchItemNo = searchItemNo,
SearchItemName = searchItemName,
SearchDept = searchDept,
SearchProjectCode = searchProjectCode,
PageNumber = pageNumber,
PageSize = pageSize
};
throw;
}
var result = await _inventory.GetInventory(GetUser(), request);
int draw = int.TryParse(Request.Query["draw"], out int d) ? d : 1;
return Json(new
{
draw = draw,
recordsTotal = result.TotalCount,
recordsFiltered = result.TotalCount,
data = result.Data,
departmentList=result.DepartmentList,
});
}
#endregion
#region POST PUT
public async Task<IActionResult> PostPutLotNo(InventoryVM viewModel)
{
try
{
var postPutItem = await _inventory.PostPutLotNo(GetUser(), viewModel);
var postPutItem = await _inventory.PostPutLotNo(GetUser(), viewModel);
if (postPutItem.StatusResponse != "Error")
{
return Json(new { success = true });
}
return Json(new { success = false, Response = postPutItem.Message });
}
catch (Exception ex)
if (postPutItem.StatusResponse != "Error")
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
return Json(new { success = true });
}
return Json(new { success = false, Response = postPutItem.Message });
}
public async Task<IActionResult> PostPutLotBin(InventoryVM viewModel)
{
try
var postPutItem = await _inventory.PostPutLotBin(GetUser(), viewModel);
if (postPutItem.StatusResponse != "Error")
{
var postPutItem = await _inventory.PostPutLotBin(GetUser(), viewModel);
if (postPutItem.StatusResponse != "Error")
{
return Json(new { success = true });
}
return Json(new { success = false, Response = postPutItem.Message });
return Json(new { success = true });
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
}
return Json(new { success = false, Response = postPutItem.Message });
}
public async Task<IActionResult> PostPutReqApproval(InventoryVM viewModel)
{
try
{
var postPutItem = await _inventory.PostPutReqApproval(GetUser(), viewModel);
var postPutItem = await _inventory.PostPutReqApproval(GetUser(), viewModel);
if (postPutItem.StatusResponse != "Error")
{
return Json(new { success = true });
}
return Json(new { success = false, Response = postPutItem.Message });
}
catch (Exception ex)
if (postPutItem.StatusResponse != "Error")
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
return Json(new { success = true });
}
return Json(new { success = false, Response = postPutItem.Message });
}
public async Task<IActionResult> PostPutReqItems(InventoryVM viewModel)
{
try
{
var postPutItem = await _inventory.PostPutReqItems(GetUser(), viewModel);
var postPutItem = await _inventory.PostPutReqItems(GetUser(), viewModel);
if (postPutItem.StatusResponse != "Error")
{
return Json(new { success = true });
}
return Json(new { success = false, Response = postPutItem.Message });
}
catch (Exception ex)
if (postPutItem.StatusResponse != "Error")
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
throw;
return Json(new { success = true });
}
return Json(new { success = false, Response = postPutItem.Message });
}
#endregion
#region Views
public IActionResult GetInventoryTabPage(int id)
{
return ViewComponent("InventoryTabPage", new { inventoryTabPageId = id });
}
public async Task<IActionResult> Inventory()
{
return await IsAuthenTicated();

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApps.Controllers.Inventory
{
public class MRSController : Controller
{
public IActionResult Index()
{
return View();
}
}
}

View File

@ -0,0 +1,101 @@
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.WebApps.Controllers.Base;
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApps.Controllers.Inventory
{
public class RISMgmtController : BaseMethod
{
private readonly IRIS _ris;
public RISMgmtController(ErrorLogHelper errorMessageService,
IWebHostEnvironment webHostEnvironment, TokenHelper tokenHelper
, IRIS ris, IAccount account)
: base(errorMessageService, webHostEnvironment, tokenHelper, account)
{
_ris = ris;
}
[HttpPost]
public async Task<IActionResult> CreateRIS([FromBody] CreateRISRequest request,CancellationToken ct)
{
var result = await _ris.CreateRIS(request,ct);
return Json(new { success = true, message= $"RIS {result.RISNo} created successfully.", data = result });
}
[HttpPost]
public async Task<IActionResult> ApproveRIS([FromBody] ApproveRISRequest request,CancellationToken ct)
{
bool isSuccess = await _ris.ApproveRIS(request, ct);
if (!isSuccess)
return BadRequest(new { success = false, message = "RIS cancelled failed" });
return Ok(new
{
success = true,
message = $"RIS approved successfully."
});
}
[HttpPost]
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request,CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.Reason))
return BadRequest(new
{
success = false,
message = "A reason for cancellation is required."
});
bool isSuccess = await _ris.CancelRIS(request,ct);
if (!isSuccess)
return BadRequest(new { success = false, message = "RIS cancelled failed" });
return Ok(new
{
success = true,
message = "RIS cancelled and inventory restored."
});
}
[HttpGet]
public async Task<IActionResult> GetRIS(
[FromQuery] string? searchRISNo,string? searchItemName,string? searchIssuedTo,string? discipline,string? status,
int pageNumber = 1,int pageSize = 12,CancellationToken ct = default)
{
short? statusCode = status switch
{
"0" => 0,
"1" => 1,
"2" => 2,
_ => null
};
var result = await _ris.GetRISPaged(new RISPagedRequest
{
SearchRISNo = searchRISNo,
SearchItemName = searchItemName,
SearchIssuedTo = searchIssuedTo,
Discipline = discipline,
Status = statusCode,
PageNumber = pageNumber,
PageSize = pageSize
}, ct);
return Json(new
{
data = result.Data,
recordsTotal = result.RecordsTotal,
departmentList = result.DepartmentList,
disciplineList = result.DisciplineList
});
}
public async Task<IActionResult> GetRISById(int risId, CancellationToken ct)
{
var response = await _ris.GetRISById(risId,ct);
return GetResponse(response);
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApps.ViewComponents.Inventory
{
public class InventoryTabPageViewComponent : ViewComponent
{
public IViewComponentResult Invoke(int InventoryTabPageId)
{
string viewName = InventoryTabPageId switch
{
1 => "~/Views/Components/Inventory/TabPage/Inventory.cshtml",
2 => "~/Views/Components/Inventory/TabPage/RISForApproval.cshtml",
3 => "~/Views/Components/Inventory/TabPage/MRS.cshtml",
_ => "~/Views/Components/Inventory/TabPage/InventoryTransaction.cshtml"
};
return View(viewName);
}
}
}

View File

@ -70,5 +70,5 @@
@await Html.PartialAsync("PagesView/Canvass/_Suppliers")
@await Html.PartialAsync("PagesView/PR/_PRWOCanvass")
@await Html.PartialAsync("PagesView/Canvass/_CanvassSCript")
<script src="~/jsfunctions/common/ParamConfigV2.js"></script>
<script src="~/jsfunctions/common/ParamConfigV3.js"></script>
</div>

View File

@ -0,0 +1,299 @@
<div class="inv-filters">
<div class="inv-search-box">
<i class="fas fa-file-alt"></i>
<input type="text" id="inv-srchPRNo" placeholder="PR Number..." />
</div>
<div class="inv-search-box">
<i class="fas fa-hashtag"></i>
<input type="text" id="inv-srchItemNo" placeholder="Item Number..." />
</div>
<div class="inv-search-box">
<i class="fas fa-box"></i>
<input type="text" id="inv-srchItemName" placeholder="Item Name..." />
</div>
<div class="inv-search-box">
<i class="fas fa-qrcode"></i>
<input type="text" id="inv-srchProjectCode" placeholder="Project Code..." />
</div>
<div class="inv-department-wrap" id="inv-departmentWrap">
<div class="inv-dep-trigger">
<span class="inv-dep-left">
<i class="fas fa-building"></i>
<span class="inv-dep-lbl">All Department</span>
</span>
<i class="fas fa-chevron-down inv-dep-caret"></i>
</div>
<div class="inv-dep-dropdown">
<div class="inv-dep-searchbox">
<i class="fas fa-search"></i>
<input type="text" placeholder="Search department name..." autocomplete="off" />
</div>
<div class="inv-dep-list">
<div class="inv-dep-opt active" data-value="">
<i class="fas fa-th-large"></i> All Department
</div>
</div>
</div>
</div>
<div class="inv-filter-right">
<span class="inv-pgsz-lbl">Show</span>
<select class="inv-pgsz-sel" id="inv-pageSize">
<option value="6">6 per page</option>
<option value="12" selected>12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
<option value="96">96 per page</option>
</select>
<span class="inv-result-count" id="inv-resultCount">0 results</span>
</div>
</div>
@* {{-- CARD GRID --}} *@
<div class="inv-grid" id="inv-grid">
<div class="inv-state" style="grid-column:1/-1">
<div class="inv-spinner"></div><p>Loading…</p>
</div>
</div>
@* {{-- PAGINATION --}} *@
<div class="inv-pagination">
<span class="inv-pg-info" id="inv-pageInfo"></span>
<div class="inv-pg-btns" id="inv-pageButtons"></div>
</div>
<script>
(function () {
"use strict";
const H = window.InventoryHelpers;
// ── Single state object ──
const s = {
page: 1, pageSize: 12, totalCount: 0,
Department: "", searchPR: "", searchItem: "", searchName: "",
searchProjectCode: "", timer: null
};
// ── Elements ──
const grid = document.getElementById("inv-grid");
const countEl = document.getElementById("inv-resultCount");
const pageInfo = document.getElementById("inv-pageInfo");
const pageBtns = document.getElementById("inv-pageButtons");
const inItemNo = document.getElementById("inv-srchItemNo");
const inName = document.getElementById("inv-srchItemName");
const inPR = document.getElementById("inv-srchPRNo");
const inSize = document.getElementById("inv-pageSize");
const depWrap = document.getElementById("inv-departmentWrap");
// ── Guard ──
if (!grid || !depWrap) {
console.error("Tab 1 init failed. Missing:", { grid, depWrap });
return;
}
// ── Single dropdown, using s.Department ──
const depDropdown = H.initDepartmentDropdown(depWrap, val => {
s.Department = val;
s.page = 1;
fetchData();
});
// ── Search inputs ──
[inItemNo, inName, inPR].forEach(el => {
if (!el) return;
el.addEventListener("input", () => {
clearTimeout(s.timer);
s.timer = setTimeout(() => {
s.searchItem = inItemNo?.value.trim() ?? "";
s.searchName = inName?.value.trim() ?? "";
s.searchPR = inPR?.value.trim() ?? "";
s.page = 1;
fetchData();
}, 350);
});
});
if (inSize) {
inSize.addEventListener("change", () => {
s.pageSize = parseInt(inSize.value, 10);
s.page = 1;
fetchData();
});
}
async function fetchData() {
grid.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
<div class="inv-spinner"></div><p>Loading…</p></div>`;
const p = new URLSearchParams({
searchPRNo: s.searchPR,
searchItemNo: s.searchItem,
searchItemName: s.searchName,
searchDept: s.Department,
searchProjectCode: s.searchProjectCode ?? "",
pageNumber: s.page,
pageSize: s.pageSize,
draw: Date.now()
});
try {
const res = await fetch(`/InventoryMgmt/GetInventory?${p}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
s.totalCount = json.recordsTotal ?? 0;
// Populate department dropdown from API response
if (json.departmentList) depDropdown.setItems(json.departmentList);
renderCards(json.data ?? []);
H.renderPagination(pageBtns, pageInfo, s, pg => {
s.page = pg;
fetchData();
});
if (countEl) countEl.textContent =
`${s.totalCount.toLocaleString()} result${s.totalCount !== 1 ? "s" : ""}`;
} catch (err) {
console.error("GetInventory error:", err);
grid.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<p>Failed to load data.</p></div>`;
}
}
function renderCards(data) {
if (!data.length) {
grid.innerHTML = `
<div class="inv-state" style="grid-column:1/-1">
<i class="fas fa-inbox"></i>
<p>No records found.</p>
</div>`;
return;
}
grid.innerHTML = data.map(item => buildInventoryCardHtml(item)).join("");
// Wire Transact buttons
grid.querySelectorAll(".btn-transact").forEach(btn => {
btn.addEventListener("click", () => {
const inventoryId = parseInt(btn.dataset.inventoryid, 10);
TransactModal.open(inventoryId, () => fetchData());
});
});
}
function buildInventoryCardHtml(item) {
const H = window.InventoryHelpers;
const qtyIn = item.qtyIn ?? 0;
const qtyOut = item.qtyOut ?? 0;
const qtyOnHand = item.qtyOnHand ?? 0;
// Stock level % for progress bar (relative to qtyIn; floor at 0)
const pct = qtyIn > 0 ? Math.max(0, Math.min(100, Math.round((qtyOnHand / qtyIn) * 100))) : 0;
const isLow = pct < 20;
// Status badge
const statusHtml = qtyOnHand <= 0
? `<span class="inv-stock-badge inv-stock-empty">Out of stock</span>`
: isLow
? `<span class="inv-stock-badge inv-stock-low">Low stock</span>`
: `<span class="inv-stock-badge inv-stock-ok">In stock</span>`;
return `
<div class="inv-card">
<!-- HEAD -->
<div class="inv-card-hd">
<div style="display:flex;align-items:flex-start;gap:10px;flex:1;min-width:0">
<div class="inv-card-icon-wrap ${isLow ? "inv-card-icon-low" : ""}">
<i class="fas fa-box${qtyOnHand <= 0 ? "-open" : ""}"></i>
</div>
<div style="flex:1;min-width:0">
<div class="inv-card-code">PR NO #${(item.prNo ?? "—")}</div>
<div class="inv-card-code">ITEM NO #${H.escHtml(String(item.itemNo ?? "—"))}</div>
<div class="inv-card-name">${H.escHtml(item.itemName ?? "—")}</div>
<div class="inv-card-meta-row">
<span><i class="fas fa-building"></i> ${H.escHtml(item.department ?? "—")}</span>
<span><i class="fas fa-tag"></i> ${H.escHtml(item.itemCategoryName ?? "—")}</span>
</div>
<div class="inv-card-meta-row">
<span><i class="fas fa-clock"></i> ${_formatDate(item.createdDate)}</span>
${item.lotNo ? `<span><i class="fas fa-barcode"></i> ${H.escHtml(item.lotNo)}</span>` : ""}
</div>
<div class="inv-card-sub">
<i class="fas fa-qrcode"></i>
${(item.projectCode ?? "—")}
</div>
</div>
${statusHtml}
</div>
</div>
<!-- STATS GRID -->
<div class="inv-stats-grid">
<div class="inv-stat-cell inv-stat-in">
<span class="inv-stat-lbl"><i class="fas fa-arrow-circle-down"></i> QTY IN</span>
<span class="inv-stat-val">${_fmtNum(qtyIn)}</span>
</div>
<div class="inv-stat-cell inv-stat-out">
<span class="inv-stat-lbl"><i class="fas fa-arrow-circle-up"></i> QTY OUT</span>
<span class="inv-stat-val">${_fmtNum(qtyOut)}</span>
</div>
<div class="inv-stat-cell inv-stat-hand">
<span class="inv-stat-lbl"><i class="fas fa-layer-group"></i> ON HAND</span>
<span class="inv-stat-val">${_fmtNum(qtyOnHand)}</span>
</div>
</div>
<!-- PROGRESS BAR -->
<div class="inv-progress-wrap">
<div class="inv-progress-labels">
<span>Stock level</span>
<span>${pct}%</span>
</div>
<div class="inv-progress-track">
<div class="inv-progress-fill ${isLow ? "inv-progress-low" : ""}"
style="width:${pct}%"></div>
</div>
</div>
<!-- ITEM ROW -->
${item.itemDescription ? `
<div class="inv-item-desc-row">
<span class="inv-item-desc-lbl"><i class="fas fa-info-circle"></i> ITEM</span>
<span class="inv-item-desc-name">${H.escHtml(item.itemName ?? "—")}</span>
<span class="inv-item-desc-qty">
<i class="fas fa-cubes"></i> ${_fmtNum(qtyIn)}
</span>
</div>` : ""}
<!-- FOOTER -->
<div class="inv-card-ft">
<button class="inv-btn inv-btn-primary btn-transact"
data-inventoryid="${item.inventoryId}">
<i class="fas fa-pen"></i> Transact
</button>
</div>
</div>`;
}
// ── Helpers ──────────────────────────────────────────────────────────────────
function _fmtNum(n) {
const v = parseFloat(n);
return isNaN(v) ? "—" : v.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function _formatDate(raw) {
if (!raw) return "—";
const d = new Date(raw);
return isNaN(d) ? raw : d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
fetchData();
})();
</script>

View File

@ -0,0 +1,936 @@
@* ── Tab: Return Issuance Slip — For Approval ───────────────────────────────
Returned by GetInventoryTabPage?id=2 (or whichever tab id you assign)
Depends on: _InventoryStyles, _InventoryHelpers, window.InventoryHelpers
────────────────────────────────────────────────────────────────────────────── *@
@* ── FILTER BAR ── *@
<div class="inv-filters">
<div class="inv-search-box">
<i class="fas fa-hashtag"></i>
<input type="text" id="ris-srchRISNo" placeholder="RIS Number..." />
</div>
<div class="inv-search-box">
<i class="fas fa-box"></i>
<input type="text" id="ris-srchItemName" placeholder="Item Name..." />
</div>
<div class="inv-search-box">
<i class="fas fa-user"></i>
<input type="text" id="ris-srchIssuedTo" placeholder="Issued To..." />
</div>
@* ── Discipline dropdown ── *@
<div class="inv-department-wrap" id="ris-disciplineWrap">
<div class="inv-dep-trigger">
<span class="inv-dep-left">
<i class="fas fa-tools"></i>
<span class="inv-dep-lbl">All Disciplines</span>
</span>
<i class="fas fa-chevron-down inv-dep-caret"></i>
</div>
<div class="inv-dep-dropdown">
<div class="inv-dep-searchbox">
<i class="fas fa-search"></i>
<input type="text" placeholder="Search discipline..." autocomplete="off" />
</div>
<div class="inv-dep-list">
<div class="inv-dep-opt active" data-value="">
<i class="fas fa-th-large"></i> All Disciplines
</div>
</div>
</div>
</div>
@* ── Status filter ── *@
<div class="inv-department-wrap" id="ris-statusWrap" style="max-width:160px">
<div class="inv-dep-trigger">
<span class="inv-dep-left">
<i class="fas fa-circle-dot"></i>
<span class="inv-dep-lbl">All Status</span>
</span>
<i class="fas fa-chevron-down inv-dep-caret"></i>
</div>
<div class="inv-dep-dropdown">
<div class="inv-dep-searchbox">
<i class="fas fa-search"></i>
<input type="text" placeholder="Search status..." autocomplete="off" />
</div>
<div class="inv-dep-list">
<div class="inv-dep-opt active" data-value="">
<i class="fas fa-th-large"></i> All Status
</div>
</div>
</div>
</div>
<div class="inv-filter-right">
<span class="inv-pgsz-lbl">Show</span>
<select class="inv-pgsz-sel" id="ris-pageSize">
<option value="6">6 per page</option>
<option value="12" selected>12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
</select>
<span class="inv-result-count" id="ris-resultCount">0 results</span>
</div>
</div>
@* ── CARD GRID ── *@
<div class="ris-grid" id="ris-grid">
<div class="inv-state">
<div class="inv-spinner"></div>
<p>Loading…</p>
</div>
</div>
@* ── PAGINATION ── *@
<div class="inv-pagination">
<span class="inv-pg-info" id="ris-pageInfo"></span>
<div class="inv-pg-btns" id="ris-pageButtons"></div>
</div>
@* ── APPROVE CONFIRMATION MODAL ── *@
<div id="ris-approve-modal-overlay" style="display:none;position:fixed;inset:0;
z-index:1080;background:rgba(0,0,0,.45);
display:none;align-items:center;justify-content:center;padding:16px">
<div style="background:var(--card-bg,#fff);border-radius:14px;
border:1px solid var(--border,#d6eaec);width:100%;max-width:440px;
overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.2)">
<div style="background:linear-gradient(135deg,#0d5c63,#0e7c86);
padding:16px 18px;display:flex;align-items:center;gap:12px">
<div style="width:38px;height:38px;border-radius:10px;
background:rgba(255,255,255,.15);
display:flex;align-items:center;justify-content:center">
<i class="fas fa-check-circle" style="color:#fff;font-size:16px"></i>
</div>
<div>
<div style="font-size:14px;font-weight:600;color:#fff">Approve RIS</div>
<div id="ris-approve-subtitle"
style="font-size:11px;color:rgba(255,255,255,.65);margin-top:2px">
Loading…
</div>
</div>
</div>
<div style="padding:20px 18px">
<div id="ris-approve-detail"
style="background:var(--teal-pale,#e6f7f8);border:1px solid var(--border,#d6eaec);
border-radius:10px;padding:14px 16px;margin-bottom:16px;
display:grid;grid-template-columns:1fr 1fr;gap:10px">
</div>
<p style="font-size:13px;color:var(--text-dark,#1a2e35);line-height:1.6">
Approving this slip will confirm the issuance and update the inventory.
<strong>This action cannot be undone.</strong>
</p>
</div>
<div style="padding:12px 18px;border-top:1px solid var(--border,#d6eaec);
background:var(--bg-page,#f0f6f7);
display:flex;align-items:center;justify-content:flex-end;gap:8px">
<button id="ris-approve-cancel-btn" class="tm-btn-cancel">Cancel</button>
<button id="ris-approve-confirm-btn"
style="padding:8px 20px;border-radius:8px;border:none;
background:#0e7c86;color:#fff;font-family:'DM Sans',sans-serif;
font-size:.85rem;font-weight:600;cursor:pointer;
display:flex;align-items:center;gap:7px">
<i class="fas fa-check"></i> Approve RIS
</button>
</div>
</div>
</div>
@* ── CANCEL CONFIRMATION MODAL ── *@
<div id="ris-cancel-modal-overlay" style="display:none;position:fixed;inset:0;
z-index:1080;background:rgba(0,0,0,.45);
display:none;align-items:center;justify-content:center;padding:16px">
<div style="background:var(--card-bg,#fff);border-radius:14px;
border:1px solid var(--border,#d6eaec);width:100%;max-width:440px;
overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.2)">
<div style="background:linear-gradient(135deg,#7f1d1d,#b91c1c);
padding:16px 18px;display:flex;align-items:center;gap:12px">
<div style="width:38px;height:38px;border-radius:10px;
background:rgba(255,255,255,.15);
display:flex;align-items:center;justify-content:center">
<i class="fas fa-ban" style="color:#fff;font-size:16px"></i>
</div>
<div>
<div style="font-size:14px;font-weight:600;color:#fff">Cancel RIS</div>
<div id="ris-cancel-subtitle"
style="font-size:11px;color:rgba(255,255,255,.65);margin-top:2px">
Loading…
</div>
</div>
</div>
<div style="padding:20px 18px">
<div id="ris-cancel-detail"
style="background:#fef2f2;border:1px solid #fecaca;
border-radius:10px;padding:14px 16px;margin-bottom:16px;
display:grid;grid-template-columns:1fr 1fr;gap:10px">
</div>
<div style="display:flex;gap:8px;padding:10px 12px;background:#fef2f2;
border:1px solid #fecaca;border-radius:8px;margin-bottom:12px">
<i class="fas fa-exclamation-triangle"
style="color:#dc2626;flex-shrink:0;margin-top:2px"></i>
<p style="font-size:12px;color:#7f1d1d;line-height:1.6;margin:0">
Cancelling this slip will <strong>reverse the inventory deduction</strong>
and restore the qty back to on-hand. This cannot be undone.
</p>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-sticky-note"></i> Reason for cancellation
<span class="tm-req">*</span>
</label>
<input id="ris-cancel-reason" class="tm-input"
type="text" placeholder="Enter reason…" maxlength="500">
</div>
</div>
<div style="padding:12px 18px;border-top:1px solid var(--border,#d6eaec);
background:var(--bg-page,#f0f6f7);
display:flex;align-items:center;justify-content:flex-end;gap:8px">
<button id="ris-cancel-dismiss-btn" class="tm-btn-cancel">Dismiss</button>
<button id="ris-cancel-confirm-btn"
style="padding:8px 20px;border-radius:8px;border:none;
background:#dc2626;color:#fff;font-family:'DM Sans',sans-serif;
font-size:.85rem;font-weight:600;cursor:pointer;
display:flex;align-items:center;gap:7px">
<i class="fas fa-ban"></i> Cancel RIS
</button>
</div>
</div>
</div>
<style>
/* ── RIS card grid ── */
.ris-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 16px;
min-height: 220px;
}
@@media (max-width: 768px) { .ris-grid { grid-template-columns: 1fr; } }
@@media (min-width: 769px) and (max-width: 1100px) {
.ris-grid { grid-template-columns: repeat(2, 1fr); }
}
/* ── RIS card ── */
.ris-card {
background: var(--card-bg, #fff);
border-radius: var(--radius-lg, 14px);
box-shadow: var(--shadow-card);
border: 1px solid var(--border, #d6eaec);
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow .25s, transform .25s;
}
.ris-card:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
/* ── Card header ── */
.ris-card-hd {
background: linear-gradient(135deg, var(--teal-dark, #0d5c63), var(--teal-mid, #0e7c86));
padding: 14px 16px 12px;
position: relative;
}
.ris-card-no {
font-size: .7rem;
color: rgba(255,255,255,.6);
font-weight: 700;
letter-spacing: .07em;
text-transform: uppercase;
margin-bottom: 3px;
}
.ris-card-title {
font-family: 'Space Grotesk', sans-serif;
font-size: 1rem;
font-weight: 700;
color: #fff;
line-height: 1.25;
word-break: break-word;
margin-bottom: 6px;
}
.ris-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 11px;
color: rgba(255,255,255,.65);
}
.ris-card-meta span {
display: flex;
align-items: center;
gap: 4px;
}
/* ── Status badge positioned top-right ── */
.ris-status-badge {
position: absolute;
top: 12px;
right: 14px;
font-size: 10px;
font-weight: 700;
padding: 3px 10px;
border-radius: 50px;
letter-spacing: .05em;
text-transform: uppercase;
}
.ris-status-draft { background: rgba(255,255,255,.18); color: #fff; }
.ris-status-approved { background: #dcfce7; color: #166534; }
.ris-status-cancelled { background: #fee2e2; color: #991b1b; }
/* ── Stats row ── */
.ris-stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(--border, #d6eaec);
border-bottom: 1px solid var(--border, #d6eaec);
}
.ris-stat {
background: var(--card-bg, #fff);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 2px;
}
.ris-stat-lbl {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--text-muted, #6b8890);
display: flex;
align-items: center;
gap: 4px;
}
.ris-stat-lbl i { font-size: 11px; }
.ris-stat-val {
font-size: 18px;
font-weight: 700;
color: var(--text-dark, #1a2e35);
line-height: 1.2;
}
.ris-stat-val.teal { color: var(--teal-mid, #0e7c86); }
.ris-stat-val.red { color: #dc2626; }
.ris-stat-val.amber { color: #92400e; }
/* ── Body ── */
.ris-card-body {
padding: 12px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.ris-field-row {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 12px;
}
.ris-field-lbl {
min-width: 80px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--text-muted, #6b8890);
padding-top: 1px;
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.ris-field-lbl i { font-size: 11px; }
.ris-field-val {
color: var(--text-dark, #1a2e35);
line-height: 1.5;
word-break: break-word;
}
/* ── Discipline chip ── */
.ris-discipline-chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 50px;
font-size: 11px;
font-weight: 600;
background: var(--teal-pale, #e6f7f8);
color: var(--teal-dark, #0d5c63);
border: 1px solid var(--border, #d6eaec);
}
/* ── Footer ── */
.ris-card-ft {
padding: 10px 16px 14px;
display: flex;
gap: 8px;
border-top: 1px solid var(--border, #d6eaec);
}
.ris-btn {
flex: 1;
padding: 8px 12px;
border-radius: var(--radius-sm, 8px);
border: none;
cursor: pointer;
font-family: 'DM Sans', sans-serif;
font-size: .82rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all .2s;
}
.ris-btn-approve {
background: var(--teal-mid, #0e7c86);
color: #fff;
}
.ris-btn-approve:hover { background: var(--teal-dark, #0d5c63); }
.ris-btn-approve:disabled { opacity: .4; cursor: default; }
.ris-btn-cancel {
background: transparent;
color: #dc2626;
border: 1.5px solid #fca5a5;
}
.ris-btn-cancel:hover { background: #fef2f2; border-color: #dc2626; }
.ris-btn-cancel:disabled { opacity: .4; cursor: default; }
/* ── Approve modal detail field ── */
.am-field { display: flex; flex-direction: column; gap: 3px; }
.am-lbl {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--text-muted, #6b8890);
display: flex;
align-items: center;
gap: 4px;
}
.am-lbl i { font-size: 11px; }
.am-val {
font-size: 13px;
font-weight: 600;
color: var(--teal-dark, #0d5c63);
}
</style>
<script>
(function () {
"use strict";
const H = window.InventoryHelpers;
// ── State ──────────────────────────────────────────────────────────────
const s = {
page: 1, pageSize: 12, totalCount: 0,
searchRISNo: "", searchItemName: "", searchIssuedTo: "",
discipline: "", status: "",
timer: null
};
// ── Elements ───────────────────────────────────────────────────────────
const grid = document.getElementById("ris-grid");
const countEl = document.getElementById("ris-resultCount");
const pageInfo = document.getElementById("ris-pageInfo");
const pageBtns = document.getElementById("ris-pageButtons");
const inRISNo = document.getElementById("ris-srchRISNo");
const inItem = document.getElementById("ris-srchItemName");
const inIssued = document.getElementById("ris-srchIssuedTo");
const inSize = document.getElementById("ris-pageSize");
const discWrap = document.getElementById("ris-disciplineWrap");
const statusWrap = document.getElementById("ris-statusWrap");
// ── Guard ──────────────────────────────────────────────────────────────
if (!grid || !discWrap) {
console.error("RIS tab init failed — missing elements.");
return;
}
// ── Discipline dropdown ────────────────────────────────────────────────
const discDropdown = H.initSearchDropdown(discWrap, val => {
s.discipline = val;
s.page = 1;
fetchData();
}, {
cssPrefix: "inv-dep",
allLabel: "All Disciplines",
allIcon: "fas fa-th-large",
itemIcon: "fas fa-tools"
});
// ── Status dropdown (static options) ──────────────────────────────────
const statusDropdown = H.initSearchDropdown(statusWrap, val => {
s.status = val;
s.page = 1;
fetchData();
}, {
cssPrefix: "inv-dep",
allLabel: "All Status",
allIcon: "fas fa-th-large",
itemIcon: "fas fa-circle-dot"
});
// Seed static status options immediately
statusDropdown.setItems(["Draft", "Approved", "Cancelled"]);
// ── Search inputs ──────────────────────────────────────────────────────
[inRISNo, inItem, inIssued].forEach(el => {
if (!el) return;
el.addEventListener("input", () => {
clearTimeout(s.timer);
s.timer = setTimeout(() => {
s.searchRISNo = inRISNo?.value.trim() ?? "";
s.searchItemName = inItem?.value.trim() ?? "";
s.searchIssuedTo = inIssued?.value.trim() ?? "";
s.page = 1;
fetchData();
}, 350);
});
});
if (inSize) {
inSize.addEventListener("change", () => {
s.pageSize = parseInt(inSize.value, 10);
s.page = 1;
fetchData();
});
}
// ── Status value → short code for API ─────────────────────────────────
function statusToCode(label) {
return { "Draft": "0", "Approved": "1", "Cancelled": "2" }[label] ?? "";
}
// ══════════════════════════════════════════════════════════════════════
// FETCH
// ══════════════════════════════════════════════════════════════════════
async function fetchData() {
grid.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
<div class="inv-spinner"></div><p>Loading…</p></div>`;
const p = new URLSearchParams({
searchRISNo: s.searchRISNo,
searchItemName: s.searchItemName,
searchIssuedTo: s.searchIssuedTo,
discipline: s.discipline,
status: statusToCode(s.status),
pageNumber: s.page,
pageSize: s.pageSize,
draw: Date.now()
});
try {
const res = await fetch(`/RISMgmt/GetRIS?${p}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const data = json.data ?? json;
s.totalCount = json.recordsTotal ?? 0;
if (json.disciplineList?.length)
discDropdown.setItems(json.disciplineList.map(d => d.disciplineName));
renderCards(json.data ?? []);
H.renderPagination(
document.getElementById("ris-pageButtons"),
document.getElementById("ris-pageInfo"),
s,
pg => { s.page = pg; fetchData(); }
);
const c = document.getElementById("ris-resultCount");
if (c) c.textContent =
`${s.totalCount.toLocaleString()} result${s.totalCount !== 1 ? "s" : ""}`;
} catch (err) {
console.error("GetRIS error:", err);
grid.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<p>Failed to load data.</p></div>`;
}
}
// ══════════════════════════════════════════════════════════════════════
// RENDER CARDS
// ══════════════════════════════════════════════════════════════════════
function renderCards(data) {
const g = document.getElementById("ris-grid");
if (!data.length) {
g.innerHTML = `<div class="inv-state" style="grid-column:1/-1">
<i class="fas fa-inbox"></i>
<p>No RIS records found.</p></div>`;
return;
}
g.innerHTML = data.map(item => buildRISCardHtml(item)).join("");
// ── Wire Approve buttons ──
g.querySelectorAll(".btn-ris-approve").forEach(btn =>
btn.addEventListener("click", () => {
const risId = parseInt(btn.dataset.risid, 10);
const risNo = btn.dataset.risno;
openApproveModal(risId, risNo, btn.dataset);
})
);
// ── Wire Cancel buttons ──
g.querySelectorAll(".btn-ris-cancel").forEach(btn =>
btn.addEventListener("click", () => {
const risId = parseInt(btn.dataset.risid, 10);
const risNo = btn.dataset.risno;
openCancelModal(risId, risNo, btn.dataset);
})
);
}
// ══════════════════════════════════════════════════════════════════════
// CARD HTML BUILDER
// ══════════════════════════════════════════════════════════════════════
function buildRISCardHtml(item) {
const risId = item.risId ?? item.RISId ?? 0;
const risNo = item.risNo ?? item.RISNo ?? "—";
const itemName = item.itemName ?? item.ItemName ?? "—";
const itemNo = item.itemNo ?? item.ItemNo ?? "—";
const issuedTo = item.issuedTo ?? item.IssuedTo ?? "—";
const discipline = item.disciplineName ?? item.DisciplineName ?? "—";
const qtyIssued = item.qtyIssued ?? item.QtyIssued ?? 0;
const totalReturned= item.totalReturned ?? item.TotalReturned ?? 0;
const netIssued = qtyIssued - totalReturned;
const status = item.status ?? item.Status ?? 0;
const statusLabel = item.statusLabel ?? item.StatusLabel ?? _statusLabel(status);
const remarks = item.remarks ?? item.Remarks ?? null;
const createdDate = item.createdDate ?? item.CreatedDate ?? null;
const approvedBy = item.approvedBy ?? item.ApprovedBy ?? null;
const approvedDate = item.approvedDate ?? item.ApprovedDate ?? null;
const mrsCount = item.mrsCount ?? item.MRSCount ?? 0;
const isDraft = status === 0;
const isApproved = status === 1;
const isCancelled = status === 2;
const statusCls = isDraft ? "ris-status-draft"
: isApproved ? "ris-status-approved"
: "ris-status-cancelled";
const statusIcon = isDraft ? "fas fa-clock"
: isApproved ? "fas fa-check-circle"
: "fas fa-ban";
return `
<div class="ris-card">
<!-- HEAD -->
<div class="ris-card-hd">
<span class="${statusCls} ris-status-badge">
<i class="${statusIcon}"></i> ${H.escHtml(statusLabel)}
</span>
<div class="ris-card-no">RIS NO</div>
<div class="ris-card-title">${H.escHtml(risNo)}</div>
<div class="ris-card-meta">
<span><i class="fas fa-box"></i> ${H.escHtml(itemName)}</span>
<span><i class="fas fa-hashtag"></i> #${H.escHtml(String(itemNo))}</span>
</div>
<div class="ris-card-meta" style="margin-top:4px">
<span><i class="fas fa-user"></i> ${H.escHtml(issuedTo)}</span>
<span><i class="fas fa-clock"></i> ${_formatDate(createdDate)}</span>
</div>
</div>
<!-- STATS -->
<div class="ris-stats-row">
<div class="ris-stat">
<span class="ris-stat-lbl">
<i class="fas fa-cubes"></i> Qty Issued
</span>
<span class="ris-stat-val teal">${qtyIssued}</span>
</div>
<div class="ris-stat">
<span class="ris-stat-lbl">
<i class="fas fa-undo"></i> Returned
</span>
<span class="ris-stat-val amber">${totalReturned}</span>
</div>
<div class="ris-stat">
<span class="ris-stat-lbl">
<i class="fas fa-layer-group"></i> Net Issued
</span>
<span class="ris-stat-val ${netIssued > 0 ? "red" : ""}">${netIssued}</span>
</div>
</div>
<!-- BODY -->
<div class="ris-card-body">
<div class="ris-field-row">
<span class="ris-field-lbl">
<i class="fas fa-tools"></i> Discipline
</span>
<span class="ris-field-val">
<span class="ris-discipline-chip">
<i class="fas fa-tools"></i> ${H.escHtml(discipline)}
</span>
</span>
</div>
${mrsCount > 0 ? `
<div class="ris-field-row">
<span class="ris-field-lbl">
<i class="fas fa-file-import"></i> MRS Count
</span>
<span class="ris-field-val">
<span style="background:#E6F1FB;color:#185FA5;padding:2px 8px;
border-radius:50px;font-size:11px;font-weight:600">
${mrsCount} return slip${mrsCount !== 1 ? "s" : ""}
</span>
</span>
</div>` : ""}
${remarks ? `
<div class="ris-field-row">
<span class="ris-field-lbl">
<i class="fas fa-sticky-note"></i> Remarks
</span>
<span class="ris-field-val" style="color:var(--text-muted,#6b8890)">
${H.escHtml(remarks)}
</span>
</div>` : ""}
${isApproved && approvedBy ? `
<div class="ris-field-row">
<span class="ris-field-lbl">
<i class="fas fa-user-check"></i> Approved by
</span>
<span class="ris-field-val">
${H.escHtml(approvedBy)}
<span style="color:var(--text-muted,#6b8890);font-size:11px;margin-left:4px">
${_formatDate(approvedDate)}
</span>
</span>
</div>` : ""}
</div>
<!-- FOOTER -->
<div class="ris-card-ft">
${isDraft ? `
<button class="ris-btn ris-btn-approve btn-ris-approve"
data-risid="${risId}"
data-risno="${H.escAttr(risNo)}"
data-itemname="${H.escAttr(itemName)}"
data-issuedto="${H.escAttr(issuedTo)}"
data-discipline="${H.escAttr(discipline)}"
data-qtyissued="${qtyIssued}">
<i class="fas fa-check-circle"></i> Approve
</button>
<button class="ris-btn ris-btn-cancel btn-ris-cancel"
data-risid="${risId}"
data-risno="${H.escAttr(risNo)}"
data-itemname="${H.escAttr(itemName)}"
data-issuedto="${H.escAttr(issuedTo)}"
data-discipline="${H.escAttr(discipline)}"
data-qtyissued="${qtyIssued}">
<i class="fas fa-ban"></i> Cancel
</button>` : `
<button class="ris-btn ris-btn-cancel btn-ris-cancel"
${isCancelled ? "disabled" : ""}
data-risid="${risId}"
data-risno="${H.escAttr(risNo)}"
data-itemname="${H.escAttr(itemName)}"
data-issuedto="${H.escAttr(issuedTo)}"
data-discipline="${H.escAttr(discipline)}"
data-qtyissued="${qtyIssued}"
style="flex:1">
<i class="fas fa-ban"></i>
${isCancelled ? "Cancelled" : "Cancel"}
</button>`}
</div>
</div>`;
}
// ══════════════════════════════════════════════════════════════════════
// APPROVE MODAL
// ══════════════════════════════════════════════════════════════════════
function openApproveModal(risId, risNo, dataset) {
const overlay = document.getElementById("ris-approve-modal-overlay");
document.getElementById("ris-approve-subtitle").textContent =
`${risNo} — ${dataset.itemname ?? ""}`;
document.getElementById("ris-approve-detail").innerHTML = `
<div class="am-field">
<span class="am-lbl"><i class="fas fa-hashtag"></i> RIS No</span>
<span class="am-val">${H.escHtml(risNo)}</span>
</div>
<div class="am-field">
<span class="am-lbl"><i class="fas fa-cubes"></i> Qty Issued</span>
<span class="am-val">${dataset.qtyissued ?? 0} pcs</span>
</div>
<div class="am-field">
<span class="am-lbl"><i class="fas fa-box"></i> Item</span>
<span class="am-val">${H.escHtml(dataset.itemname ?? "—")}</span>
</div>
<div class="am-field">
<span class="am-lbl"><i class="fas fa-user"></i> Issued To</span>
<span class="am-val">${H.escHtml(dataset.issuedto ?? "—")}</span>
</div>
<div class="am-field">
<span class="am-lbl"><i class="fas fa-tools"></i> Discipline</span>
<span class="am-val">${H.escHtml(dataset.discipline ?? "—")}</span>
</div>`;
overlay.style.display = "flex";
// ── Wire buttons fresh each open ──
const confirmBtn = document.getElementById("ris-approve-confirm-btn");
const cancelBtn = document.getElementById("ris-approve-cancel-btn");
const closeModal = () => { overlay.style.display = "none"; };
// Clone to remove stale listeners
const newConfirm = confirmBtn.cloneNode(true);
const newCancel = cancelBtn.cloneNode(true);
confirmBtn.replaceWith(newConfirm);
cancelBtn.replaceWith(newCancel);
newCancel.addEventListener("click", closeModal);
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
newConfirm.addEventListener("click", async () => {
newConfirm.disabled = true;
newConfirm.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Approving…`;
try {
const res = await fetch(`/RISMgmt/ApproveRIS`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ risId })
});
const json = await res.json();
const d = json.data ?? json;
if (!res.ok || !d.success) {
showToast("error", d.message ?? "Could not approve RIS.", "Failed", 4000);
return;
}
showToast("success", d.message ?? `RIS ${risNo} approved.`, "Approved!", 3500);
closeModal();
fetchData();
} catch (err) {
console.error("ApproveRIS error:", err);
showToast("error", "Request failed. Please try again.", "Error", 4000);
} finally {
newConfirm.disabled = false;
newConfirm.innerHTML = `<i class="fas fa-check"></i> Approve RIS`;
}
});
}
// ══════════════════════════════════════════════════════════════════════
// CANCEL MODAL
// ══════════════════════════════════════════════════════════════════════
function openCancelModal(risId, risNo, dataset) {
const overlay = document.getElementById("ris-cancel-modal-overlay");
const reasonEl = document.getElementById("ris-cancel-reason");
reasonEl.value = "";
reasonEl.classList.remove("error");
document.getElementById("ris-cancel-subtitle").textContent =
`${risNo} — ${dataset.itemname ?? ""}`;
document.getElementById("ris-cancel-detail").innerHTML = `
<div class="am-field">
<span class="am-lbl"><i class="fas fa-hashtag"></i> RIS No</span>
<span class="am-val" style="color:#991b1b">${H.escHtml(risNo)}</span>
</div>
<div class="am-field">
<span class="am-lbl"><i class="fas fa-cubes"></i> Qty Issued</span>
<span class="am-val" style="color:#991b1b">${dataset.qtyissued ?? 0} pcs</span>
</div>
<div class="am-field">
<span class="am-lbl"><i class="fas fa-box"></i> Item</span>
<span class="am-val">${H.escHtml(dataset.itemname ?? "—")}</span>
</div>
<div class="am-field">
<span class="am-lbl"><i class="fas fa-user"></i> Issued To</span>
<span class="am-val">${H.escHtml(dataset.issuedto ?? "—")}</span>
</div>`;
overlay.style.display = "flex";
const confirmBtn = document.getElementById("ris-cancel-confirm-btn");
const dismissBtn = document.getElementById("ris-cancel-dismiss-btn");
const closeModal = () => { overlay.style.display = "none"; };
const newConfirm = confirmBtn.cloneNode(true);
const newDismiss = dismissBtn.cloneNode(true);
confirmBtn.replaceWith(newConfirm);
dismissBtn.replaceWith(newDismiss);
newDismiss.addEventListener("click", closeModal);
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
newConfirm.addEventListener("click", async () => {
const reason = reasonEl.value.trim();
if (!reason) {
reasonEl.classList.add("error");
reasonEl.focus();
showToast("warning", "Please enter a reason for cancellation.", "Required", 3000);
return;
}
newConfirm.disabled = true;
newConfirm.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Cancelling…`;
try {
const res = await fetch(`/RISMgmt/CancelRIS`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ risId, reason })
});
const json = await res.json();
const d = json.data ?? json;
if (!res.ok || !d.success) {
showToast("error", d.message ?? "Could not cancel RIS.", "Failed", 4000);
return;
}
showToast("success", d.message ?? `RIS ${risNo} cancelled.`, "Cancelled", 3500);
closeModal();
fetchData();
} catch (err) {
console.error("CancelRIS error:", err);
showToast("error", "Request failed. Please try again.", "Error", 4000);
} finally {
newConfirm.disabled = false;
newConfirm.innerHTML = `<i class="fas fa-ban"></i> Cancel RIS`;
}
});
}
// ══════════════════════════════════════════════════════════════════════
// HELPERS
// ══════════════════════════════════════════════════════════════════════
function _statusLabel(code) {
return { 0: "Draft", 1: "Approved", 2: "Cancelled" }[code] ?? "Unknown";
}
function _formatDate(raw) {
if (!raw) return "—";
const d = new Date(raw);
return isNaN(d) ? raw : d.toLocaleDateString("en-US", {
month: "short", day: "numeric", year: "numeric"
});
}
// ── Initial load ───────────────────────────────────────────────────────
fetchData();
})();
</script>

View File

@ -1,176 +1,114 @@
<body>
<div class="container-fluid">
<div class="table-container shadow-lg p-3 mb-5 bg-white rounded">
<div class="header-container">
<h2 style="display: flex; flex-direction: column; align-items: center;">Inventory List</h2>
</div>
<div class="row" style="margin-top:10px">
<div class="col-md-3 col-sm-6 form-group">
<label for="dateFrom">From:</label>
<input type="date" id="dateFrom" class="form-control" />
</div>
<div class="col-md-3 col-sm-6 form-group">
<label for="dateTo">To:</label>
<input type="date" id="dateTo" class="form-control" />
</div>
<div class="col-md-2 col-sm-6 form-group d-flex align-items-end">
<button type="button" id="btnSort" onclick="sortByDate();" class="btn btn-primary">Sort</button>
</div>
</div>
<br />
<table id="InventoryTable" class="row-border" cellspacing="0" width="100%">
<thead>
<tr>
<th>Txn.Id.</th>
<th>ItemNo</th>
<th>ItemName</th>
<th>Specification</th>
<th>CategoryName</th>
<th>Department</th>
<th>In</th>
<th>Out</th>
<th>OnHand</th>
<th>Ousts.Qty</th>
<th>Location</th>
<th>Trans.Date</th>
<th>Action</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<!-- Modal viewItemDetails -->
<div class="modal fade custom-modal-backdrop" id="viewItemDetails" tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ModalLabel">Item Details</h5>
</div>
<div class="modal-body">
@* <div style="margin-bottom:10px;margin-left:10px;font-size:large">
<label for="PONumber">PO#: </label>
<label id="PONumber"></label>
<input hidden id="PRNumber" />
</div> *@
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="itemName">Item Name</label>
<input readonly class="form-control" style="margin-bottom:5px;" id="itemName">
</div>
<div class="form-group">
<label for="itemDescription">Item Description</label>
<textarea readonly id="itemDescription" style="width: 100%; height: 100px;"></textarea>
</div>
@await Html.PartialAsync("PagesView/Inventory/_InventoryStyles")
@await Html.PartialAsync("PagesView/Inventory/_InventoryHelpers")
<div class="form-group">
<label for="itemCategoryName">Category</label>
<input readonly id="itemCategoryName" class="form-control" style="margin-bottom:5px;" name="itemCategoryName" />
</div>
<div class="form-group">
<label for="uomName">UOM</label>
<input readonly id="uomName" class="form-control" style="margin-bottom:5px;" name="uomName" />
</div>
<div class="form-group">
<label for="itemColorName">Item Color</label>
<input readonly id="itemColorName" class="form-control" style="margin-bottom:5px;" name="itemColorName" />
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="itemNo">Item No</label>
<input readonly class="form-control" style="margin-bottom:5px;" id="itemNo">
</div>
<div class="form-group">
<label for="qtyIn">Qty In</label>
<input readonly id="qtyIn" class="form-control" style="margin-bottom:5px;" name="qtyIn" />
</div>
<div class="form-group">
<label for="qtyOut">Qty Out</label>
<input readonly id="qtyOut" class="form-control" style="margin-bottom:5px;" name="qtyOut" />
</div>
<div class="form-group">
<label for="qtyOnHand">Qty On Hand</label>
<input readonly id="qtyOnHand" class="form-control" style="margin-bottom:5px;" name="qtyOnHand" />
</div>
<div class="form-group">
<label for="lotTypeName">Packing Type</label>
<input readonly id="lotTypeName" class="form-control" style="margin-bottom:5px;" name="lotTypeName" />
</div>
<div class="form-group">
<label for="lotNo">Location</label>
<select name="lotNo" id="lotNo" class="form-control">
</select>
<input type="hidden" id="lotId" name="lotId" required>
</div>
</div>
</div>
<hr />
<div>
<img id="itemPictureImage" alt="itemPictureImage" width="450" class="img-fluid" style="margin-bottom:5px; border-radius:15px; box-shadow:15px;">
<input type="file" id="itemPictureImageInput" accept="image/*" style="display: none; margin-bottom:5px; border-radius:15px; box-shadow:15px;">
<input type="hidden" id="prDetailsId" name="prDetailsId" />
<input type="hidden" id="itemAttachId" name="itemAttachId" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Back</button>
<button type="button" id="btnUpdateItem" onclick="postPutLotBin(0);" class="btn btn-warning">Update</button>
</div>
</div>
</div>
</div>
<!-- Modal addNewItem -->
<div class="modal fade custom-modal-backdrop" id="addNewItem"
tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header" style="display: flex; flex-direction: column; align-items: center;">
<h2 class="modal-title" id="ModalLabel">Item List</h2>
</div>
<br />
<div class="modal-body">
<div class="table-container shadow-lg p-3 mb-5 bg-white rounded">
<table id="ItemDataTable" class="row-border" style="width: 100%;">
<thead>
<tr>
<th>ItemNo</th>
<th>ItemName</th>
<th>Specs</th>
<th>Qty</th>
<th>E-Address</th>
<th>Sup.Name</th>
<th>Manufacturer</th>
<th>Price</th>
<th>Action</th>
<th hidden></th>
<th hidden></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Back</button>
</div>
</div>
</div>
</div>
<input hidden id="roleRights" value="@ViewBag.UserRoles" />
<div id="overlay" class="overlay" style="display: none;">
<div id="loader" class="loader"></div>
<div class="inventory-wrapper">
@* {{-- HEADER --}} *@
<div class="inv-header">
<div class="inv-header-inner">
<i class="fas fa-file-invoice inv-header-icon"></i>
<div>
<h1>Inventory Management</h1>
<p>Manage inventory transaction, status, and comparison</p>
</div>
</div>
<script src="~/jsfunctions/Inventory/InventoryV2.js"></script>
<script src="~/jsfunctions/utilities/utilsV3.js"></script>
<script src="~/jsfunctions/inventory/inventvar.js"></script>
</body>
</div>
@* {{-- TAB NAV --}}
{{-- data-tab-id matches the ViewComponent switch:*@
<div class="inv-tabs" role="tablist">
<button class="inv-tab-btn active" data-tab-id="1" role="tab">
<i class="fas fa-user-tag"></i> Inventory
</button>
<button class="inv-tab-btn" data-tab-id="2" role="tab">
<i class="fas fa-clock"></i> For Approval
</button>
<button class="inv-tab-btn" data-tab-id="3" role="tab">
<i class="fas fa-id-badge"></i> Return Issuance Slip (Report)
</button>
<button class="inv-tab-btn" data-tab-id="4" role="tab">
<i class="fas fa-store"></i> Material Return Slip (Report)
</button>
<button class="inv-tab-btn" data-tab-id="5" role="tab">
<i class="fas fa-check-circle"></i> Transaction History
</button>
</div>
<div id="inv-tab-content">
<div class="inv-tab-loading">
<div class="inv-spinner"></div>
<span>Loading…</span>
</div>
</div>
</div>
<script>
(function () {
"use strict";
// ── Prevent re-initialization on cache restore ──
if (window.__invTab1Initialized) return;
window.__invTab1Initialized = true;
const tabContent = document.getElementById("inv-tab-content");
const tabBtns = document.querySelectorAll(".inv-tab-btn");
// Cache stores the raw HTML string per tab id
const cache = {};
let activeTabId = null;
async function loadTab(tabId) {
if (activeTabId === tabId) return;
activeTabId = tabId;
// Mark active button
tabBtns.forEach(b => b.classList.toggle("active", b.dataset.tabId === String(tabId)));
if (cache[tabId]) {
// ── FIX: inject cached HTML then re-run its <script> blocks ──────
injectHtml(tabContent, cache[tabId]);
return;
}
tabContent.innerHTML = `<div class="inv-tab-loading">
<div class="inv-spinner"></div><span>Loading…</span></div>`;
try {
const res = await fetch(`/InventoryMgmt/GetInventoryTabPage?id=${tabId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
cache[tabId] = html;
injectHtml(tabContent, html);
} catch (err) {
console.error("Tab load error:", err);
tabContent.innerHTML = `
<div class="inv-placeholder">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<h3>Failed to load</h3>
<p>Please try again or refresh the page.</p>
</div>`;
}
}
/**
* Set innerHTML then re-execute every <script> block so IIFEs inside
* ViewComponent views fire correctly — both on first load AND cache restore.
*/
function injectHtml(container, html) {
container.innerHTML = html;
container.querySelectorAll("script").forEach(oldScript => {
const newScript = document.createElement("script");
Array.from(oldScript.attributes).forEach(attr =>
newScript.setAttribute(attr.name, attr.value));
newScript.textContent = oldScript.textContent;
oldScript.replaceWith(newScript);
});
}
// Wire tab clicks
tabBtns.forEach(btn =>
btn.addEventListener("click", () => loadTab(parseInt(btn.dataset.tabId, 10)))
);
// Auto-load the first tab on page ready
loadTab(1);
})();
</script>
@await Html.PartialAsync("PagesView/Inventory/_InventoryTransactModal")

View File

@ -0,0 +1,5 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}

View File

@ -18,7 +18,7 @@
<script src="~/jsfunctions/canvass/CanvassView.js"></script>
<script src="~/jsfunctions/common/termsV2.js"></script>
<script src="~/jsfunctions/canvass/PostPut.js"></script>
<script src="~/jsfunctions/common/PostPutV2.js"></script>
<script src="~/jsfunctions/common/PostPutV3.js"></script>
<script src="~/jsfunctions/canvass/buttonsV5.js"></script>
<script src="~/jsfunctions/canvass/rowCallBackV2.js"></script>
<script src="~/jsfunctions/utilities/StylesV3.js"></script>

View File

@ -0,0 +1,314 @@
<script>
window.InventoryHelpers = (function () {
"use strict";
/* ── String helpers ──────────────────────────────── */
function splitAggr(raw) {
if (!raw) return [];
return raw.replace(/<br\s*\/?>/gi, ",").split(",").map(s => s.trim()).filter(Boolean);
}
function escHtml(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
function escAttr(s) {
return String(s).replace(/"/g,"&quot;").replace(/'/g,"&#39;");
}
/* ── Pagination helpers ──────────────────────────── */
function buildPageRange(cur, total) {
if (total <= 7) return Array.from({length: total}, (_, i) => i + 1);
if (cur <= 4) return [1, 2, 3, 4, 5, "…", total];
if (cur >= total - 3) return [1, "…", total-4, total-3, total-2, total-1, total];
return [1, "…", cur-1, cur, cur+1, "…", total];
}
function mkPageBtn(html, disabled, active) {
const b = document.createElement("button");
b.className = "inv-pg-btn" + (active ? " active" : "");
b.innerHTML = html; b.disabled = !!disabled;
return b;
}
function renderPagination(container, infoEl, state, onPageChange) {
const { page, pageSize, totalCount } = state;
const totalPages = Math.ceil(totalCount / pageSize) || 1;
const from = Math.min((page - 1) * pageSize + 1, totalCount);
const to = Math.min(page * pageSize, totalCount);
infoEl.textContent = totalCount
? `Showing ${from.toLocaleString()}${to.toLocaleString()} of ${totalCount.toLocaleString()}`
: "No records";
container.innerHTML = "";
const prev = mkPageBtn('<i class="fas fa-chevron-left"></i>', page <= 1);
prev.addEventListener("click", () => { if (page > 1) onPageChange(page - 1); });
container.appendChild(prev);
buildPageRange(page, totalPages).forEach(p => {
if (p === "…") {
const d = document.createElement("span");
d.className = "inv-pg-btn"; d.style.cursor = "default"; d.textContent = "…";
container.appendChild(d); return;
}
const b = mkPageBtn(p, false, p === page);
b.addEventListener("click", () => onPageChange(p));
container.appendChild(b);
});
const next = mkPageBtn('<i class="fas fa-chevron-right"></i>', page >= totalPages);
next.addEventListener("click", () => { if (page < totalPages) onPageChange(page + 1); });
container.appendChild(next);
}
/* ── Generic searchable dropdown factory ─────────────────────────────
*
* Works for BOTH Departments and ClientNames (or any list).
* CSS class prefixes are passed in so each dropdown is fully independent.
*
* param wrap HTMLElement — the root wrapper element
* param onChange Function — called with the selected value string
* param opts Object — optional overrides:
* allLabel : string label for "All" option (default "All")
* icon : string FA icon class (default "fa-th-large / fa-store")
* cssPrefix : string CSS class prefix (default "inv-dep")
*
* CSS classes expected inside `wrap`:
* {prefix}-trigger, {prefix}-dropdown, {prefix}-lbl,
* {prefix}-searchbox > input, {prefix}-list
*
* Returns: { setItems(list), getCurrent() }
──────────────────────────────────────────────────────────────────── */
function initSearchDropdown(wrap, onChange, opts) {
opts = opts || {};
const prefix = opts.cssPrefix || "inv-dep";
const allLabel = opts.allLabel || "All";
const allIcon = opts.allIcon || "fas fa-th-large";
const itemIcon = opts.itemIcon || "fas fa-store";
const trigger = wrap.querySelector(`.${prefix}-trigger`);
const dropdown = wrap.querySelector(`.${prefix}-dropdown`);
const label = wrap.querySelector(`.${prefix}-lbl`);
const search = wrap.querySelector(`.${prefix}-searchbox input`);
const list = wrap.querySelector(`.${prefix}-list`);
if (!trigger || !dropdown || !label || !search || !list) {
//console.warn("initSearchDropdown: one or more required elements not found in", wrap);
return { setItems: () => {}, getCurrent: () => "" };
}
let allItems = [];
let current = "";
let isOpen = false; // ← tracks whether dropdown is visible
// ── AbortController lets us remove the document listener in one call ──
const ac = new AbortController();
trigger.addEventListener("click", () => {
isOpen = dropdown.classList.toggle("open");
trigger.classList.toggle("open", isOpen);
if (isOpen) {
search.value = "";
renderOpts(allItems);
search.focus();
}
});
document.addEventListener("click", e => {
if (!wrap.contains(e.target)) {
isOpen = false;
dropdown.classList.remove("open");
trigger.classList.remove("open");
}
}, { signal: ac.signal });
search.addEventListener("input", () => {
const q = search.value.trim().toLowerCase();
renderOpts(q ? allItems.filter(s => s.toLowerCase().includes(q)) : allItems);
});
list.addEventListener("click", e => {
const opt = e.target.closest("[data-value]");
if (!opt) return;
current = opt.dataset.value;
label.textContent = current || allLabel;
isOpen = false;
dropdown.classList.remove("open");
trigger.classList.remove("open");
onChange(current);
});
// ── Watch for wrap being removed from DOM → abort the document listener ──
const observer = new MutationObserver(() => {
if (!document.contains(wrap)) {
ac.abort(); // removes the document click listener
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
function renderOpts(items) {
// ── Only exclude current when the dropdown is open (user is browsing) ──
// ── When called from setItems (background refresh), show everything ─
const switchable = (isOpen && current)
? items.filter(n => n !== current)
: items;
const clearRow = `<div class="${prefix}-opt${current === "" ? " active" : ""}" data-value="">
<i class="${allIcon}"></i> ${escHtml(allLabel)}
${isOpen && current !== ""
? `<span style="margin-left:auto;font-size:.72rem;color:var(--text-muted);opacity:.7">clear</span>`
: ""}
</div>`;
if (!switchable.length) {
list.innerHTML = clearRow +
`<div style="padding:12px;text-align:center;font-size:.85rem;color:#6b8890">
${isOpen && current ? "No other departments" : "No results found"}
</div>`;
return;
}
list.innerHTML = clearRow + switchable.map(n =>
`<div class="${prefix}-opt${!isOpen && n === current ? " active" : ""}"
data-value="${escAttr(n)}">
<i class="${itemIcon}"></i> ${escHtml(n)}
</div>`
).join("");
}
function setItems(newList) {
allItems = (newList || []).filter(Boolean);
renderOpts(allItems); // isOpen is false here → full list rendered, active class applied
}
return { setItems, getCurrent: () => current };
}
/* ── Convenience wrappers (keep back-compat names) ───────────────── */
function initDepartmentDropdown(wrap, onChange) {
return initSearchDropdown(wrap, onChange, {
cssPrefix: "inv-dep",
allLabel: "All Departments",
allIcon: "fas fa-th-large",
itemIcon: "fas fa-store"
});
}
/* ── Card HTML builder ───────────────────────────── */
function buildCardHtml(item, footerHtml) {
const DESC_THRESHOLD = 80; // chars — expand toggle only if longer than this
const desc = (item.ItemDescription ?? "").trim();
const hasLongDesc = desc.length > DESC_THRESHOLD;
const fmt = v => v != null ? Number(v).toLocaleString("en-US", {
minimumFractionDigits: 2, maximumFractionDigits: 2
}) : "—";
const createdDate = item.createdDate
? new Date(item.createdDate).toLocaleDateString("en-US",
{ year: "numeric", month: "short", day: "numeric" })
: "—";
const lotPart = item.LotNo
? ` &nbsp;·&nbsp; Lot: ${escHtml(item.LotNo)}`
: "";
const descBlock = !desc ? "" : hasLongDesc
? `<div class="inv-desc-wrap">
<div class="inv-desc-header" onclick="
this.querySelector('.inv-desc-caret').classList.toggle('open');
this.nextElementSibling.classList.toggle('open');">
<span class="inv-desc-lbl">
<i class="fas fa-align-left"></i> Description
</span>
<i class="fas fa-chevron-down inv-desc-caret"></i>
</div>
<div class="inv-desc-body">${escHtml(desc)}</div>
</div>`
: `<div class="inv-desc-wrap">
<div style="padding:8px 11px;">
<span class="inv-desc-lbl" style="margin-bottom:4px;">
<i class="fas fa-align-left"></i> Description
</span>
<div style="font-size:.82rem;color:var(--text-dark);line-height:1.5;margin-top:4px;">
${escHtml(desc)}
</div>
</div>
</div>`;
return `
<div class="inv-card">
<div class="inv-card-hd">
<div class="inv-card-code">ITEMNO #${escHtml(String(item.itemNo))}${lotPart}</div>
<div class="inv-card-name">${escHtml(item.itemName ?? "—")}</div>
${(item.department || item.itemCategoryName) ? `
<div class="inv-card-sub">
${item.department ? `<i class="fas fa-building"></i> ${escHtml(item.department)}` : ""}
${item.department && item.itemCategoryName ? `&nbsp;·&nbsp;` : ""}
${item.itemCategoryName ? `<i class="fas fa-tag"></i> ${escHtml(item.itemCategoryName)}` : ""}
</div>
<div class="inv-card-sub">
<i class="fas fa-clock"></i>
${escHtml(createdDate ?? "—")}
</div>
<div class="inv-card-sub">
<i class="fas fa-qrcode"></i>
${escHtml(item.projectCode ?? "—")}
</div>
` : ""}
</div>
<div class="inv-card-body">
<div class="inv-agg-row">
<div class="inv-agg-badge">
<span class="inv-agg-lbl"><i class="fas fa-arrow-alt-circle-down"></i> Qty In</span>
<span class="inv-agg-val">${fmt(item.qtyIn)}</span>
</div>
<div class="inv-agg-badge">
<span class="inv-agg-lbl"><i class="fas fa-arrow-alt-circle-up"></i> Qty Out</span>
<span class="inv-agg-val">${fmt(item.qtyOut)}</span>
</div>
</div>
<div class="inv-agg-row">
<div class="inv-agg-badge">
<span class="inv-agg-lbl"><i class="fas fa-layer-group"></i> On Hand</span>
<span class="inv-agg-val">${fmt(item.qtyOnHand)}</span>
</div>
<div class="inv-agg-badge">
<span class="inv-agg-lbl"><i class="fas fa-history"></i> Remaining</span>
<span class="inv-agg-val">${fmt(item.remainingQty)}</span>
</div>
</div>
${descBlock}
<div>
<div class="inv-item-lbl"><i class="fas fa-box"></i> Item</div>
<div class="inv-item-row">
<span class="inv-item-name" title="${escAttr(item.itemName ?? "")}">
${escHtml(item.itemName ?? "—")}
</span>
<span class="inv-item-qty">
<i class="fas fa-cubes"></i> ${fmt(item.qtyIn)}
</span>
</div>
</div>
</div>
<div class="inv-card-ft">${footerHtml(item)}</div>
</div>`;
}
return {
splitAggr,
escHtml,
escAttr,
buildPageRange,
mkPageBtn,
renderPagination,
buildCardHtml,
initSearchDropdown,
initDepartmentDropdown,
};
})();
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,738 @@
<style>
.tm-type-opt {
border: 1.5px solid var(--border, #d6eaec);
border-radius: 12px;
padding: 12px;
cursor: pointer;
transition: all .15s;
display: flex;
flex-direction: column;
gap: 6px;
}
.tm-type-opt:hover {
border-color: var(--teal-mid, #0e7c86);
background: var(--teal-pale, #e6f7f8);
}
.tm-type-opt.selected {
border-color: var(--teal-mid, #0e7c86);
background: var(--teal-pale, #e6f7f8);
}
.tm-type-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
}
.tm-icon-ris {
background: #e6f7f8;
color: #0e7c86;
}
.tm-icon-mrs {
background: #E6F1FB;
color: #185FA5;
}
.tm-type-opt.selected .tm-icon-ris {
background: #0e7c86;
color: #fff;
}
.tm-type-opt.selected .tm-icon-mrs {
background: #185FA5;
color: #fff;
}
.tm-stock-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
background: var(--teal-pale, #e6f7f8);
border: 1px solid var(--border, #d6eaec);
border-radius: 8px;
font-size: 13px;
color: var(--text-dark, #1a2e35);
}
.tm-stock-badge i {
color: var(--teal-mid, #0e7c86);
}
.tm-stock-badge strong {
margin-left: auto;
color: var(--teal-dark, #0d5c63);
}
.tm-form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.tm-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted, #6b8890);
text-transform: uppercase;
letter-spacing: .05em;
display: flex;
align-items: center;
gap: 5px;
}
.tm-label i {
font-size: 11px;
}
.tm-req {
color: #e53e3e;
}
.tm-input {
padding: 8px 10px;
border: 1.5px solid var(--border, #d6eaec);
border-radius: 8px;
background: #fff;
color: var(--text-dark, #1a2e35);
font-family: 'DM Sans', sans-serif;
font-size: 13px;
width: 100%;
outline: none;
transition: border-color .2s;
}
.tm-input:focus {
border-color: var(--teal-mid, #0e7c86);
}
.tm-input.error {
border-color: #e53e3e;
}
.tm-warn {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: #c53030;
}
.tm-hint {
font-size: 11px;
color: var(--text-muted, #6b8890);
}
.tm-info-box {
display: flex;
gap: 8px;
align-items: flex-start;
padding: 10px 12px;
background: #E6F1FB;
border: 1px solid #B5D4F4;
border-radius: 8px;
font-size: 12px;
color: #0C447C;
line-height: 1.5;
}
.tm-btn-cancel {
padding: 8px 18px;
border-radius: 8px;
border: 1.5px solid var(--border, #d6eaec);
background: transparent;
color: var(--text-muted, #6b8890);
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
font-weight: 600;
cursor: pointer;
}
.tm-btn-submit {
padding: 8px 20px;
border-radius: 8px;
border: none;
background: var(--teal-mid, #0e7c86);
color: #fff;
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 7px;
transition: background .18s;
}
.tm-btn-submit:hover:not(:disabled) {
background: var(--teal-dark, #0d5c63);
}
.tm-btn-submit:disabled {
opacity: .5;
cursor: default;
}
</style>
<script>
window.TransactModal = (function () {
"use strict";
const H = window.InventoryHelpers;
// ── State ────────────────────────────────────────────────────────────────
let _ctx = null; // TransactContextDto from server
let _activeType = "ris"; // "ris" | "mrs"
let _onSuccess = null; // callback after successful submit
// ── DOM refs (resolved once modal is injected) ────────────────────────────
let modal, overlay, form, btnSubmit, submitLabel;
let optRIS, optMRS, formRIS, formMRS;
// ── RIS fields ────────────────────────────────────────────────────────────
let risItemBadge, risOnHand, risDiscipline, risIssuedTo, risQty,
risPRRef, risRemarks, risQtyWarn;
// ── MRS fields ────────────────────────────────────────────────────────────
let mrsRISSelect, mrsQty, mrsCondition, mrsRemarks,
mrsQtyWarn, mrsMaxHint;
// ════════════════════════════════════════════════════════════════════════
// PUBLIC API
// ════════════════════════════════════════════════════════════════════════
async function open(inventoryId, onSuccessCallback) {
_onSuccess = onSuccessCallback || null;
_injectModalHtml();
_bindRefs();
_showLoading(true);
_showOverlay(true);
try {
const res = await fetch(
`/InventoryMgmt/GetTransactContext?inventoryId=${inventoryId}`
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
_ctx = json.data ?? json;
} catch (err) {
showToast("error", "Could not load item context.", "Error", 4000);
_showOverlay(false);
return;
}
_populateContext();
_selectType("ris");
_showLoading(false);
}
// ════════════════════════════════════════════════════════════════════════
// MODAL HTML INJECTION
// ════════════════════════════════════════════════════════════════════════
function _injectModalHtml() {
const existing = document.getElementById("transact-modal-overlay");
if (existing) existing.remove();
document.body.insertAdjacentHTML("beforeend", `
<div id="transact-modal-overlay" style="
position:fixed;inset:0;z-index:1080;
background:rgba(0,0,0,.45);
display:flex;align-items:center;justify-content:center;
padding:16px">
<div id="transact-modal" style="
background:var(--card-bg,#fff);
border-radius:14px;
border:1px solid var(--border,#d6eaec);
width:100%;max-width:520px;
overflow:hidden;
box-shadow:0 20px 60px rgba(0,0,0,.2)">
<!-- HEAD -->
<div style="background:linear-gradient(135deg,#0d5c63,#0e7c86);
padding:16px 18px;display:flex;align-items:center;
justify-content:space-between">
<div style="display:flex;align-items:center;gap:12px">
<div style="width:38px;height:38px;border-radius:10px;
background:rgba(255,255,255,.15);
display:flex;align-items:center;justify-content:center">
<i class="fas fa-transfer-alt" style="color:#fff;font-size:16px"></i>
</div>
<div>
<div style="font-size:14px;font-weight:600;color:#fff">
New Transaction
</div>
<div id="tm-subtitle" style="font-size:11px;color:rgba(255,255,255,.65);margin-top:2px">
Loading…
</div>
</div>
</div>
<button id="tm-close" style="
width:30px;height:30px;border-radius:8px;
background:rgba(255,255,255,.12);
border:1px solid rgba(255,255,255,.2);
color:rgba(255,255,255,.85);cursor:pointer;
display:flex;align-items:center;justify-content:center">
<i class="fas fa-times" style="font-size:13px"></i>
</button>
</div>
<!-- LOADING STATE -->
<div id="tm-loading" style="
display:flex;align-items:center;justify-content:center;
gap:12px;padding:60px 20px;color:var(--text-muted,#6b8890)">
<div class="inv-spinner"></div>
<span style="font-size:.9rem">Loading…</span>
</div>
<!-- BODY (hidden until loaded) -->
<div id="tm-body" style="display:none">
<!-- TYPE PICKER -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;
padding:14px 16px;border-bottom:1px solid var(--border,#d6eaec)">
<div id="tm-opt-ris" class="tm-type-opt" data-type="ris">
<div class="tm-type-icon tm-icon-ris">
<i class="fas fa-file-export"></i>
</div>
<div style="font-size:13px;font-weight:600;color:var(--text-dark,#1a2e35)">
Return Issuance Slip
</div>
<div style="font-size:11px;color:var(--text-muted,#6b8890);line-height:1.4">
Issue items out of inventory
</div>
</div>
<div id="tm-opt-mrs" class="tm-type-opt" data-type="mrs">
<div class="tm-type-icon tm-icon-mrs">
<i class="fas fa-file-import"></i>
</div>
<div style="font-size:13px;font-weight:600;color:var(--text-dark,#1a2e35)">
Material Return Slip
</div>
<div style="font-size:11px;color:var(--text-muted,#6b8890);line-height:1.4">
Return unused items to stock
</div>
</div>
</div>
<!-- RIS FORM -->
<div id="tm-form-ris" style="padding:14px 16px;display:flex;flex-direction:column;gap:12px">
<div id="tm-ris-stock-badge" class="tm-stock-badge"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-tools"></i> Discipline <span class="tm-req">*</span>
</label>
<select id="tm-ris-discipline" class="tm-input">
<option value="">Select discipline…</option>
</select>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-user"></i> Issued to <span class="tm-req">*</span>
</label>
<input id="tm-ris-issuedto" class="tm-input"
type="text" placeholder="Name or user ID…">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-cubes"></i> Qty to issue <span class="tm-req">*</span>
</label>
<input id="tm-ris-qty" class="tm-input"
type="number" min="1" placeholder="0">
<span id="tm-ris-qty-warn" class="tm-warn" style="display:none">
<i class="fas fa-exclamation-triangle"></i>
<span id="tm-ris-qty-warn-text"></span>
</span>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-file-alt"></i> PR reference
</label>
<input id="tm-ris-prref" class="tm-input"
type="text" placeholder="PR-2026-XXXX">
</div>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-sticky-note"></i> Remarks
</label>
<input id="tm-ris-remarks" class="tm-input"
type="text" placeholder="Optional notes…">
</div>
</div>
<!-- MRS FORM -->
<div id="tm-form-mrs" style="padding:14px 16px;display:none;flex-direction:column;gap:12px">
<div class="tm-info-box">
<i class="fas fa-info-circle" style="color:#185FA5;flex-shrink:0;margin-top:2px"></i>
<span>Select the original RIS for this return.
Only the qty issued on that slip can be returned.</span>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-receipt"></i> Original RIS reference <span class="tm-req">*</span>
</label>
<select id="tm-mrs-ris" class="tm-input">
<option value="">Select RIS…</option>
</select>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-cubes"></i> Qty to return <span class="tm-req">*</span>
</label>
<input id="tm-mrs-qty" class="tm-input"
type="number" min="1" placeholder="0">
<span id="tm-mrs-max-hint" class="tm-hint" style="display:none"></span>
<span id="tm-mrs-qty-warn" class="tm-warn" style="display:none">
<i class="fas fa-exclamation-triangle"></i>
<span id="tm-mrs-qty-warn-text"></span>
</span>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-tag"></i> Condition
</label>
<select id="tm-mrs-condition" class="tm-input">
<option value="Good">Good</option>
<option value="Damaged">Damaged</option>
<option value="Partial">Partial</option>
</select>
</div>
</div>
<div class="tm-form-group">
<label class="tm-label">
<i class="fas fa-sticky-note"></i> Remarks
</label>
<input id="tm-mrs-remarks" class="tm-input"
type="text" placeholder="Reason for return…">
</div>
</div>
</div><!-- /tm-body -->
<!-- FOOTER -->
<div style="
padding:12px 16px;
border-top:1px solid var(--border,#d6eaec);
background:var(--bg-page,#f0f6f7);
display:flex;align-items:center;justify-content:flex-end;gap:8px">
<button id="tm-cancel" class="tm-btn-cancel">Cancel</button>
<button id="tm-submit" class="tm-btn-submit" disabled>
<i class="fas fa-check"></i>
<span id="tm-submit-label">Create RIS</span>
</button>
</div>
</div>
</div>`);
}
// ════════════════════════════════════════════════════════════════════════
// BIND DOM REFS + EVENTS
// ════════════════════════════════════════════════════════════════════════
function _bindRefs() {
overlay = document.getElementById("transact-modal-overlay");
modal = document.getElementById("transact-modal");
btnSubmit = document.getElementById("tm-submit");
submitLabel = document.getElementById("tm-submit-label");
optRIS = document.getElementById("tm-opt-ris");
optMRS = document.getElementById("tm-opt-mrs");
formRIS = document.getElementById("tm-form-ris");
formMRS = document.getElementById("tm-form-mrs");
// RIS fields
risItemBadge = document.getElementById("tm-ris-stock-badge");
risDiscipline = document.getElementById("tm-ris-discipline");
risIssuedTo = document.getElementById("tm-ris-issuedto");
risQty = document.getElementById("tm-ris-qty");
risPRRef = document.getElementById("tm-ris-prref");
risRemarks = document.getElementById("tm-ris-remarks");
risQtyWarn = document.getElementById("tm-ris-qty-warn");
// MRS fields
mrsRISSelect = document.getElementById("tm-mrs-ris");
mrsQty = document.getElementById("tm-mrs-qty");
mrsCondition = document.getElementById("tm-mrs-condition");
mrsRemarks = document.getElementById("tm-mrs-remarks");
mrsQtyWarn = document.getElementById("tm-mrs-qty-warn");
mrsMaxHint = document.getElementById("tm-mrs-max-hint");
// Type picker
[optRIS, optMRS].forEach(el =>
el.addEventListener("click", () => _selectType(el.dataset.type))
);
// Close / Cancel
document.getElementById("tm-close").addEventListener("click", _close);
document.getElementById("tm-cancel").addEventListener("click", _close);
overlay.addEventListener("click", e => { if (e.target === overlay) _close(); });
// Qty live validation
risQty.addEventListener("input", _validateRISQty);
mrsRISSelect.addEventListener("change", _onMRSRISChange);
mrsQty.addEventListener("input", _validateMRSQty);
// Submit
btnSubmit.addEventListener("click", _handleSubmit);
}
// ════════════════════════════════════════════════════════════════════════
// POPULATE FROM CONTEXT
// ════════════════════════════════════════════════════════════════════════
function _populateContext() {
// Header subtitle
document.getElementById("tm-subtitle").textContent =
`${H.escHtml(_ctx.itemName)} · Item #${_ctx.itemNo}`;
document.getElementById("tm-ris-prref").value = _ctx.prNo ?? "";
// Stock badge
risItemBadge.innerHTML = `
<i class="fas fa-layer-group"></i>
<span>On hand</span>
<strong>${_ctx.qtyOnHand} pcs</strong>
<span style="margin-left:8px;color:var(--text-muted,#6b8890)">
In: ${_ctx.qtyIn} · Out: ${_ctx.qtyOut}
</span>`;
// Discipline dropdown
risDiscipline.innerHTML = `
<option value="">Select discipline…</option>` +
(_ctx.disciplines || []).map(d =>
`
<option value="${d.disciplineId}">${H.escHtml(d.disciplineName)}</option>`
).join("");
// MRS — open RIS list
const hasRIS = _ctx.openRISList && _ctx.openRISList.length > 0;
mrsRISSelect.innerHTML = `
<option value="">Select RIS…</option>` +
(hasRIS
? _ctx.openRISList.map(r =>
`
<option value="${r.risId}"
data-max="${r.qtyAvailableToReturn}"
data-discipline="${H.escAttr(r.disciplineName)}">
${H.escHtml(r.risNo)} — ${r.qtyAvailableToReturn} pcs avail — ${H.escHtml(r.disciplineName)}
</option>`)
.join("")
: "");
if (!hasRIS) {
optMRS.style.opacity = ".45";
optMRS.style.cursor = "not-allowed";
optMRS.title = "No approved RIS records with remaining qty for this item.";
optMRS.onclick = null;
}
}
// ════════════════════════════════════════════════════════════════════════
// TYPE SWITCHING
// ════════════════════════════════════════════════════════════════════════
function _selectType(type) {
_activeType = type;
const isRIS = type === "ris";
optRIS.classList.toggle("selected", isRIS);
optMRS.classList.toggle("selected", !isRIS);
formRIS.style.display = isRIS ? "flex" : "none";
formMRS.style.display = !isRIS ? "flex" : "none";
submitLabel.textContent = isRIS ? "Create RIS" : "Create MRS";
btnSubmit.disabled = false;
_clearErrors();
}
// ════════════════════════════════════════════════════════════════════════
// VALIDATION
// ════════════════════════════════════════════════════════════════════════
function _validateRISQty() {
const val = parseInt(risQty.value, 10);
const max = _ctx?.qtyOnHand ?? 0;
const over = !isNaN(val) && val > max;
const zero = !isNaN(val) && val < 1;
document.getElementById("tm-ris-qty-warn-text").textContent =
over ? `Cannot exceed ${max} on hand.`
: zero ? "Must be at least 1." : "";
risQtyWarn.style.display = (over || zero) ? "flex" : "none";
risQty.classList.toggle("error", over || zero);
}
function _onMRSRISChange() {
const opt = mrsRISSelect.selectedOptions[0];
const max = opt ? parseInt(opt.dataset.max, 10) : 0;
if (opt && opt.value) {
mrsMaxHint.style.display = "block";
mrsMaxHint.textContent = `Max returnable: ${max} pcs`;
mrsQty.max = max;
} else {
mrsMaxHint.style.display = "none";
mrsQty.max = "";
}
_validateMRSQty();
}
function _validateMRSQty() {
const val = parseInt(mrsQty.value, 10);
const opt = mrsRISSelect.selectedOptions[0];
const max = opt ? parseInt(opt.dataset.max, 10) : Infinity;
const over = !isNaN(val) && val > max;
const zero = !isNaN(val) && val < 1;
document.getElementById("tm-mrs-qty-warn-text").textContent =
over ? `Cannot exceed ${max} available.`
: zero ? "Must be at least 1." : "";
mrsQtyWarn.style.display = (over || zero) ? "flex" : "none";
mrsQty.classList.toggle("error", over || zero);
}
function _clearErrors() {
[risQty, risIssuedTo, risDiscipline, mrsRISSelect, mrsQty].forEach(el => {
if (el) el.classList.remove("error");
});
[risQtyWarn, mrsQtyWarn].forEach(el => {
if (el) el.style.display = "none";
});
}
function _validateForm() {
let valid = true;
if (_activeType === "ris") {
if (!risDiscipline.value) { risDiscipline.classList.add("error"); valid = false; }
if (!risIssuedTo.value.trim()) { risIssuedTo.classList.add("error"); valid = false; }
const qty = parseInt(risQty.value, 10);
if (isNaN(qty) || qty < 1 || qty > _ctx.qtyOnHand) {
risQty.classList.add("error"); valid = false;
}
} else {
if (!mrsRISSelect.value) { mrsRISSelect.classList.add("error"); valid = false; }
const qty = parseInt(mrsQty.value, 10);
const opt = mrsRISSelect.selectedOptions[0];
const maxRet = opt ? parseInt(opt.dataset.max, 10) : 0;
if (isNaN(qty) || qty < 1 || qty > maxRet) {
mrsQty.classList.add("error"); valid = false;
}
}
return valid;
}
// ════════════════════════════════════════════════════════════════════════
// SUBMIT
// ════════════════════════════════════════════════════════════════════════
async function _handleSubmit() {
_clearErrors();
if (!_validateForm()) {
showToast("warning", "Please fix the highlighted fields.", "Validation", 3000);
return;
}
const confirmed = await showConfirmation({
title: _activeType === "ris" ? "Create Return Issuance Slip" : "Create Material Return Slip",
message: _activeType === "ris"
? `Issue <strong>${risQty.value} pcs</strong> from inventory?`
: `Return <strong>${mrsQty.value} pcs</strong> back to stock?`,
type: "warning",
confirmText: _activeType === "ris" ? "Create RIS" : "Create MRS",
cancelText: "Cancel"
});
if (!confirmed) return;
btnSubmit.disabled = true;
btnSubmit.querySelector("i").className = "fas fa-spinner fa-spin";
try {
const payload = _activeType === "ris"
? {
InventoryId: _ctx.inventoryId,
PRDetailId: parseInt(risPRRef.value, 10) || 0,
IssuedTo: risIssuedTo.value.trim(),
DisciplineId: parseInt(risDiscipline.value, 10),
QtyIssued: parseInt(risQty.value, 10),
Remarks: risRemarks.value.trim() || null
}
: {
RISId: parseInt(mrsRISSelect.value, 10),
QtyReturned: parseInt(mrsQty.value, 10),
ReturnedBy: _ctx.department || "N/A",
Condition: mrsCondition.value,
Remarks: mrsRemarks.value.trim() || null
};
const endpoint = _activeType === "ris"
? "/RISMgmt/CreateRIS"
: "/MRSMgmt/CreateMRS";
const res = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const json = await res.json();
if (!res.ok || !json.success) {
showToast("error", json.message ?? "An error occurred.", "Failed", 4000);
return;
}
showToast("success", json.message, "Done!", 3500);
const onSuccess = _onSuccess;
_close();
if (typeof onSuccess === "function") onSuccess();
} catch (err) {
showToast("error", "Request failed. Please try again.", "Error", 4000);
} finally {
btnSubmit.disabled = false;
btnSubmit.querySelector("i").className = "fas fa-check";
}
}
// ════════════════════════════════════════════════════════════════════════
// HELPERS
// ════════════════════════════════════════════════════════════════════════
function _showOverlay(show) {
const el = document.getElementById("transact-modal-overlay");
if (el) el.style.display = show ? "flex" : "none";
}
function _showLoading(loading) {
document.getElementById("tm-loading").style.display = loading ? "flex" : "none";
document.getElementById("tm-body").style.display = loading ? "none" : "block";
if (btnSubmit) btnSubmit.disabled = loading;
}
function _close() {
document.getElementById("transact-modal-overlay")?.remove();
_ctx = null;
_onSuccess = null;
}
return { open };
})();
</script>

View File

@ -18,6 +18,6 @@
<link href="~/lib/jquery-ui-1132custom/jquery-ui.css" rel="stylesheet" />
<link href="~/lib/jquery-ui-1132custom/jquery-ui.min.css" rel="stylesheet" />
<link href="~/css/StyleConflict.css" rel="stylesheet" />
<link href="~/css/toast-notifications.css" rel="stylesheet" />
<link href="~/css/confirmation-modal.css" rel="stylesheet" />
<link href="~/css/toast-notificationsV2.css" rel="stylesheet" />
<link href="~/css/confirmation-modalV2.css" rel="stylesheet" />

View File

@ -18,5 +18,5 @@
<script src="~/datatables/pdfmake-0.2.7/pdfmake.min.js"></script>
<script src="~/datatables/pdfmake-0.2.7/vfs_fonts.js"></script>
<script src="~/js/toast-notifications.js"></script>
<script src="~/js/confirmation-modal.js"></script>
<script src="~/js/confirmation-modalV2.js"></script>

View File

@ -113,6 +113,8 @@
"PutSuppBidDetails": "api/CanvassMgmt/PutSuppBidDetails/",
"PostPutMySupplier": "api/CanvassMgmt/PostPutMySupplier/",
"PostPutItemTagging": "api/CanvassMgmt/PostPutItemTagging/",
"PostSupplierForCanvass": "api/CanvassMgmt/PostSupplierForCanvass/",
"StartCanvass": "api/CanvassMgmt/StartCanvass/",
"UnlockFormLink": "api/CanvassMgmt/UnlockFormLink/"
},
"POMgmt": {
@ -188,10 +190,16 @@
"GetLotNo": "api/InventoryMgmt/GetLotNo/",
"GetLotQtyByItem": "api/InventoryMgmt/GetLotQtyByItem/",
"GetLotNoById": "api/InventoryMgmt/GetLotNoById/",
"GetInventory": "api/InventoryMgmt/GetInventory/",
"GetTransactContextAsync": "api/InventoryMgmt/GetTransactContextAsync/",
"PostPutReqApproval": "api/InventoryMgmt/PostPutReqApproval/",
"PostPutLotNo": "api/InventoryMgmt/PostPutLotNo/",
"PostPutReqItems": "api/InventoryMgmt/PostPutReqItems/",
"PostPutLotBin": "api/InventoryMgmt/PostPutLotBin/"
"PostPutLotBin": "api/InventoryMgmt/PostPutLotBin/",
"ApproveRIS": "api/RISMgmt/ApproveRIS/",
"CancelRIS": "api/RISMgmt/CancelRIS/",
"CreateRIS": "api/RISMgmt/",
"GetRIS": "api/RISMgmt/GetRIS/"
},
"ImageUploadSettings": {
"UploadPath": "C:\\WebApps\\wwwroot\\Content\\Images"

View File

@ -0,0 +1,161 @@
(function () {
'use strict';
const tabConfig = {
'Inventory-inv-per-supplier': {
title: 'inv Per (Supplier)',
tableId: 1,
endpoint: '/InventoryMgmt/GetTabbedById'
},
'Inventory-inv-per-item': {
title: 'inv Per (Item)',
tableId: 2,
endpoint: '/InventoryMgmt/GetTabbedById'
},
'Inventory-suppliers': {
title: 'Supplier Management',
tableId: 3,
endpoint: '/InventoryMgmt/GetTabbedById'
}
};
let currentTab = 'Inventory-inv-per-supplier';
let isSwitching = false;
function init() {
const tabs = document.querySelectorAll('.Inventory-tab-btn');
tabs.forEach(tab => {
tab.addEventListener('click', handleTabClick);
});
loadTabContent('Inventory-inv-per-supplier');
}
function handleTabClick(e) {
if (isSwitching) return;
const btn = e.currentTarget;
const tabId = btn.getAttribute('data-tab');
if (tabId === currentTab) return;
switchTab(tabId, btn);
}
function switchTab(tabId, btn) {
if (!tabConfig[tabId]) {
console.error('Invalid tab ID:', tabId);
return;
}
isSwitching = true;
document.querySelectorAll('.Inventory-tab-btn').forEach(b => {
b.classList.remove('active');
});
btn.classList.add('active');
updateTitle(tabConfig[tabId].title);
loadTabContent(tabId, btn);
currentTab = tabId;
}
function updateTitle(title) {
const titleEl = document.getElementById('pageTitle');
if (!titleEl) return;
titleEl.classList.add('updating');
setTimeout(() => {
titleEl.textContent = title;
void titleEl.offsetWidth;
titleEl.classList.remove('updating');
}, 250);
}
function loadTabContent(tabId, btn = null) {
const config = tabConfig[tabId];
if (!config) {
console.error('Tab config not found:', tabId);
isSwitching = false;
return;
}
if (btn) {
btn.classList.add('loading');
}
showContainerLoading(true);
InventoryTabbedComponent(config.tableId, config.endpoint, function (success) {
if (btn) {
btn.classList.remove('loading');
}
showContainerLoading(false);
isSwitching = false;
if (!success) {
console.error('Failed to load tab content');
}
});
}
function showContainerLoading(isLoading) {
const container = document.getElementById('TabbedContainer');
if (!container) return;
if (isLoading) {
container.style.opacity = '0.5';
container.style.pointerEvents = 'none';
} else {
container.style.opacity = '1';
container.style.pointerEvents = 'auto';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.PRTabs = {
switchTo: function (tabId) {
const btn = document.querySelector(`.Inventory-tab-btn[data-tab="${tabId}"]`);
if (btn && !isSwitching) {
switchTab(tabId, btn);
}
},
reload: function () {
loadTabContent(currentTab);
},
getCurrent: function () {
return currentTab;
},
addTab: function (tabId, config) {
tabConfig[tabId] = config;
}
};
})();
function InventoryTabbedComponent(id, endpoint, callback) {
$.ajax({
url: endpoint,
type: 'GET',
data: { TableId: id },
success: function (response) {
$('#TabbedContainer').html(response);
if (callback) callback(true);
},
error: function (xhr, status, error) {
console.error("Error loading component:", error);
$('#TabbedContainer').html('<div class="alert alert-danger">Failed to load content. Please try again.</div>');
if (callback) callback(false);
}
});
}

View File

@ -1,5 +1,5 @@
.confirmation-modal {
z-index: 9999;
z-index: 99999 !important;
background-color: rgba(0, 0, 0, 0.6);
}

View File

@ -2,7 +2,7 @@
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
z-index: 99999 !important;
max-width: 400px;
}

View File

@ -17,38 +17,46 @@
const config = {
title: 'Confirm Action',
message: 'Are you sure you want to proceed?',
type: 'warning', // warning, danger, info, success
type: 'warning',
confirmText: 'Confirm',
cancelText: 'Cancel',
size: 'md', // sm, md, lg, xl
size: 'md',
...options
};
const modalId = `confirmation-modal-${++this.modalCount}`;
const modal = this.createModal(modalId, config);
this.container.appendChild(modal);
// ── Append directly to body, AFTER everything else ────────────
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal, {
backdrop: 'static',
keyboard: false
});
// Handle confirm button
modal.addEventListener('shown.bs.modal', () => {
// Force modal above transact overlay
modal.style.zIndex = '99999';
// Grab the very last backdrop Bootstrap added
const backdrops = document.querySelectorAll('.modal-backdrop');
const lastBackdrop = backdrops[backdrops.length - 1];
if (lastBackdrop) lastBackdrop.style.zIndex = '99998';
});
const confirmBtn = modal.querySelector('.btn-confirm');
confirmBtn.addEventListener('click', () => {
bsModal.hide();
resolve(true);
});
// Handle cancel button
const cancelBtn = modal.querySelector('.btn-cancel');
cancelBtn.addEventListener('click', () => {
bsModal.hide();
resolve(false);
});
// Handle modal close (X button or backdrop)
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
if (!confirmBtn.clicked && !cancelBtn.clicked) {
@ -56,7 +64,6 @@
}
});
// Mark buttons as clicked to differentiate from modal close
confirmBtn.addEventListener('click', () => confirmBtn.clicked = true);
cancelBtn.addEventListener('click', () => cancelBtn.clicked = true);