RIS creation, approval and Cancel working well
This commit is contained in:
parent
6710f04bd7
commit
44862d01b5
@ -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);
|
||||
|
||||
19
CPRNIMS.Domain/Contracts/Inventory/IMRS.cs
Normal file
19
CPRNIMS.Domain/Contracts/Inventory/IMRS.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
22
CPRNIMS.Domain/Contracts/Inventory/IRIS.cs
Normal file
22
CPRNIMS.Domain/Contracts/Inventory/IRIS.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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,9 +21,92 @@ 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 inv = await _dbContext.Inventories
|
||||
.Where(i => i.InventoryId == inventoryId && i.IsActive)
|
||||
.Select(i => new
|
||||
{
|
||||
i.InventoryId,
|
||||
i.QtyOnHand,
|
||||
i.QtyIn,
|
||||
i.QtyOut,
|
||||
|
||||
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
|
||||
{
|
||||
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}'")
|
||||
@ -31,21 +114,90 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
|
||||
return allItems ?? new List<Infrastructure.Entities.Inventory.ItemDetail>();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
public async Task<PagedResult<InventoryResponse>> GetInventory(InventoryRequest request, CancellationToken ct)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
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"])
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<List<Infrastructure.Entities.Inventory.Inventory>>
|
||||
GetInventoryByUserId(InventoryDto itemDto)
|
||||
await conn.CloseAsync();
|
||||
|
||||
return new PagedResult<InventoryResponse>
|
||||
{
|
||||
try
|
||||
Data = items,
|
||||
TotalCount = totalCount,
|
||||
PageNumber = request.PageNumber,
|
||||
PageSize = request.PageSize,
|
||||
DepartmentList = departmentList
|
||||
};
|
||||
}
|
||||
public async Task<List<InventoryByIdResponse>> GetInventoryById(InventoryRequest itemDto,CancellationToken ct)
|
||||
{
|
||||
if(itemDto.IsSorting == false)
|
||||
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)
|
||||
{
|
||||
itemDto.DateFrom=DateTime.Now;
|
||||
if (itemDto.IsSorting == false)
|
||||
{
|
||||
itemDto.DateFrom = DateTime.Now;
|
||||
itemDto.DateTo = DateTime.Now;
|
||||
}
|
||||
var allItems = await _dbContext.Inventories
|
||||
@ -58,16 +210,8 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
|
||||
return allItems ?? new List<Infrastructure.Entities.Inventory.Inventory>();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Lot>> GetLotNoById(InventoryDto itemDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allItems = await _dbContext.Lots
|
||||
.FromSqlRaw($"EXEC GetLotById @UserId = '{itemDto.UserId}'")
|
||||
@ -75,12 +219,6 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
|
||||
return allItems ?? new List<Lot>();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Lot>> GetLotNo(InventoryDto itemDto)
|
||||
{
|
||||
@ -93,8 +231,6 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
}
|
||||
|
||||
public async Task<Lot> PostPutLotNo(InventoryDto itemDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _dbContext.Database
|
||||
.ExecuteSqlRawAsync("EXEC PostPutLotNo @UserId, @LotId, @LotTypeId,@LotName",
|
||||
@ -104,16 +240,8 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
new SqlParameter("@LotName", itemDto.LotName));
|
||||
return new Lot();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Infrastructure.Entities.Inventory.Inventory> PostPutReqApproval(InventoryDto itemDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _dbContext.Database
|
||||
.ExecuteSqlRawAsync("EXEC PostPutReqApproval @UserId, @ItemNo, @Status, @Remarks",
|
||||
@ -123,16 +251,8 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
new SqlParameter("@Remarks", itemDto.Remarks ?? "N/A"));
|
||||
return new Infrastructure.Entities.Inventory.Inventory();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ItemDetail> PostPutLotBin(InventoryDto itemDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _dbContext.Database
|
||||
.ExecuteSqlRawAsync("EXEC PostPutLotBin @UserId, @LotId, @InventoryId",
|
||||
@ -141,22 +261,14 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
new SqlParameter("@LotId", itemDto.LotId));
|
||||
return new ItemDetail();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (itemDto.LotId == null || itemDto.LotId == 0)
|
||||
{
|
||||
itemDto.LotId = 0;
|
||||
}
|
||||
@ -172,16 +284,8 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
new SqlParameter("@LotId", itemDto.LotId));
|
||||
return new RequestItem();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<RequestItemDetail>> GetRequestedItemByUserId(InventoryDto itemDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allItems = await _dbContext.RequestItemDetails
|
||||
.FromSqlRaw($"EXEC GetRequestedItemByUserId @UserId = '{itemDto.UserId}', " +
|
||||
@ -190,16 +294,8 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
|
||||
return allItems ?? new List<RequestItemDetail>();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<LotQtyByItem>> GetLotQtyByItem(InventoryDto itemDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allItems = await _dbContext.LotQtyByItems
|
||||
.FromSqlRaw($"EXEC GetLotQtyByItem @UserId = '{itemDto.UserId}', " +
|
||||
@ -208,11 +304,5 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
|
||||
return allItems ?? new List<LotQtyByItem>();
|
||||
}
|
||||
catch (SqlException ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
155
CPRNIMS.Domain/Services/Inventory/MRS.cs
Normal file
155
CPRNIMS.Domain/Services/Inventory/MRS.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
307
CPRNIMS.Domain/Services/Inventory/RIS.cs
Normal file
307
CPRNIMS.Domain/Services/Inventory/RIS.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
20
CPRNIMS.Domain/UIContracts/Inventory/IRIS.cs
Normal file
20
CPRNIMS.Domain/UIContracts/Inventory/IRIS.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
170
CPRNIMS.Domain/UIServices/Inventory/RIS.cs
Normal file
170
CPRNIMS.Domain/UIServices/Inventory/RIS.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
CPRNIMS.Infrastructure/Dto/Common/ApiResponse.cs
Normal file
15
CPRNIMS.Infrastructure/Dto/Common/ApiResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
CPRNIMS.Infrastructure/Dto/Inventory/DisciplineDto.cs
Normal file
14
CPRNIMS.Infrastructure/Dto/Inventory/DisciplineDto.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
20
CPRNIMS.Infrastructure/Dto/Inventory/MRSFilterDto.cs
Normal file
20
CPRNIMS.Infrastructure/Dto/Inventory/MRSFilterDto.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
||||
17
CPRNIMS.Infrastructure/Dto/Inventory/MRSPagedResult.cs
Normal file
17
CPRNIMS.Infrastructure/Dto/Inventory/MRSPagedResult.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
20
CPRNIMS.Infrastructure/Dto/Inventory/PagedResult.cs
Normal file
20
CPRNIMS.Infrastructure/Dto/Inventory/PagedResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
21
CPRNIMS.Infrastructure/Dto/Inventory/RISFilterDto.cs
Normal file
21
CPRNIMS.Infrastructure/Dto/Inventory/RISFilterDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
17
CPRNIMS.Infrastructure/Dto/Inventory/RISPagedResult.cs
Normal file
17
CPRNIMS.Infrastructure/Dto/Inventory/RISPagedResult.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
19
CPRNIMS.Infrastructure/Dto/Inventory/RISReferenceDto.cs
Normal file
19
CPRNIMS.Infrastructure/Dto/Inventory/RISReferenceDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
28
CPRNIMS.Infrastructure/Dto/Inventory/Response/MRSResponse.cs
Normal file
28
CPRNIMS.Infrastructure/Dto/Inventory/Response/MRSResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
32
CPRNIMS.Infrastructure/Dto/Inventory/Response/RISResponse.cs
Normal file
32
CPRNIMS.Infrastructure/Dto/Inventory/Response/RISResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
23
CPRNIMS.Infrastructure/Dto/Inventory/TransactContextDto.cs
Normal file
23
CPRNIMS.Infrastructure/Dto/Inventory/TransactContextDto.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
18
CPRNIMS.Infrastructure/Entities/Inventory/Discipline.cs
Normal file
18
CPRNIMS.Infrastructure/Entities/Inventory/Discipline.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
22
CPRNIMS.Infrastructure/Entities/Inventory/InventTrans.cs
Normal file
22
CPRNIMS.Infrastructure/Entities/Inventory/InventTrans.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
18
CPRNIMS.Infrastructure/Entities/Inventory/LotType.cs
Normal file
18
CPRNIMS.Infrastructure/Entities/Inventory/LotType.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
46
CPRNIMS.Infrastructure/Entities/Inventory/MRS.cs
Normal file
46
CPRNIMS.Infrastructure/Entities/Inventory/MRS.cs
Normal 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!;
|
||||
}
|
||||
|
||||
}
|
||||
48
CPRNIMS.Infrastructure/Entities/Inventory/RIS.cs
Normal file
48
CPRNIMS.Infrastructure/Entities/Inventory/RIS.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,6 +147,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Sql\Phase 5\" />
|
||||
<Folder Include="wwwroot\Content\Uploads\PRAttachment\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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)
|
||||
{
|
||||
@ -27,152 +36,76 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
}
|
||||
[HttpPost("GetRequestedItemByUserId")]
|
||||
public async Task<IActionResult> GetRequestedItemByUserId(InventoryDto itemCodeDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
//GetRequestedItemByUserId
|
||||
[HttpPost("GetInventoryById")]
|
||||
public async Task<IActionResult> GetInventoryById(InventoryDto itemCodeDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allPR = await _inventory.GetInventoryById(itemCodeDto);
|
||||
|
||||
return Ok(allPR);
|
||||
}
|
||||
catch (Exception ex)
|
||||
[HttpPost("GetInventory")]
|
||||
public async Task<IActionResult> GetInventory(InventoryRequest request, CancellationToken ct)
|
||||
{
|
||||
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);
|
||||
|
||||
return Ok(allPR);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
|
||||
await PostErrorMessage(message + "GetInventoryById", "WebApi");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
[HttpPost("GetLotNoById")]
|
||||
public async Task<IActionResult> GetLotNoById(InventoryDto itemCodeDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
[HttpPost("GetLotQtyByItem")]
|
||||
public async Task<IActionResult> GetLotQtyByItem(InventoryDto itemCodeDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Post Put
|
||||
[HttpPost("PostPutReqItems")]
|
||||
public async Task<IActionResult> PostPutReqItems(InventoryDto InventoryDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
[HttpPost("PostPutReqApproval")]
|
||||
public async Task<IActionResult> PostPutReqApproval(InventoryDto InventoryDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
[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;
|
||||
}
|
||||
}
|
||||
[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;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
12
CPRNIMS.WebApi/Controllers/Inventory/MRSMgmtController.cs
Normal file
12
CPRNIMS.WebApi/Controllers/Inventory/MRSMgmtController.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
{
|
||||
public class MRSMgmtController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
CPRNIMS.WebApi/Controllers/Inventory/RISMgmtController.cs
Normal file
76
CPRNIMS.WebApi/Controllers/Inventory/RISMgmtController.cs
Normal 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." });
|
||||
}
|
||||
}
|
||||
}
|
||||
117
CPRNIMS.WebApi/Sql/Phase 5/StoredProc.sql
Normal file
117
CPRNIMS.WebApi/Sql/Phase 5/StoredProc.sql
Normal 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
|
||||
61
CPRNIMS.WebApi/Sql/Phase 5/Table.sql
Normal file
61
CPRNIMS.WebApi/Sql/Phase 5/Table.sql
Normal 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')
|
||||
@ -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)
|
||||
|
||||
@ -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,86 +22,71 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
_inventory = inventory;
|
||||
}
|
||||
#region Get
|
||||
public async Task<IActionResult> GetLotQtyByItem(InventoryVM viewModels)
|
||||
public async Task<IActionResult> GetTransactContext(int inventoryId)
|
||||
{
|
||||
try
|
||||
var response = await _inventory.GetTransactContextAsync(inventoryId);
|
||||
return GetResponse(response);
|
||||
}
|
||||
public async Task<IActionResult> GetLotQtyByItem(InventoryVM viewModels)
|
||||
{
|
||||
response = await _inventory.GetLotQtyByItem(GetUser(), viewModels);
|
||||
return GetResponse(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
public async Task<IActionResult> GetInventoryByUserId(InventoryVM viewModels)
|
||||
{
|
||||
response = await _inventory.GetInventoryByUserId(GetUser(), viewModels);
|
||||
return GetResponse(response);
|
||||
}
|
||||
public async Task<IActionResult> GetRequestedItemByUserId(InventoryVM viewModels)
|
||||
{
|
||||
try
|
||||
{
|
||||
response = await _inventory.GetRequestedItemByUserId(GetUser(), viewModels);
|
||||
return GetResponse(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetInventory(string searchPRNo = "", string searchItemNo = "",string searchItemName = "",
|
||||
string searchDept = "",string searchProjectCode="", int pageNumber = 1, int pageSize = 10)
|
||||
{
|
||||
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
|
||||
var request = new InventoryRequest
|
||||
{
|
||||
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);
|
||||
|
||||
@ -113,15 +97,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
|
||||
return Json(new { success = false, Response = postPutItem.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
public async Task<IActionResult> PostPutLotBin(InventoryVM viewModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var postPutItem = await _inventory.PostPutLotBin(GetUser(), viewModel);
|
||||
|
||||
@ -132,16 +108,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
|
||||
return Json(new { success = false, Response = postPutItem.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
public async Task<IActionResult> PostPutReqApproval(InventoryVM viewModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var postPutItem = await _inventory.PostPutReqApproval(GetUser(), viewModel);
|
||||
|
||||
@ -152,15 +119,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
|
||||
return Json(new { success = false, Response = postPutItem.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
public async Task<IActionResult> PostPutReqItems(InventoryVM viewModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var postPutItem = await _inventory.PostPutReqItems(GetUser(), viewModel);
|
||||
|
||||
@ -171,14 +130,12 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
|
||||
return Json(new { success = false, Response = postPutItem.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region Views
|
||||
public IActionResult GetInventoryTabPage(int id)
|
||||
{
|
||||
return ViewComponent("InventoryTabPage", new { inventoryTabPageId = id });
|
||||
}
|
||||
public async Task<IActionResult> Inventory()
|
||||
{
|
||||
return await IsAuthenTicated();
|
||||
|
||||
12
CPRNIMS.WebApps/Controllers/Inventory/MRSController.cs
Normal file
12
CPRNIMS.WebApps/Controllers/Inventory/MRSController.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
{
|
||||
public class MRSController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
101
CPRNIMS.WebApps/Controllers/Inventory/RISMgmtController.cs
Normal file
101
CPRNIMS.WebApps/Controllers/Inventory/RISMgmtController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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 class="inventory-wrapper">
|
||||
@* {{-- HEADER --}} *@
|
||||
<div class="inv-header">
|
||||
<div class="inv-header-inner">
|
||||
<i class="fas fa-file-invoice inv-header-icon"></i>
|
||||
<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>
|
||||
<h1>Inventory Management</h1>
|
||||
<p>Manage inventory transaction, status, and comparison</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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 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;
|
||||
|
||||
<input hidden id="roleRights" value="@ViewBag.UserRoles" />
|
||||
<div id="overlay" class="overlay" style="display: none;">
|
||||
<div id="loader" class="loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/jsfunctions/Inventory/InventoryV2.js"></script>
|
||||
<script src="~/jsfunctions/utilities/utilsV3.js"></script>
|
||||
<script src="~/jsfunctions/inventory/inventvar.js"></script>
|
||||
</body>
|
||||
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")
|
||||
5
CPRNIMS.WebApps/Views/RISMgmt/Index.cshtml
Normal file
5
CPRNIMS.WebApps/Views/RISMgmt/Index.cshtml
Normal file
@ -0,0 +1,5 @@
|
||||
@*
|
||||
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
||||
*@
|
||||
@{
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||
}
|
||||
|
||||
function escAttr(s) {
|
||||
return String(s).replace(/"/g,""").replace(/'/g,"'");
|
||||
}
|
||||
|
||||
/* ── 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
|
||||
? ` · 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 ? ` · ` : ""}
|
||||
${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
@ -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>
|
||||
@ -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" />
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
161
CPRNIMS.WebApps/wwwroot/JsFunctions/Inventory/InventoryTabs.js
Normal file
161
CPRNIMS.WebApps/wwwroot/JsFunctions/Inventory/InventoryTabs.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
.confirmation-modal {
|
||||
z-index: 9999;
|
||||
z-index: 99999 !important;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
z-index: 99999 !important;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user