Full CRUD with RIS, MRS, Inventory including reports

This commit is contained in:
rowell_m_soriano 2026-06-18 16:51:31 +08:00
parent 44862d01b5
commit fa03ef5a3d
82 changed files with 7524 additions and 422 deletions

View File

@ -10,6 +10,9 @@
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="CaptchaGen.NetCore" Version="1.1.2" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="FastReport.OpenSource" Version="2026.2.3" />
<PackageReference Include="FastReport.OpenSource.Export.PdfSimple" Version="2026.2.3" />
<PackageReference Include="FastReport.OpenSource.Web" Version="2026.2.3" />
<PackageReference Include="Google.Apis.Drive.v3" Version="1.67.0.3373" />
<PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.2.0" />

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Contracts.Common
{
public interface ITransactionFacade
{
Task<T> ExecuteAsync<T>(Func<Task<T>> operation,CancellationToken ct);
Task ExecuteAsync(Func<Task> operation,CancellationToken ct);
}
}

View File

@ -0,0 +1,16 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Contracts.Inventory
{
public interface IInventoryReports
{
Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "");
Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "");
Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "");
}
}

View File

@ -11,9 +11,10 @@ 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);
Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter, CancellationToken ct,int? departmentId = null, string? userName = "");
Task<MRS?> GetByIdAsync(long mrsId, CancellationToken ct);
Task<MRS> CreateAsync(CreateMRSRequest dto, string createdBy, CancellationToken ct);
Task ApproveAsync(long mrsId, string approvedBy, CancellationToken ct);
Task CancelAsync(CancelMRSRequest request, string canceledBy, CancellationToken ct);
}
}

View File

@ -13,10 +13,10 @@ namespace CPRNIMS.Domain.Contracts.Inventory
{
public interface IRIS
{
Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct);
Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct, int? departmentId = null, string? userName = "");
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);
Task CancelAsync(CancelRISRequest request,string canceledBy, CancellationToken ct);
}
}

View File

@ -0,0 +1,16 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using FastReport;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Contracts.Reports
{
public interface IReportBuilder
{
Task<Report> RISBuildAsync(DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct);
Task<Report> MRSBuildAsync(DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct);
}
}

View File

@ -0,0 +1,16 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Contracts.Reports
{
public interface IReportDataService
{
List<RISRowDto> GetMain(DateTime dateFrom, DateTime dateTo);
List<DisciplineAggDto> GetDisciplines(DateTime dateFrom, DateTime dateTo);
List<TopRecipientDto> GetRecipients(DateTime dateFrom, DateTime dateTo);
}
}

View File

@ -57,6 +57,7 @@ namespace CPRNIMS.Domain.Services.Account
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim("FullName", user.FullName ?? ""),
new Claim("Company", user.Company ?? ""),
new Claim("DepartmentId", Convert.ToString(user.DepartmentId)),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};

View File

@ -0,0 +1,49 @@
using CPRNIMS.Domain.Contracts.Common;
using CPRNIMS.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
namespace CPRNIMS.Domain.Services.Common
{
public class TransactionFacade : ITransactionFacade
{
private readonly NonInventoryDbContext _db;
public TransactionFacade(NonInventoryDbContext db)
=> _db = db ?? throw new ArgumentNullException(nameof(db));
public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(operation);
var strategy = _db.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
try
{
var result = await operation();
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return result;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
});
}
public async Task ExecuteAsync(Func<Task> operation, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(operation);
await ExecuteAsync<bool>(async () =>
{
await operation();
return true;
}, ct);
}
}
}

View File

@ -0,0 +1,294 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using Microsoft.EntityFrameworkCore;
namespace CPRNIMS.Domain.Services.Inventory
{
public class InventoryReports : IInventoryReports
{
private readonly NonInventoryDbContext _db;
public InventoryReports(NonInventoryDbContext db) => _db = db;
public async Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
int? departmentId = null, string? userName = "")
{
var endDate = dateTo.Date.AddDays(1);
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
var dateToInclusive = dateTo.AddDays(1);
var query = _db.RIS
.Include(r => r.Discipline)
.Include(r => r.Inventory)
.Include(r => r.MaterialReturns)
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < dateToInclusive);
if (departmentId.HasValue && !seeAllDepartments)
{
query = query.Where(m =>
m.Inventory.User != null &&
m.Inventory.User.DepartmentId == departmentId.Value);
}
var rows = await query
.OrderByDescending(r => r.CreatedDate)
.Select(r => new RISReportRow
{
RISNo = r.RISNo,
CreatedDate = r.CreatedDate,
ItemName = r.PRDetail != null ? r.PRDetail.ItemName : "—",
ItemNo = r.Inventory.ItemNo,
DisciplineName = r.Discipline.DisciplineName,
IssuedTo = r.IssuedTo,
QtyIssued = r.QtyIssued,
TotalReturned = r.MaterialReturns
.Where(m => m.Status != 2)
.Sum(m => (int?)m.QtyReturned) ?? 0,
Status = r.Status,
StatusLabel = r.Status == 0 ? "Draft"
: r.Status == 1 ? "Approved"
: "Cancelled"
})
.ToListAsync(ct);
foreach (var row in rows)
row.NetIssued = row.QtyIssued - row.TotalReturned;
var summary = new RISReportSummary
{
TotalRIS = rows.Count,
TotalApproved = rows.Count(r => r.Status == 1),
TotalPending = rows.Count(r => r.Status == 0),
TotalCancelled = rows.Count(r => r.Status == 2),
TotalQtyIssued = rows.Sum(r => r.QtyIssued),
TotalQtyReturned = rows.Sum(r => r.TotalReturned),
TotalNetIssued = rows.Sum(r => r.NetIssued),
ApprovalRatePct = rows.Count > 0
? Math.Round(rows.Count(r => r.Status == 1) * 100m / rows.Count, 1)
: 0
};
var byDiscipline = rows
.GroupBy(r => r.DisciplineName)
.Select(g => new DisciplineCount { DisciplineName = g.Key, Count = g.Count() })
.OrderByDescending(d => d.Count)
.ToList();
var topRecipients = rows
.GroupBy(r => r.IssuedTo)
.Select(g => new TopRecipient
{
IssuedTo = g.Key,
SlipCount = g.Count(),
TotalQty = g.Sum(r => r.QtyIssued)
})
.OrderByDescending(t => t.TotalQty)
.Take(5)
.ToList();
return new RISReportDto
{
ReportNo = $"RPT-RIS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
DateFrom = dateFrom,
DateTo = dateTo,
Summary = summary,
Rows = rows,
ByDiscipline = byDiscipline,
TopRecipients = topRecipients
};
}
public async Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
int? departmentId = null, string? userName = "")
{
var endDate = dateTo.Date.AddDays(1);
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
var query = _db.MRS
.Include(m => m.RIS)
.Include(m => m.Inventory)
.ThenInclude(i => i.User)
.Where(m => m.CreatedDate >= dateFrom &&
m.CreatedDate < endDate);
if (departmentId.HasValue && !seeAllDepartments)
{
query = query.Where(m =>
m.Inventory.User != null &&
m.Inventory.User.DepartmentId == departmentId.Value);
}
var rows = await query
.OrderByDescending(m => m.CreatedDate)
.Select(m => new MRSReportRow
{
MRSNo = m.MRSNo,
CreatedDate = m.CreatedDate,
RISNo = m.RIS.RISNo,
ItemName = m.RIS.PRDetail != null ? m.RIS.PRDetail.ItemName : "—",
ReturnedBy = m.ReturnedBy,
QtyReturned = m.QtyReturned,
Condition = m.Condition ?? "Good",
Status = m.Status,
StatusLabel = m.Status == 0 ? "Draft"
: m.Status == 1 ? "Approved"
: "Cancelled"
})
.ToListAsync(ct);
// Total RIS qty issued in the same period (for the comparison panel)
var totalRISQty = await _db.RIS
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < endDate
&& r.Status != 2)
.SumAsync(r => (int?)r.QtyIssued, ct) ?? 0;
var totalReturned = rows.Where(r => r.Status != 2).Sum(r => r.QtyReturned);
var goodCount = rows.Count(r => r.Condition == "Good");
var summary = new MRSReportSummary
{
TotalMRS = rows.Count,
TotalQtyReturned = totalReturned,
TotalQtyIssuedRIS = totalRISQty,
NetQtyConsumed = totalRISQty - totalReturned,
ReturnRatePct = totalRISQty > 0
? Math.Round(totalReturned * 100m / totalRISQty, 1)
: 0,
GoodConditionPct = rows.Count > 0
? Math.Round(goodCount * 100m / rows.Count, 1)
: 0
};
var byCondition = rows
.Where(r => r.Status != 2)
.GroupBy(r => r.Condition)
.Select(g => new ConditionTotal { Condition = g.Key, TotalQty = g.Sum(r => r.QtyReturned) })
.OrderByDescending(c => c.TotalQty)
.ToList();
return new MRSReportDto
{
ReportNo = $"RPT-MRS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
DateFrom = dateFrom,
DateTo = dateTo,
Summary = summary,
Rows = rows,
ByCondition = byCondition
};
}
public async Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
int? departmentId = null, string? userName = "")
{
var endDate = dateTo.Date.AddDays(1);
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25" , "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
var query = _db.InventTransDetails
.Where(itd =>
itd.IsActive &&
itd.InventTrans.IsActive &&
itd.InventTrans.Inventory.IsActive &&
itd.CreatedDate >= dateFrom &&
itd.CreatedDate < endDate);
if (departmentId.HasValue && !seeAllDepartments)
{
query = query.Where(itd =>
itd.InventTrans.Inventory.User.DepartmentId == departmentId.Value);
}
var rawRows = await query
.Select(itd => new
{
itd.InventTrans.Inventory.InventoryId,
itd.InventTrans.Inventory.ItemNo,
itd.InventTrans.Inventory.QtyIn,
itd.InventTrans.Inventory.QtyOut,
itd.InventTrans.Inventory.QtyOnHand,
LotNo = itd.InventTrans.Inventory.Lot != null
? itd.InventTrans.Inventory.Lot.LotName
: null,
ItemName = itd.InventTrans.Inventory.Item.ItemCode.ItemName ?? "None",
ItemCategoryName =
itd.InventTrans.Inventory.Item.ItemCode.ItemCategory.ItemCategoryName ?? "None",
itd.CreatedDate
})
.ToListAsync(ct);
// De-duplicate: one row per Inventory (latest trans detail wins for the date shown)
var rows = rawRows
.GroupBy(r => r.InventoryId)
.Select(g =>
{
var inv = g.OrderByDescending(x => x.CreatedDate).First();
return new InventoryReportRow
{
ItemName = inv.ItemName,
ItemNo = inv.ItemNo,
ItemCategoryName = inv.ItemCategoryName,
LotNo = inv.LotNo,
QtyIn = inv.QtyIn,
QtyOut = inv.QtyOut,
QtyOnHand = inv.QtyOnHand,
StockPct = inv.QtyIn > 0
? (int)Math.Round(Math.Max(0, Math.Min(100, (inv.QtyOnHand / inv.QtyIn) * 100)))
: 0
};
})
.OrderBy(r => r.ItemName)
.ToList();
var summary = new InventoryReportSummary
{
TotalSKUs = rows.Count,
TotalOnHand = rows.Sum(r => r.QtyOnHand),
TotalQtyIn = rows.Sum(r => r.QtyIn),
TotalQtyOut = rows.Sum(r => r.QtyOut),
LowStockCount = rows.Count(r => r.StockPct < 20 && r.QtyOnHand > 0),
OutOfStockCount = rows.Count(r => r.QtyOnHand <= 0)
};
var byCategory = rows
.GroupBy(r => r.ItemCategoryName)
.Select(g => new CategoryStockLevel
{
CategoryName = g.Key,
AvgStockPct = (int)Math.Round(g.Average(r => r.StockPct))
})
.OrderByDescending(c => c.AvgStockPct)
.ToList();
var alerts = rows
.Where(r => r.StockPct < 20)
.OrderBy(r => r.StockPct)
.Take(10)
.Select(r => new InventoryAlert
{
ItemName = r.ItemName,
QtyOnHand = r.QtyOnHand,
Severity = r.QtyOnHand <= 0 ? "Critical" : "Low"
})
.ToList();
return new InventoryReportDto
{
ReportNo = $"RPT-INV-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
AsOf = dateTo.Date, // reflect the requested period end, not always today
Summary = summary,
Rows = rows,
ByCategory = byCategory,
Alerts = alerts
};
}
}
}

View File

@ -1,24 +1,23 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Domain.Contracts.Common;
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)
private readonly ITransactionFacade _transactionFacade;
public MRS(NonInventoryDbContext db, ITransactionFacade transactionFacade)
{
_db = db;
_transactionFacade = transactionFacade;
}
public async Task ApproveAsync(long mrsId, string approvedBy, CancellationToken ct)
{
var rms = await _db.MRS.FindAsync(mrsId)
?? throw new InvalidOperationException("MRS not found.");
@ -30,14 +29,38 @@ namespace CPRNIMS.Domain.Services.Inventory
rms.ApprovedBy = approvedBy;
rms.ApprovedDate = DateTime.Now;
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(ct);
}
public async Task<Infrastructure.Entities.Inventory.MRS> CreateAsync(CreateMRSRequest dto, string createdBy)
public async Task CancelAsync(CancelMRSRequest request,string canceledBy, CancellationToken ct)
{
await _transactionFacade.ExecuteAsync(async () =>
{
var mrs = await _db.MRS
.Include(m => m.Inventory)
.FirstOrDefaultAsync(m => m.MRSId == request.MRSId)
?? throw new InvalidOperationException("MRS not found.");
if (mrs.Status == 2)
throw new InvalidOperationException("MRS is already cancelled.");
// Reverse the return: deduct qty back out
mrs.Inventory.QtyOut = Math.Max(0m, mrs.Inventory.QtyOut) + mrs.QtyReturned;
mrs.Inventory.QtyOnHand = mrs.Inventory.QtyIn - mrs.Inventory.QtyOut;
mrs.Reason = request.Reason;
mrs.Status = 2;
mrs.CanceledDate = DateTime.Now;
mrs.CanceledBy = canceledBy;
}, ct);
}
public async Task<Infrastructure.Entities.Inventory.MRS> CreateAsync(CreateMRSRequest dto, string createdBy, CancellationToken ct)
{
return await _transactionFacade.ExecuteAsync(async () =>
{
var ris = await _db.RIS
.Include(r => r.Inventory)
.FirstOrDefaultAsync(r => r.RISId == dto.RISId)
.FirstOrDefaultAsync(r => r.RISId == dto.RISId, ct)
?? throw new InvalidOperationException("Referenced RIS not found.");
if (dto.QtyReturned > ris.QtyIssued)
@ -67,7 +90,7 @@ namespace CPRNIMS.Domain.Services.Inventory
inventory.QtyOnHand = inventory.QtyIn - inventory.QtyOut;
var trans = await _db.InventTrans
.FirstOrDefaultAsync(t => t.InventoryId == ris.InventoryId && t.IsActive == true)!;
.FirstOrDefaultAsync(t => t.InventoryId == ris.InventoryId && t.IsActive == true, ct)!;
_db.InventTransDetails.Add(new InventTransDetail
{
@ -79,24 +102,35 @@ namespace CPRNIMS.Domain.Services.Inventory
IsActive = true
});
await _db.SaveChangesAsync();
return mrs;
}, ct);
}
public async Task<Infrastructure.Entities.Inventory.MRS?> GetByIdAsync(long mrsId)
public async Task<Infrastructure.Entities.Inventory.MRS?> GetByIdAsync(long mrsId, CancellationToken ct)
=> await _db.MRS
.Include(r => r.Inventory)
.Include(r => r.RIS)
.FirstOrDefaultAsync(r => r.RISId == mrsId);
.FirstOrDefaultAsync(r => r.RISId == mrsId, ct);
public async Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter)
public async Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter, CancellationToken ct,
int? departmentId = null, string? userName = "")
{
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
var q = _db.MRS
.Include(m => m.RIS)
.Include(m => m.Inventory)
.AsQueryable();
if (departmentId.HasValue && !seeAllDepartments)
{
q = q.Where(itd =>
itd.Inventory.User.DepartmentId == departmentId.Value);
}
if (!string.IsNullOrWhiteSpace(filter.SearchMRSNo))
q = q.Where(m => m.MRSNo.Contains(filter.SearchMRSNo));
@ -112,13 +146,13 @@ namespace CPRNIMS.Domain.Services.Inventory
if (filter.DateTo.HasValue)
q = q.Where(m => m.CreatedDate <= filter.DateTo.Value.AddDays(1));
var total = await q.CountAsync();
var total = await q.CountAsync(ct);
var data = await q
.OrderByDescending(m => m.CreatedDate)
.Skip((filter.Page - 1) * filter.PageSize)
.Skip((filter.PageNumber - 1) * filter.PageSize)
.Take(filter.PageSize)
.Select(m => new MRSResponse
.Select(m => new MRSPagedDto
{
MRSId = m.MRSId,
MRSNo = m.MRSNo,
@ -139,7 +173,7 @@ namespace CPRNIMS.Domain.Services.Inventory
ApprovedBy = m.ApprovedBy,
ApprovedDate = m.ApprovedDate
})
.ToListAsync();
.ToListAsync(ct);
return new MRSPagedResult { Data = data, RecordsTotal = total };
}

View File

@ -1,5 +1,5 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Domain.UIServices.Inventory;
using CPRNIMS.Domain.Contracts.Common;
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
@ -17,17 +17,15 @@ namespace CPRNIMS.Domain.Services.Inventory
public class RIS : IRIS
{
private readonly NonInventoryDbContext _db;
public RIS(NonInventoryDbContext db) => _db = db;
private readonly ITransactionFacade _transactionFacade;
public RIS(NonInventoryDbContext db, ITransactionFacade transactionFacade)
{
_db = db;
_transactionFacade = transactionFacade;
}
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
return await _transactionFacade.ExecuteAsync(async () =>
{
var inventory = await _db.Inventories
.FirstOrDefaultAsync(i => i.InventoryId == dto.InventoryId, ct)
@ -53,7 +51,6 @@ namespace CPRNIMS.Domain.Services.Inventory
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)
@ -75,17 +72,8 @@ namespace CPRNIMS.Domain.Services.Inventory
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;
}
});
}, ct);
}
public async Task ApproveAsync(ApproveRISRequest request, string approvedBy, CancellationToken ct)
@ -103,14 +91,9 @@ namespace CPRNIMS.Domain.Services.Inventory
await _db.SaveChangesAsync(ct);
}
public async Task CancelAsync(CancelRISRequest request, CancellationToken ct)
public async Task CancelAsync(CancelRISRequest request,string canceledBy, CancellationToken ct)
{
var strategy = _db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
try
await _transactionFacade.ExecuteAsync(async () =>
{
var ris = await _db.RIS
.Include(r => r.Inventory)
@ -120,6 +103,23 @@ namespace CPRNIMS.Domain.Services.Inventory
if (ris.Status == 2)
throw new InvalidOperationException("RIS is already cancelled.");
//Check if already approved the related RIS No to MRS must cannot be cancel
var mrs = await _db.MRS.FirstOrDefaultAsync(m => m.RISId == request.RISId, ct);
if (mrs != null)
{
if (mrs.Status == 1)
{
throw new InvalidOperationException(
$"MRS #{mrs.MRSNo} has already been approved in relation to RIS #{ris.RISNo}.");
}
mrs.Status = 2;
mrs.CanceledDate = DateTime.Now;
mrs.CanceledBy = canceledBy;
mrs.Reason = "Canceled in RIS already";
}
ris.Inventory.QtyOut = Math.Max(0m, ris.Inventory.QtyOut - ris.QtyIssued);
ris.Inventory.QtyOnHand = ris.Inventory.QtyIn - ris.Inventory.QtyOut;
ris.Reason = request.Reason;
@ -141,31 +141,7 @@ namespace CPRNIMS.Domain.Services.Inventory
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;
}
});
}, ct);
}
private async Task<string> GenerateRISNoAsync(CancellationToken ct)
@ -177,19 +153,30 @@ namespace CPRNIMS.Domain.Services.Inventory
return $"RIS-{year}{month}-{count:D4}"; // e.g. RIS-202606-0001
}
public async Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct)
public async Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct,
int? departmentId = null, string? userName = "")
{
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
var q = _db.RIS
.Include(r => r.Discipline)
.Include(r => r.Inventory)
.Include(r => r.MaterialReturns)
.AsQueryable();
if (departmentId.HasValue && !seeAllDepartments)
{
q = q.Where(itd =>
itd.Inventory.User.DepartmentId == departmentId.Value);
}
// 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);
//else
// q = q.Where(r => r.Status == 0);
// RIS No
if (!string.IsNullOrWhiteSpace(filter.SearchRISNo))

View File

@ -0,0 +1,80 @@
using CPRNIMS.Domain.Contracts.Reports;
using CPRNIMS.Domain.UIContracts.Common;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Helper;
using FastReport;
using Microsoft.Extensions.Configuration;
using System.Data;
using System.Net.Http.Json;
using System.Text.Json;
namespace CPRNIMS.Infrastructure.Reports
{
public class ReportBuilder : IReportBuilder
{
private readonly IConfiguration _configuration;
private readonly TokenHelper _tokenHelper;
private readonly IApiConfigurationService _apiConfigurationService;
public ReportBuilder(IConfiguration configuration, TokenHelper tokenHelper,
IApiConfigurationService apiConfigurationService)
{
_configuration = configuration;
_tokenHelper = tokenHelper;
_apiConfigurationService = apiConfigurationService;
}
private static readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public Task<Report> MRSBuildAsync(DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct)
{
throw new NotImplementedException();
}
public async Task<Report> RISBuildAsync(
DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct = default)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:RISReportData"]
?? throw new InvalidOperationException("RISReportData endpoint is not configured.");
// Append the date range as query string
var url = $"{endpoint}?dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}";
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync(url, ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
throw new InvalidOperationException(
$"Failed to fetch RIS report data. HTTP {(int)response.StatusCode}: {json}");
var dto = JsonSerializer.Deserialize<RISReportDataDto>(json, _jsonOptions)
?? new RISReportDataDto();
var report = new Report();
report.Load(templatePath);
// RegisterData accepts IEnumerable<object> — names must match the .frx datasources.
report.RegisterData(dto.Rows ?? new(), "TRIS");
report.RegisterData(dto.Disciplines ?? new(), "TDisciplineAgg");
report.RegisterData(dto.Recipients ?? new(), "TTopRecipients");
/* report.GetDataSource("Table").Enabled = true;
report.GetDataSource("Table1").Enabled = true;
report.GetDataSource("Table3").Enabled = true;*/
report.GetDataSource("TRIS").Enabled = true;
report.GetDataSource("TDisciplineAgg").Enabled = true;
report.GetDataSource("TTopRecipients").Enabled = true;
report.SetParameterValue("DateFrom", dateFrom);
report.SetParameterValue("DateTo", dateTo);
return report;
}
}
}

View File

@ -0,0 +1,109 @@
using CPRNIMS.Domain.Contracts.Reports;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
namespace CPRNIMS.Infrastructure.Reports
{
public class ReportDataService : IReportDataService
{
private readonly NonInventoryDbContext _context;
public ReportDataService(NonInventoryDbContext context) => _context = context;
private const string MainSql = """
SELECT
R.RISNo,
PR.PRNo,
R.QtyIssued,
R.IssuedTo,
CASE R.Status
WHEN 0 THEN 'Draft'
WHEN 1 THEN 'Approved'
WHEN 2 THEN 'Cancelled'
ELSE 'Unknown'
END AS StatusLabel,
CreatedBy.CreatedBy,
ApprovedBy.ApprovedBy,
R.ApprovedDate,
R.CreatedDate,
D.DisciplineName,
PRD.ItemName,
PRD.ItemNo,
IV.QtyIn,
IV.QtyOut,
IV.QtyOnHand,
DEP.Department AS DepartmentName,
ISNULL(MRS_AGG.TotalReturned, 0) AS TotalReturned,
ISNULL(MRS_AGG.MRSCount, 0) AS MRSCount,
R.QtyIssued - ISNULL(MRS_AGG.TotalReturned, 0) AS NetIssued
FROM dbo.RIS R
INNER JOIN dbo.Disciplines D ON R.DisciplineId = D.DisciplineId
INNER JOIN dbo.Inventory IV ON R.InventoryId = IV.InventoryId AND IV.IsActive = 1
LEFT JOIN dbo.Lot L ON IV.LotId = L.LotId
OUTER APPLY (SELECT U.FullName ApprovedBy FROM dbo.Users U WHERE R.ApprovedBy = U.UserName) ApprovedBy
OUTER APPLY (SELECT U2.FullName CreatedBy FROM dbo.Users U2 WHERE R.CreatedBy = U2.UserName) CreatedBy
INNER JOIN Users U ON IV.UserId = U.Id
LEFT JOIN dbo.Departments DEP ON U.DepartmentId = DEP.DepartmentId
LEFT JOIN dbo.PRDetails PRD ON R.PRDetailId = PRD.PRDetailsId AND PRD.IsActive = 1
LEFT JOIN dbo.PR PR ON PRD.PRId = PR.PRId AND PR.IsActive = 1
LEFT JOIN (
SELECT
RISId,
COUNT(*) AS MRSCount,
SUM(QtyReturned) AS TotalReturned
FROM dbo.MRS
WHERE Status != 2
GROUP BY RISId
) MRS_AGG ON R.RISId = MRS_AGG.RISId
WHERE R.Status != 2
AND R.CreatedDate >= @DateFrom
AND R.CreatedDate < DATEADD(DAY, 1, @DateTo)
ORDER BY R.CreatedDate DESC, R.RISNo ASC;
""";
private const string DisciplineSql = """
SELECT
D.DisciplineName,
COUNT(*) AS SlipCount
FROM dbo.RIS R
INNER JOIN dbo.Disciplines D ON R.DisciplineId = D.DisciplineId
WHERE R.Status != 2
AND R.CreatedDate >= @DateFrom
AND R.CreatedDate < DATEADD(DAY, 1, @DateTo)
GROUP BY D.DisciplineName
ORDER BY SlipCount DESC;
""";
private const string RecipientsSql = """
SELECT TOP 5
R.IssuedTo AS Name,
COUNT(*) AS SlipCount,
SUM(IV.QtyOut) AS QtyOut
FROM dbo.RIS R
INNER JOIN dbo.Inventory IV ON R.InventoryId = IV.InventoryId AND IV.IsActive = 1
WHERE R.Status != 2
AND R.CreatedDate >= @DateFrom
AND R.CreatedDate < DATEADD(DAY, 1, @DateTo)
GROUP BY R.IssuedTo
ORDER BY COUNT(*) DESC;
""";
public List<RISRowDto> GetMain(DateTime dateFrom, DateTime dateTo) =>
Query<RISRowDto>(MainSql, dateFrom, dateTo);
public List<DisciplineAggDto> GetDisciplines(DateTime dateFrom, DateTime dateTo) =>
Query<DisciplineAggDto>(DisciplineSql, dateFrom, dateTo);
public List<TopRecipientDto> GetRecipients(DateTime dateFrom, DateTime dateTo) =>
Query<TopRecipientDto>(RecipientsSql, dateFrom, dateTo);
private List<T> Query<T>(string sql, DateTime from, DateTime to)
{
using var conn = new SqlConnection(_context.Database.GetConnectionString());
return conn.Query<T>(sql, new { DateFrom = from, DateTo = to }).ToList();
}
}
}

View File

@ -0,0 +1,18 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.UIContracts.Inventory
{
public interface IInventoryReports
{
Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
}
}

View File

@ -0,0 +1,20 @@
using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.UIContracts.Inventory
{
public interface IMRS
{
Task<MRSPagedResponse> GetMRSPaged(MRSPagedRequest request, CancellationToken ct);
Task<ApiResponse<object>> CreateMRS(CreateMRSRequest request, CancellationToken ct);
Task<ApiResponse<object>> ApproveMRS(ApproveMRSRequest request, CancellationToken ct);
Task<ApiResponse<object>> CancelMRS(CancelMRSRequest request, CancellationToken ct);
}
}

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.ViewModel.Inventory;
using System;
@ -11,10 +12,9 @@ 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<ApiResponse<object>> ApproveRIS(ApproveRISRequest request, CancellationToken ct);
Task<ApiResponse<object>> CancelRIS(CancelRISRequest request, CancellationToken ct);
Task<ApiResponse<object>> CreateRIS(CreateRISRequest request, CancellationToken ct);
Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct);
}
}

View File

@ -0,0 +1,96 @@
using CPRNIMS.Domain.UIContracts.Common;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response;
using CPRNIMS.Infrastructure.Helper;
using Microsoft.Extensions.Configuration;
using System.Text;
using System.Text.Json;
namespace CPRNIMS.Domain.UIServices.Inventory
{
public class InventoryReports : IInventoryReports
{
private readonly IConfiguration _configuration;
private readonly TokenHelper _tokenHelper;
private readonly IApiConfigurationService _apiConfigurationService;
public InventoryReports(IConfiguration configuration, TokenHelper tokenHelper, IApiConfigurationService apiConfigurationService)
{
_configuration = configuration;
_tokenHelper = tokenHelper;
_apiConfigurationService = apiConfigurationService;
}
private static readonly JsonSerializerOptions _jsonOpts = new()
{
PropertyNameCaseInsensitive = true
};
public async Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetInventoryReport"]
?? throw new InvalidOperationException("GetInventoryReport endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await http.GetAsync(qs.ToString(), ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
return new InventoryReportDto();
var result = JsonSerializer.Deserialize<InventoryReportDto>(json, _jsonOpts);
return result ?? new InventoryReportDto();
}
public async Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetMRSReport"]
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await http.GetAsync(qs.ToString(), ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
return new MRSReportDto { };
var result = JsonSerializer.Deserialize<MRSReportDto>(json, _jsonOpts);
return result ?? new MRSReportDto();
}
public async Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRISReport"]
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await http.GetAsync(qs.ToString(), ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
return new RISReportDto {};
var result = JsonSerializer.Deserialize<RISReportDto>(json, _jsonOpts);
return result ?? new RISReportDto();
}
}
}

View File

@ -0,0 +1,145 @@
using CPRNIMS.Domain.Services;
using CPRNIMS.Domain.UIContracts.Common;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Dto.Inventory;
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.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.UIServices.Inventory
{
public class MRS : IMRS
{
private readonly IConfiguration _configuration;
private readonly TokenHelper _tokenHelper;
private readonly IApiConfigurationService _apiConfigurationService;
public MRS(IConfiguration configuration, TokenHelper tokenHelper,IApiConfigurationService apiConfigurationService)
{
_configuration = configuration;
_tokenHelper = tokenHelper;
_apiConfigurationService = apiConfigurationService;
}
private static readonly JsonSerializerOptions _mrsJsonOpts = new()
{
PropertyNameCaseInsensitive = true
};
public async Task<MRSPagedResponse> GetMRSPaged(MRSPagedRequest request, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetMRS"]
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"pageNumber={request.PageNumber}&pageSize={request.PageSize}");
if (!string.IsNullOrWhiteSpace(request.SearchMRSNo))
qs.Append($"&searchMRSNo={Uri.EscapeDataString(request.SearchMRSNo)}");
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.SearchReturnedBy))
qs.Append($"&searchReturnedBy={Uri.EscapeDataString(request.SearchReturnedBy)}");
if (request.Status.HasValue)
qs.Append($"&status={request.Status.Value}");
if (!string.IsNullOrWhiteSpace(request.Condition))
qs.Append($"&condition={Uri.EscapeDataString(request.Condition)}");
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await http.GetAsync(qs.ToString(), ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
return new MRSPagedResponse { Data = [], RecordsTotal = 0 };
var result = JsonSerializer.Deserialize<MRSPagedResponse>(json, _mrsJsonOpts);
return result ?? new MRSPagedResponse();
}
public async Task<ApiResponse<object>> CreateMRS(CreateMRSRequest 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:CreateMRS"]
?? throw new InvalidOperationException("CreateMRS endpoint is not configured.");
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
var response = await http.PostAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(ct);
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _mrsJsonOpts)
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
if (!response.IsSuccessStatusCode)
result.success = false;
return result;
}
public async Task<ApiResponse<object>> ApproveMRS(ApproveMRSRequest 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:ApproveMRS"]
?? throw new InvalidOperationException("ApproveMRS endpoint is not configured.");
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent(
JsonSerializer.Serialize(request),
Encoding.UTF8, "application/json");
var response = await http.PostAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(ct);
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _mrsJsonOpts)
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
if (!response.IsSuccessStatusCode)
result.success = false;
return result;
}
public async Task<ApiResponse<object>> CancelMRS(CancelMRSRequest 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:CancelMRS"]
?? throw new InvalidOperationException("CancelMRS endpoint is not configured.");
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent(
JsonSerializer.Serialize(request),
Encoding.UTF8, "application/json");
var response = await http.PostAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(ct);
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _mrsJsonOpts)
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
if (!response.IsSuccessStatusCode)
result.success = false;
return result;
}
}
}

View File

@ -3,6 +3,7 @@ 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.Dto.PR.Response;
using CPRNIMS.Infrastructure.Helper;
using Microsoft.Extensions.Configuration;
using System;
@ -29,8 +30,6 @@ namespace CPRNIMS.Domain.UIServices.Inventory
}
#region Get
public async Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct)
{
try
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
@ -74,16 +73,9 @@ namespace CPRNIMS.Domain.UIServices.Inventory
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)
public async Task<ApiResponse<object>> ApproveRIS(ApproveRISRequest request, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
@ -97,15 +89,16 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var response = await httpClient.PutAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(ct);
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _jsonOptions)
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
if (!response.IsSuccessStatusCode)
{
return false;
}
return true;
result.success = false;
return result;
}
public async Task<bool> CancelRIS(CancelRISRequest request, CancellationToken ct)
public async Task<ApiResponse<object>> CancelRIS(CancelRISRequest request, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
@ -115,19 +108,21 @@ namespace CPRNIMS.Domain.UIServices.Inventory
?? 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");
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);
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _jsonOptions)
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
if (!response.IsSuccessStatusCode)
{
return false;
result.success = false;
return result;
}
return true;
}
public async Task<RISResponse> CreateRIS(CreateRISRequest request, CancellationToken ct)
public async Task<ApiResponse<object>> CreateRIS(CreateRISRequest request, CancellationToken ct)
{
var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
@ -138,27 +133,19 @@ namespace CPRNIMS.Domain.UIServices.Inventory
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");
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();
}
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _jsonOptions)
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
public Task<RISPagedResponse> GetRISById(int risId, CancellationToken ct)
{
throw new NotImplementedException();
if (!response.IsSuccessStatusCode)
result.success = false;
return result;
}
private static readonly JsonSerializerOptions _jsonOptions = new()

View File

@ -198,7 +198,7 @@ namespace CPRNIMS.Infrastructure.Database
b.HasOne(u => u.Attachment)
.WithOne(a => a.ApplicationUser)
.HasForeignKey<Attachment>(a => a.AttachmentId)
.IsRequired(false); // Allow null if there is no attachment
.IsRequired(false);
});
modelBuilder.Entity<Attachment>(b =>
{
@ -206,7 +206,7 @@ namespace CPRNIMS.Infrastructure.Database
b.HasOne(a => a.AttachmentExtention)
.WithOne()
.HasForeignKey<Attachment>(a => a.ExtensionId)
.IsRequired(false); // Allow null if there is no extension
.IsRequired(false);
});
modelBuilder.Entity<ApplicationUser>(b =>
{
@ -270,13 +270,23 @@ namespace CPRNIMS.Infrastructure.Database
.HasForeignKey(i => i.LotId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<InventTrans>(e =>
{
e.HasMany(r => r.InventTransDetails)
.WithOne()
.HasForeignKey(t => t.InventTransId)
modelBuilder.Entity<Inventory>()
.HasOne(i => i.Item)
.WithMany()
.HasForeignKey(i => i.ItemNo)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Item>()
.HasOne(i => i.ItemCode)
.WithMany()
.HasForeignKey(i => i.ItemCodeId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ItemCode>()
.HasOne(i => i.ItemCategory)
.WithMany()
.HasForeignKey(i => i.ItemCategoryId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<InventTransDetail>(e =>
{

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
@ -20,6 +21,7 @@ namespace CPRNIMS.Infrastructure.Dto.Account
public string URLAttachment { get; set; } = string.Empty;
public string? token { get; set; }
public string? company { get; set; }
public int? departmentId { get; set; }
public string? refreshToken { get; set; }
public DateTime expiresAt { get; set; }
public int expiresInSeconds { get; set; }

View File

@ -13,5 +13,6 @@ namespace CPRNIMS.Infrastructure.Dto.Account
public string FullName { get; init; } = default!;
public string Company { get; init; } = default!;
public IReadOnlyList<string> Roles { get; init; } = [];
public int? DepartmentId { get; set; }
}
}

View File

@ -8,7 +8,7 @@ namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class MRSFilterDto
{
public int Page { get; set; } = 1;
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 12;
public string? SearchMRSNo { get; set; }
public long? RISId { get; set; }

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class MRSPagedDto
{
public long MRSId { get; set; }
public string MRSNo { get; set; } = string.Empty;
public long RISId { get; set; }
public string? RISNo { get; set; }
public string? ItemName { get; set; }
public string? ReturnedBy { get; set; }
public decimal QtyReturned { get; set; }
public string? Condition { 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 int InventoryId { get; set; }
public List<MRSPagedDto> Data { get; set; } = [];
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class MRSPagedRequest
{
public string? SearchMRSNo { get; set; }
public string? SearchRISNo { get; set; }
public string? SearchItemName { get; set; }
public string? SearchReturnedBy { get; set; }
public short? Status { get; set; }
public string? Condition { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 12;
}
}

View File

@ -9,7 +9,7 @@ namespace CPRNIMS.Infrastructure.Dto.Inventory
{
public class MRSPagedResult
{
public IEnumerable<MRSResponse> Data { get; set; } = [];
public IEnumerable<MRSPagedDto> Data { get; set; } = [];
public int RecordsTotal { get; set; }
public List<string> DepartmentList { get; set; } = new List<string>();
public List<string> DisciplineList { get; set; } = new List<string>();

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports
{
public class RISReportDataDto
{
public List<RISRowDto> Rows { get; set; } = new();
public List<DisciplineAggDto> Disciplines { get; set; } = new();
public List<TopRecipientDto> Recipients { get; set; } = new();
}
public class RISRowDto
{
public string? RISNo { get; set; }
public long? PRNo { get; set; }
public decimal QtyIssued { get; set; }
public string? IssuedTo { get; set; }
public string? StatusLabel { get; set; }
public string? CreatedBy { get; set; }
public string? ApprovedBy { get; set; }
public DateTime? ApprovedDate { get; set; }
public DateTime? CreatedDate { get; set; }
public string? DisciplineName { get; set; }
public string? ItemName { get; set; }
public long? ItemNo { get; set; }
public decimal QtyIn { get; set; }
public decimal QtyOut { get; set; }
public decimal QtyOnHand { get; set; }
public string? DepartmentName { get; set; }
public decimal TotalReturned { get; set; }
public int MRSCount { get; set; }
public decimal NetIssued { get; set; }
}
public class DisciplineAggDto
{
public string? DisciplineName { get; set; }
public int SlipCount { get; set; }
}
public class TopRecipientDto
{
public string? Name { get; set; }
public int SlipCount { get; set; }
public decimal QtyOut { get; set; }
}
}

View File

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports
{
public class RISReportDto
{
public string CompanyName { get; set; } = "Lloyd Laboratories Incorporated";
public string PreparedBy { get; set; } = "Finance Department";
public string ReportNo { get; set; } = string.Empty;
public DateTime DateFrom { get; set; }
public DateTime DateTo { get; set; }
public RISReportSummary Summary { get; set; } = new();
public List<RISReportRow> Rows { get; set; } = [];
public List<DisciplineCount> ByDiscipline { get; set; } = [];
public List<TopRecipient> TopRecipients { get; set; } = [];
}
public class RISReportSummary
{
public int TotalRIS { get; set; }
public int TotalApproved { get; set; }
public int TotalPending { get; set; }
public int TotalCancelled { get; set; }
public decimal TotalQtyIssued { get; set; }
public decimal TotalQtyReturned { get; set; }
public decimal TotalNetIssued { get; set; }
public decimal ApprovalRatePct { get; set; }
}
public class RISReportRow
{
public string RISNo { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string ItemName { get; set; } = string.Empty;
public long ItemNo { get; set; }
public string DisciplineName { get; set; } = string.Empty;
public string IssuedTo { get; set; } = string.Empty;
public decimal QtyIssued { get; set; }
public decimal TotalReturned { get; set; }
public decimal NetIssued { get; set; }
public short Status { get; set; }
public string StatusLabel { get; set; } = string.Empty;
}
public class DisciplineCount
{
public string DisciplineName { get; set; } = string.Empty;
public int Count { get; set; }
}
public class TopRecipient
{
public string IssuedTo { get; set; } = string.Empty;
public int SlipCount { get; set; }
public decimal TotalQty { get; set; }
}
public class MRSReportDto
{
public string CompanyName { get; set; } = "Lloyd Laboratories Incorporated";
public string PreparedBy { get; set; } = "Finance Department";
public string ReportNo { get; set; } = string.Empty;
public DateTime DateFrom { get; set; }
public DateTime DateTo { get; set; }
public MRSReportSummary Summary { get; set; } = new();
public List<MRSReportRow> Rows { get; set; } = [];
public List<ConditionTotal> ByCondition { get; set; } = [];
}
public class MRSReportSummary
{
public int TotalMRS { get; set; }
public decimal TotalQtyReturned { get; set; }
public decimal TotalQtyIssuedRIS { get; set; }
public decimal NetQtyConsumed { get; set; }
public decimal ReturnRatePct { get; set; }
public decimal GoodConditionPct { get; set; }
}
public class MRSReportRow
{
public string MRSNo { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string RISNo { get; set; } = string.Empty;
public string ItemName { get; set; } = string.Empty;
public string ReturnedBy { get; set; } = string.Empty;
public decimal QtyReturned { get; set; }
public string Condition { get; set; } = string.Empty;
public short Status { get; set; }
public string StatusLabel { get; set; } = string.Empty;
}
public class ConditionTotal
{
public string Condition { get; set; } = string.Empty;
public decimal TotalQty { get; set; }
}
public class InventoryReportDto
{
public string CompanyName { get; set; } = "Lloyd Laboratories Incorporated";
public string PreparedBy { get; set; } = "Finance Department";
public string ReportNo { get; set; } = string.Empty;
public DateTime AsOf { get; set; }
public InventoryReportSummary Summary { get; set; } = new();
public List<InventoryReportRow> Rows { get; set; } = [];
public List<CategoryStockLevel> ByCategory { get; set; } = [];
public List<InventoryAlert> Alerts { get; set; } = [];
}
public class InventoryReportSummary
{
public int TotalSKUs { get; set; }
public decimal TotalOnHand { get; set; }
public decimal TotalQtyIn { get; set; }
public decimal TotalQtyOut { get; set; }
public int LowStockCount { get; set; }
public int OutOfStockCount { get; set; }
}
public class InventoryReportRow
{
public string ItemName { get; set; } = string.Empty;
public long ItemNo { get; set; }
public string ItemCategoryName { get; set; } = string.Empty;
public string? LotNo { get; set; }
public decimal QtyIn { get; set; }
public decimal QtyOut { get; set; }
public decimal QtyOnHand { get; set; }
public int StockPct { get; set; }
}
public class CategoryStockLevel
{
public string CategoryName { get; set; } = string.Empty;
public int AvgStockPct { get; set; }
}
public class InventoryAlert
{
public string ItemName { get; set; } = string.Empty;
public decimal QtyOnHand { get; set; }
public string Severity { get; set; } = string.Empty; // "Critical" | "Low"
}
}

View File

@ -0,0 +1,14 @@
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.Reports.Response
{
public class InventoryData
{
public List<InventoryReportDto> Data { get; set; } = [];
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response
{
public class MRSData
{
public List<MRSReportDto> Data { get; set; } = [];
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response
{
public class RISData
{
public List<RISReportDto> Data { get; set; } = [];
}
}

View File

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

View File

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

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
{
public class MRSPagedResponse
{
public List<MRSListItem> Data { get; set; } = [];
public int RecordsTotal { get; set; }
}
public class MRSListItem
{
public long MRSId { get; set; }
public string MRSNo { get; set; } = string.Empty;
public long RISId { get; set; }
public string? RISNo { get; set; }
public string? ItemName { get; set; }
public string? ReturnedBy { get; set; }
public decimal QtyReturned { get; set; }
public string? Condition { 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 int InventoryId { get; set; }
}
}

View File

@ -1,28 +1,17 @@
using System;
using CPRNIMS.Infrastructure.Models.Inventory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
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; }
[JsonPropertyName("success")] public bool Success { get; set; }
[JsonPropertyName("message")] public string? Message { get; set; }
[JsonPropertyName("data")] public MRSData? Data { get; set; }
}
}

View File

@ -17,6 +17,8 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public long RRNo { get; set; }
public string CreatedBy { get; set; }=string.Empty;
public bool IsActive { get; set; }
public Inventory? Inventory { get; set; }
// public InventTransDetail? InventTransDetail { get; set; }
public ICollection<InventTransDetail> InventTransDetails { get; set; } = [];
}
}

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Entities.Purchasing;
using CPRNIMS.Infrastructure.Entities.Items;
using CPRNIMS.Infrastructure.Entities.Purchasing;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -24,5 +25,7 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public bool IsActive { get; set; }
public long PRDetailId { get; set; }
public PRDetails? PRDetails { get; set; }
public ItemCategory? ItemCategory { get; set; }
public InventTrans? InventTrans { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Entities.Account;
using CPRNIMS.Infrastructure.Entities.Items;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -24,5 +25,6 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public Lot? Lot { get; set; }
public ICollection<InventTrans> InventTrans { get; set; } = [];
public ApplicationUser? User { get; set; }
public Item? Item { get; set; }
}
}

View File

@ -42,5 +42,4 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public RIS RIS { get; set; } = null!;
public Inventory Inventory { get; set; } = null!;
}
}

View File

@ -1,13 +1,7 @@
using CPRNIMS.Infrastructure.Entities.Account;
using CPRNIMS.Infrastructure.Entities.Common;
using System;
using System.Collections.Generic;
using CPRNIMS.Infrastructure.Entities.Common;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CPRNIMS.Infrastructure.Entities.Items
{
@ -42,5 +36,6 @@ namespace CPRNIMS.Infrastructure.Entities.Items
public string? ItemAttachPath { get; set; }
public bool IsMDLD { get; set; }
public bool CheckBox { get; set; }
public ItemCode? ItemCode { get; set; }
}
}

View File

@ -23,5 +23,7 @@ namespace CPRNIMS.Infrastructure.Entities.Items
public byte RequestTypeId { get; set; }
public bool IsActive { get; set; }
public int CartItemCount { get; set; }
public short ItemCategoryId { get; set; }
public ItemCategory? ItemCategory { get; set; }
}
}

View File

@ -6,7 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CPRNIMS.Infrastructure.Entities.Purchasing
{

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Entities.Common;
using CPRNIMS.Infrastructure.Entities.Items;
using CPRNIMS.Infrastructure.ViewModel.Items;
using System;
using System.Collections.Generic;
@ -24,5 +25,6 @@ namespace CPRNIMS.Infrastructure.Entities.Purchasing
public decimal Qty { get; set; }
public bool IsSearched { get; set; }
public PR? PRs { get; set; }
public ItemCategory? ItemCategory { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Models.Inventory
{
public class MRSData
{
[JsonPropertyName("mrsId")] public long MRSId { get; set; }
[JsonPropertyName("mrsNo")] public string? MRSNo { get; set; }
[JsonPropertyName("risId")] public long RISId { get; set; }
[JsonPropertyName("inventoryId")] public int InventoryId { get; set; }
[JsonPropertyName("returnedBy")] public string? ReturnedBy { get; set; }
[JsonPropertyName("qtyReturned")] public decimal QtyReturned { get; set; }
[JsonPropertyName("condition")] public string? Condition { get; set; }
[JsonPropertyName("remarks")] public string? Remarks { get; set; }
[JsonPropertyName("status")] public short Status { get; set; }
[JsonPropertyName("createdBy")] public string? CreatedBy { get; set; }
[JsonPropertyName("createdDate")] public DateTime CreatedDate { get; set; }
[JsonPropertyName("approvedBy")] public string? ApprovedBy { get; set; }
[JsonPropertyName("approvedDate")] public DateTime? ApprovedDate { get; set; }
}
}

View File

@ -29,6 +29,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FastReport.OpenSource" Version="2026.2.3" />
<PackageReference Include="FastReport.OpenSource.Export.PdfSimple" Version="2026.2.3" />
<PackageReference Include="FastReport.OpenSource.Web" Version="2026.2.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

View File

@ -2,8 +2,8 @@
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
<NameOfLastUsedPublishProfile>D:\sourcecode\NonInventPurchasing\CPRNIMS.WebApi\Properties\PublishProfiles\FolderProfile1.pubxml</NameOfLastUsedPublishProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -1,15 +1,18 @@
using CPRNIMS.Domain.Contracts.Account;
using CPRNIMS.Domain.Contracts.Canvass;
using CPRNIMS.Domain.Contracts.Common;
using CPRNIMS.Domain.Contracts.Finance;
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Domain.Contracts.Items;
using CPRNIMS.Domain.Contracts.PO;
using CPRNIMS.Domain.Contracts.PR;
using CPRNIMS.Domain.Contracts.Receiving;
using CPRNIMS.Domain.Contracts.Reports;
using CPRNIMS.Domain.Contracts.SMTP;
using CPRNIMS.Domain.Services;
using CPRNIMS.Domain.Services.Account;
using CPRNIMS.Domain.Services.Canvass;
using CPRNIMS.Domain.Services.Common;
using CPRNIMS.Domain.Services.Finance;
using CPRNIMS.Domain.Services.Inventory;
using CPRNIMS.Domain.Services.PO;
@ -18,6 +21,7 @@ using CPRNIMS.Domain.Services.SMTP;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Entities.Account;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.Reports;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
@ -89,7 +93,6 @@ namespace CPRNIMS.WebApi.Common
sql.EnableRetryOnFailure(5, TimeSpan.FromHours(2), null);
sql.CommandTimeout(20);
}));
services.AddDbContext<PurchLocalDbContext>(options =>
options.UseSqlServer(localConn, sql =>
{
@ -147,6 +150,7 @@ namespace CPRNIMS.WebApi.Common
{
services.AddMemoryCache();
services.AddScoped<IRoleAuthorizationCache, RoleAuthorizationCache>();
services.AddScoped<ITransactionFacade, TransactionFacade>();
services.AddScoped<IDepartment, Department>();
services.AddScoped<IAttachment, Domain.Services.Account.Attachment>();
services.AddScoped<IItem, Domain.Services.Items.Item>();
@ -154,7 +158,8 @@ namespace CPRNIMS.WebApi.Common
services.AddScoped<ICanvass, Canvass>();
services.AddScoped<IRIS, RIS>();
services.AddScoped<IMRS, MRS>();
services.AddScoped<IReportDataService, ReportDataService>();
services.AddScoped<IInventoryReports, InventoryReports>();
#region Automation using LLM
services.AddHttpClient<SupplierSearchService>();
#endregion

View File

@ -67,6 +67,7 @@ namespace CPRNIMS.WebApi.Controllers.Account
userId = user.Id,
userName = user.UserName,
fullName = user.FullName,
departmentId = user.DepartmentId,
email = user.Email,
phoneNumber = user.PhoneNumber,
company = user.Company,

View File

@ -2,7 +2,6 @@
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;

View File

@ -0,0 +1,66 @@
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Domain.Contracts.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.WebApi.Security;
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApi.Controllers.Inventory
{
[Route("api/[controller]")]
[ApiController]
public class InventoryReportsController : ControllerBase
{
private readonly IReportDataService _data;
private readonly IInventoryReports _reports;
public InventoryReportsController(IReportDataService data, IInventoryReports reports)
{
_data = data;
_reports = reports;
}
[HttpGet("GetRISReport")]
public async Task<IActionResult> GetRISReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
var data = await _reports.GetRISReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName);
return Ok(data);
}
[HttpGet("GetMRSReport")]
public async Task<IActionResult> GetMRSReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
var data = await _reports.GetMRSReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName);
return Ok(data);
}
[HttpGet("GetInventoryReport")]
public async Task<IActionResult> GetInventoryReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
var data = await _reports.GetInventoryReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName);
return Ok(data);
}
[HttpGet("GetRISData")]
public IActionResult GetData([FromQuery] DateTime dateFrom, [FromQuery] DateTime dateTo,CancellationToken ct)
{
var result = new RISReportDataDto
{
Rows = _data.GetMain(dateFrom, dateTo),
Disciplines = _data.GetDisciplines(dateFrom, dateTo),
Recipients = _data.GetRecipients(dateFrom, dateTo)
};
return Ok(result);
}
}
}

View File

@ -1,12 +1,63 @@
using Microsoft.AspNetCore.Mvc;
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.WebApi.Security;
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApi.Controllers.Inventory
{
public class MRSMgmtController : Controller
[Route("api/[controller]")]
[ApiController]
public partial class MRSMgmtController : ControllerBase
{
public IActionResult Index()
private readonly IMRS _mrs;
public MRSMgmtController(IMRS mrs) => _mrs = mrs;
[HttpGet]
public async Task<IActionResult> GetMRS([FromQuery] MRSFilterDto filter, CancellationToken ct)
{
return View();
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
var result = await _mrs.GetPagedAsync(filter, ct, currentUser.DepartmentId, currentUser.UserName);
return Ok(result);
}
[HttpPost()]
public async Task<IActionResult> CreateMRS([FromBody] CreateMRSRequest request, CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
var result = await _mrs.CreateAsync(request, currentUser.UserName, ct);
return Ok(new { success = true, message = $"MRS# {result.MRSNo} created successfully." });
}
[HttpPost("Approve")]
public async Task<IActionResult> ApproveMRS([FromBody] ApproveMRSRequest request, CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
await _mrs.ApproveAsync(request.MRSId, currentUser.UserName, ct);
return Ok(new { success = true, message = "MRS approved successfully." });
}
[HttpPost("Cancel")]
public async Task<IActionResult> CancelMRS([FromBody] CancelMRSRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.Reason))
return BadRequest(new { success = false, message = "A reason for cancellation is required." });
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
await _mrs.CancelAsync(request, currentUser.UserName, ct);
return Ok(new {success = true,message = "MRS cancelled and inventory adjusted."});
}
}
}

View File

@ -4,7 +4,6 @@ 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
{
@ -20,7 +19,11 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
[HttpGet("GetRIS")]
public async Task<IActionResult> GetRIS([FromQuery] RISFilterDto filter, CancellationToken ct)
{
var result = await _risRepo.GetPagedAsync(filter,ct);
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
var result = await _risRepo.GetPagedAsync(filter,ct, currentUser.DepartmentId, currentUser.UserName);
return Ok(result);
}
@ -48,12 +51,7 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
});
var ris = await _risRepo.CreateAsync(request, currentUser.UserName,ct);
return Ok(new
{
success = true,
message = $"RIS {ris.RISNo} created successfully.",
data= ris
});
return Ok(new { success = true, message = $"RIS# {ris.RISNo} created successfully." });
}
[HttpPut("ApproveRIS")]
@ -69,7 +67,11 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
[HttpPut("CancelRIS")]
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request, CancellationToken ct)
{
await _risRepo.CancelAsync(request, ct);
var currentUser = User.ToUserClaims();
if (currentUser == null)
return BadRequest();
await _risRepo.CancelAsync(request, currentUser.UserName, ct);
return Ok(new { success = true, message = "RIS cancelled and inventory restored." });
}
}

View File

@ -5,6 +5,7 @@ using CPRNIMS.WebApi.Common;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices(builder);
builder.Services.AddFastReport();
builder.Services.AddAutoMapper(cfg => { }, typeof(SupplierRequestProfile));
@ -30,6 +31,8 @@ app.UseCors("AllowAnyOrigin");
app.UseHttpsRedirection();
app.UseFastReport();
app.UseAuthentication();
app.UseAuthorization();

View File

@ -16,6 +16,7 @@ namespace CPRNIMS.WebApi.Security
UserName = user.FindFirstValue(ClaimTypes.Name) ?? "",
FullName = user.FindFirstValue("FullName") ?? "",
Company = user.FindFirstValue("Company") ?? "",
DepartmentId = Convert.ToInt32(user.FindFirstValue("DepartmentId")),
Roles = user.FindAll(ClaimTypes.Role)
.Select(r => r.Value)
.ToList()

View File

@ -1,13 +1,13 @@
USE [CPRNIMS]
GO
/****** Object: StoredProcedure [dbo].[GetInventory] Script Date: 6/11/2026 9:29:29 AM ******/
/****** Object: StoredProcedure [dbo].[GetInventory] Script Date: 6/18/2026 3:09:49 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[GetInventory]
(
@UserId VARCHAR(450)='89da2977-c70f-4df9-94d4-9a610aa999ea',
@UserId VARCHAR(450)='16b37c00-131f-4205-b8f6-ad4d0f9f3a32',
@SearchPRNo VARCHAR(50) = '',
@SearchItemNo VARCHAR(50) = '',
@SearchItemName VARCHAR(100) = '',
@ -20,6 +20,18 @@ AS
BEGIN
SET NOCOUNT ON;
DECLARE @Offset INT = (@PageNumber - 1) * @PageSize;
DECLARE @HasFullAccess BIT = 0;
DECLARE @UserDepartmentId INT,@UserName VARCHAR(50);
SELECT @UserDepartmentId = DepartmentId,@UserName=UserName
FROM dbo.Users
WHERE Id = @UserId;
IF @UserName IN ('LSKRISUR24','LSCYNDIZ25','LSJONTAN25','LHRIOCAS24')
SET @HasFullAccess = 1;
IF @UserDepartmentId = 6
SET @HasFullAccess = 1;
-- ── 1. Full department list — unaffected by any filter ──────────────
SELECT DISTINCT D.Department
@ -49,38 +61,26 @@ BEGIN
, ICAT.ItemCategoryName
, RRD.RemainingQty
, ITD.CreatedDate
,COALESCE(PCD.ProjectCode,'N/A') ProjectCode
,COALESCE(PC.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)
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.PR ON PR.PRId=PRD.PRId AND PR.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
LEFT JOIN dbo.Lot L ON IV.LotId = L.LotId
LEFT JOIN dbo.LotType LT ON L.LotTypeId = LT.LotTypeId
LEFT 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
WHERE ITD.TransTypeId = 2 AND IV.IsActive = 1 AND ITD.IsActive = 1
AND (
@HasFullAccess = 1
OR D.DepartmentId = @UserDepartmentId
)
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 + '%')
@ -100,7 +100,7 @@ BEGIN
, ICAT.ItemCategoryName
, RRD.RemainingQty
, ITD.CreatedDate
,PCD.ProjectCode;
,PC.ProjectCode;
-- ── 3. Total count of filtered results ──────────────────────────────
SELECT COUNT(*) AS TotalCount

View File

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8"?>
<Report ScriptLanguage="CSharp" ReportInfo.Created="06/16/2026 15:54:16" ReportInfo.Modified="06/17/2026 12:14:22" ReportInfo.CreatorVersion="2024.2.0.0">
<Dictionary>
<TableDataSource Name="TRIS" Alias="TRIS" DataType="System.Int32" Enabled="true">
<Column Name="RISNo" DataType="System.String"/>
<Column Name="PRNo" DataType="System.Int64"/>
<Column Name="QtyIssued" DataType="System.Decimal"/>
<Column Name="IssuedTo" DataType="System.String"/>
<Column Name="StatusLabel" Alias="Status" DataType="System.String"/>
<Column Name="CreatedBy" DataType="System.String"/>
<Column Name="ApprovedBy" DataType="System.String"/>
<Column Name="ApprovedDate" DataType="System.DateTime"/>
<Column Name="CreatedDate" DataType="System.DateTime"/>
<Column Name="DisciplineName" Alias="Discipline" DataType="System.String"/>
<Column Name="ItemName" DataType="System.String"/>
<Column Name="ItemNo" DataType="System.Int64"/>
<Column Name="QtyIn" DataType="System.Decimal"/>
<Column Name="QtyOut" DataType="System.Decimal"/>
<Column Name="QtyOnHand" DataType="System.Decimal"/>
<Column Name="DepartmentName" Alias="Department" DataType="System.String"/>
<Column Name="TotalReturned" DataType="System.Decimal"/>
<Column Name="MRSCount" DataType="System.Int32"/>
<Column Name="NetIssued" DataType="System.Decimal"/>
</TableDataSource>
<TableDataSource Name="TDisciplineAgg" Alias="TDisciplineAgg" DataType="System.Int32" Enabled="true">
<Column Name="DisciplineName" DataType="System.String"/>
<Column Name="SlipCount" DataType="System.Int32"/>
</TableDataSource>
<TableDataSource Name="TTopRecipients" Alias="TTopRecipients" DataType="System.Int32" Enabled="true">
<Column Name="Name" DataType="System.String"/>
<Column Name="SlipCount" DataType="System.Int32"/>
<Column Name="QtyOut" DataType="System.Decimal"/>
</TableDataSource>
<Parameter Name="PreparedBy" DataType="System.String" AsString=""/>
<Parameter Name="PrintDate" DataType="System.String" AsString=""/>
<Parameter Name="ReportNo" DataType="System.String" AsString=""/>
<Parameter Name="TotalRISIssued" DataType="System.String" AsString=""/>
<Parameter Name="ApprovedRIS" DataType="System.String" AsString=""/>
<Parameter Name="DateFrom" DataType="System.DateTime"/>
<Parameter Name="DateTo" DataType="System.DateTime"/>
</Dictionary>
<ReportPage Name="Page1" PaperWidth="216" PaperHeight="279" RawPaperSize="1" Watermark.Font="Arial, 60pt">
<ReportTitleBand Name="ReportTitle1" Width="740.88" Height="56.7">
<TextObject Name="Text1" Left="179.55" Top="18.9" Width="311.85" Height="28.35" Text="Return Issuance Slip Report" HorzAlign="Center" Font="Segoe UI, 18pt"/>
<TextObject Name="Text3" Width="283.5" Height="18.9" Text="LLOYD LABORATORIES INCORPORATED" Font="Arial, 10pt"/>
<TextObject Name="Text7" Left="-9450" Top="-9450" Width="94.5" Height="18.9" Text="PreparedBy" Font="Arial, 10pt"/>
<TextObject Name="Text11" Left="-9450" Top="-9450" Width="94.5" Height="18.9" Text="Table_RIS_Query" Font="Arial, 10pt"/>
</ReportTitleBand>
<PageHeaderBand Name="PageHeader1" Top="59.9" Width="740.88" Height="42.05">
<TextObject Name="Text2" Width="113.4" Height="18.9" Text="PREPARED BY" Font="Arial, 10pt"/>
<TextObject Name="Text4" Left="151.2" Width="94.5" Height="18.9" Text="PRINT DATE" Font="Arial, 10pt"/>
<TextObject Name="Text5" Left="302.4" Width="94.5" Height="18.9" Text="REPORT NO." Font="Arial, 10pt"/>
<TextObject Name="Text6" Top="18.9" Width="94.5" Height="18.9" Text="[PreparedBy]" Font="Arial, 10pt"/>
<TextObject Name="Text9" Left="302.4" Top="18.9" Width="94.5" Height="18.9" Text="[ReportNo]" Font="Arial, 10pt"/>
<TextObject Name="Text10" Left="154.5" Top="23.15" Width="94.5" Height="18.9" Text="[PrintDate]" Font="Arial, 10pt"/>
<TextObject Name="Text8" Left="538.65" Width="94.5" Height="18.9" Text="QtyIssued" Font="Arial, 10pt"/>
<TextObject Name="Text15" Left="670.95" Width="94.5" Height="18.9" Text="NetIssued" Font="Arial, 10pt"/>
</PageHeaderBand>
<DataBand Name="Data1" Top="105.15" Width="740.88" Height="529.2" DataSource="TRIS">
<TableObject Name="Table_RIS" Width="803.2" Height="311.85">
<TableColumn Name="Column6" Width="42.85"/>
<TableColumn Name="Column7" Width="42.85"/>
<TableColumn Name="Column8" Width="146.77"/>
<TableColumn Name="Column11" Width="59.84"/>
<TableColumn Name="Column9" Width="90.07"/>
<TableColumn Name="Column10" Width="90.07"/>
<TableColumn Name="Column20"/>
<TableColumn Name="Column21"/>
<TableColumn Name="Column22"/>
<TableColumn Name="Column23"/>
<TableColumn Name="Column24"/>
<TableRow Name="Row6" Height="20.79">
<TableCell Name="Cell26" Text="[TRIS.RISNo]" Font="Arial, 10pt"/>
<TableCell Name="Cell27" Text="[TRIS.PRNo]" Font="Arial, 10pt"/>
<TableCell Name="Cell28" Text="[TRIS.ItemName]" Font="Arial, 10pt"/>
<TableCell Name="Cell51" Text="[TRIS.ItemNo]" Font="Arial, 10pt"/>
<TableCell Name="Cell29" Text="[TRIS.Discipline]" Font="Arial, 10pt"/>
<TableCell Name="Cell30" Text="[TRIS.IssuedTo]" Font="Arial, 10pt">
<TextObject Name="Text12" Left="75.6" Width="66.15" Height="18.9" Text="[TRIS.QtyIssued]" Format="Currency" Format.UseLocale="true" Format.DecimalDigits="2" HorzAlign="Right" WordWrap="false" Font="Arial, 10pt" Trimming="EllipsisCharacter"/>
</TableCell>
<TableCell Name="Cell112" Font="Arial, 10pt"/>
<TableCell Name="Cell117" Text="[TRIS.TotalReturned]" Font="Arial, 10pt"/>
<TableCell Name="Cell122" Text="[TRIS.NetIssued]" Font="Arial, 10pt"/>
<TableCell Name="Cell127" Text="[TRIS.Status]" Font="Arial, 10pt"/>
<TableCell Name="Cell132" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row7" Height="20.79">
<TableCell Name="Cell31" Font="Arial, 10pt"/>
<TableCell Name="Cell32" Font="Arial, 10pt"/>
<TableCell Name="Cell33" Font="Arial, 10pt"/>
<TableCell Name="Cell52" Font="Arial, 10pt"/>
<TableCell Name="Cell34" Font="Arial, 10pt"/>
<TableCell Name="Cell35" Font="Arial, 10pt"/>
<TableCell Name="Cell113" Font="Arial, 10pt"/>
<TableCell Name="Cell118" Font="Arial, 10pt"/>
<TableCell Name="Cell123" Font="Arial, 10pt"/>
<TableCell Name="Cell128" Font="Arial, 10pt"/>
<TableCell Name="Cell133" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row8" Height="20.79">
<TableCell Name="Cell36" Font="Arial, 10pt"/>
<TableCell Name="Cell37" Font="Arial, 10pt"/>
<TableCell Name="Cell38" Font="Arial, 10pt"/>
<TableCell Name="Cell53" Font="Arial, 10pt"/>
<TableCell Name="Cell39" Font="Arial, 10pt"/>
<TableCell Name="Cell40" Font="Arial, 10pt"/>
<TableCell Name="Cell114" Font="Arial, 10pt"/>
<TableCell Name="Cell119" Font="Arial, 10pt"/>
<TableCell Name="Cell124" Font="Arial, 10pt"/>
<TableCell Name="Cell129" Font="Arial, 10pt"/>
<TableCell Name="Cell134" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row9" Height="20.79">
<TableCell Name="Cell41" Font="Arial, 10pt"/>
<TableCell Name="Cell42" Font="Arial, 10pt"/>
<TableCell Name="Cell43" Font="Arial, 10pt"/>
<TableCell Name="Cell54" Font="Arial, 10pt"/>
<TableCell Name="Cell44" Font="Arial, 10pt"/>
<TableCell Name="Cell45" Font="Arial, 10pt"/>
<TableCell Name="Cell115" Font="Arial, 10pt"/>
<TableCell Name="Cell120" Font="Arial, 10pt"/>
<TableCell Name="Cell125" Font="Arial, 10pt"/>
<TableCell Name="Cell130" Font="Arial, 10pt"/>
<TableCell Name="Cell135" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row10" Height="228.69">
<TableCell Name="Cell46" Font="Arial, 10pt"/>
<TableCell Name="Cell47" Font="Arial, 10pt"/>
<TableCell Name="Cell48" Font="Arial, 10pt"/>
<TableCell Name="Cell55" Font="Arial, 10pt"/>
<TableCell Name="Cell49" Font="Arial, 10pt"/>
<TableCell Name="Cell50" Font="Arial, 10pt"/>
<TableCell Name="Cell116" Font="Arial, 10pt"/>
<TableCell Name="Cell121" Font="Arial, 10pt"/>
<TableCell Name="Cell126" Font="Arial, 10pt"/>
<TableCell Name="Cell131" Font="Arial, 10pt"/>
<TableCell Name="Cell136" Font="Arial, 10pt"/>
</TableRow>
</TableObject>
<TextObject Name="Text18" Left="378" Top="340.2" Width="141.75" Height="18.9" Text="TOP RECIPIENTS" Font="Arial, 10pt"/>
<TableObject Name="Table5" Left="378" Top="359.1" Width="321.3" Height="113.37" PrintOnParent="true">
<TableColumn Name="Column17" Width="189"/>
<TableColumn Name="Column18"/>
<TableColumn Name="Column19"/>
<TableRow Name="Row18" Height="25.98">
<TableCell Name="Cell91" Text="[TTopRecipients.Name]" Font="Arial, 10pt"/>
<TableCell Name="Cell92" Text="[TTopRecipients.SlipCount]" Font="Arial, 10pt"/>
<TableCell Name="Cell108" Text="[TTopRecipients.QtyOut]" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row19" Height="25.98">
<TableCell Name="Cell96" Font="Arial, 10pt"/>
<TableCell Name="Cell97" Font="Arial, 10pt"/>
<TableCell Name="Cell109" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row20" Height="25.98">
<TableCell Name="Cell101" Font="Arial, 10pt"/>
<TableCell Name="Cell102" Font="Arial, 10pt"/>
<TableCell Name="Cell110" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row21" Height="35.43">
<TableCell Name="Cell106" Font="Arial, 10pt"/>
<TableCell Name="Cell107" Font="Arial, 10pt"/>
<TableCell Name="Cell111" Font="Arial, 10pt"/>
</TableRow>
</TableObject>
<TextObject Name="Text17" Top="340.2" Width="189" Height="18.9" Text="ISSUANCE BY DISCIPLINE" Font="Arial, 10pt"/>
<TableObject Name="Table4" Top="359.1" Width="311.42" Height="113.4" PrintOnParent="true">
<TableColumn Name="Column12" Width="200.79"/>
<TableColumn Name="Column13" Width="87.39"/>
<TableColumn Name="Column15" Width="22.24"/>
<TableColumn Name="Column16" Width="1"/>
<TableRow Name="Row11">
<TableCell Name="Cell56" Text="[TDisciplineAgg.DisciplineName]" Font="Arial, 10pt"/>
<TableCell Name="Cell57" Text="[TDisciplineAgg.SlipCount]" Font="Arial, 10pt"/>
<TableCell Name="Cell59" Font="Arial, 10pt"/>
<TableCell Name="Cell60" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row16">
<TableCell Name="Cell81" Font="Arial, 10pt"/>
<TableCell Name="Cell82" Font="Arial, 10pt"/>
<TableCell Name="Cell84" Font="Arial, 10pt"/>
<TableCell Name="Cell85" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row12">
<TableCell Name="Cell61" Font="Arial, 10pt"/>
<TableCell Name="Cell62" Font="Arial, 10pt"/>
<TableCell Name="Cell64" Font="Arial, 10pt"/>
<TableCell Name="Cell65" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row13">
<TableCell Name="Cell66" Font="Arial, 10pt"/>
<TableCell Name="Cell67" Font="Arial, 10pt"/>
<TableCell Name="Cell69" Font="Arial, 10pt"/>
<TableCell Name="Cell70" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row14">
<TableCell Name="Cell71" Font="Arial, 10pt"/>
<TableCell Name="Cell72" Font="Arial, 10pt"/>
<TableCell Name="Cell74" Font="Arial, 10pt"/>
<TableCell Name="Cell75" Font="Arial, 10pt"/>
</TableRow>
<TableRow Name="Row15">
<TableCell Name="Cell76" Font="Arial, 10pt"/>
<TableCell Name="Cell77" Font="Arial, 10pt"/>
<TableCell Name="Cell79" Font="Arial, 10pt"/>
<TableCell Name="Cell80" Font="Arial, 10pt"/>
</TableRow>
</TableObject>
</DataBand>
<ReportSummaryBand Name="ReportSummary1" Top="637.55" Width="740.88" Height="37.8"/>
<PageFooterBand Name="PageFooter1" Top="678.55" Width="740.88" Height="604.8"/>
</ReportPage>
</Report>

View File

@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup>
<StaticWebAssetsEnabled>true</StaticWebAssetsEnabled>
</PropertyGroup>
<ItemGroup>
<Content Remove="Views\Components\Dashboard\DeptApprover.cshtml" />
</ItemGroup>
@ -58,8 +60,8 @@
<ItemGroup>
<Folder Include="Common\Helper\" />
<Folder Include="Properties\NewFolder\" />
<Folder Include="wwwroot\Content\Uploads\PRAttachment\" />
<Folder Include="wwwroot\_content\FastReport.Web\js\" />
</ItemGroup>
<ItemGroup>
@ -68,6 +70,9 @@
<ItemGroup>
<PackageReference Include="CaptchaGen.NetCore" Version="1.1.2" />
<PackageReference Include="FastReport.OpenSource" Version="2026.2.3" />
<PackageReference Include="FastReport.OpenSource.Export.PdfSimple" Version="2026.2.3" />
<PackageReference Include="FastReport.OpenSource.Web" Version="2026.2.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0" />
</ItemGroup>

View File

@ -1,4 +1,5 @@
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.Contracts.Reports;
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.UIContracts.Canvass;
using CPRNIMS.Domain.UIContracts.CaptCha;
using CPRNIMS.Domain.UIContracts.Common;
@ -22,6 +23,7 @@ using CPRNIMS.Domain.UIServices.Receiving;
using CPRNIMS.Domain.UIServices.SMTP;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.Reports;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http.Features;
@ -88,6 +90,9 @@ namespace CPRNIMS.WebApps.Common
builder.Services.AddTransient<ICaptchaService, CaptchaService>();
builder.Services.AddScoped<ErrorLogHelper>();
builder.Services.AddScoped<IRIS, RIS>();
builder.Services.AddScoped<IMRS, MRS>();
builder.Services.AddScoped<IReportBuilder, ReportBuilder>();
builder.Services.AddScoped<IInventoryReports, InventoryReports>();
}
private static void AddSessionAndAuthentication(WebApplicationBuilder builder)
@ -170,7 +175,6 @@ namespace CPRNIMS.WebApps.Common
};
});
}
private static void AddDbContext(WebApplicationBuilder builder)
{
builder.Services.AddDbContext<NonInventoryDbContext>(options =>

View File

@ -164,6 +164,7 @@ namespace CPRNIMS.WebApps.Controllers
new Claim(ClaimTypes.NameIdentifier, login.userId),
new Claim(ClaimTypes.Name, login.userName),
new Claim("FullName", login.fullName),
new Claim("DepartmentId", Convert.ToString(login.departmentId)),
new Claim("Company", login.company),
new Claim("Token", login.token),
new Claim("TokenExpiry", expirationTime.ToString("O"))

View File

@ -1,5 +1,4 @@
using Azure.Core;
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper;

View File

@ -0,0 +1,40 @@
using CPRNIMS.Domain.UIContracts.Inventory;
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApps.Controllers.Inventory
{
public class InventoryReportsController : Controller
{
private readonly IInventoryReports _reports;
public InventoryReportsController(IInventoryReports reports)
{
_reports=reports;
}
[HttpGet]
public async Task<IActionResult> GetInventoryReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var response = await _reports.GetInventoryReportAsync(dateFrom, dateTo, ct);
return GetResponse(response);
}
[HttpGet]
public async Task<IActionResult> GetRISReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var response = await _reports.GetRISReportAsync(dateFrom, dateTo, ct);
return GetResponse(response);
}
[HttpGet]
public async Task<IActionResult> GetMRSReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
{
var response = await _reports.GetMRSReportAsync(dateFrom, dateTo, ct);
return GetResponse(response);
}
protected IActionResult GetResponse<T>(T response)
{
return Json(new
{
success = response != null,
data = response ?? Activator.CreateInstance<T>()
});
}
}
}

View File

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

View File

@ -0,0 +1,77 @@
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Infrastructure.Dto.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 partial class MRSMgmtController : BaseMethod
{
private readonly IMRS _mrs;
public MRSMgmtController(ErrorLogHelper errorMessageService,IWebHostEnvironment webHostEnvironment,TokenHelper tokenHelper,
IMRS mrs, IAccount account) : base(errorMessageService, webHostEnvironment, tokenHelper, account) => _mrs = mrs;
[HttpGet]
public async Task<IActionResult> GetMRS([FromQuery] string? searchMRSNo,string? searchRISNo,string? searchItemName,string?
searchReturnedBy,string? status,string? condition,int pageNumber = 1,int pageSize = 12,CancellationToken ct = default)
{
short? statusCode = status switch
{
"0" => 0,
"1" => 1,
"2" => 2,
_ => null
};
var result = await _mrs.GetMRSPaged(new MRSPagedRequest
{
SearchMRSNo = searchMRSNo,
SearchRISNo = searchRISNo,
SearchItemName = searchItemName,
SearchReturnedBy = searchReturnedBy,
Status = statusCode,
Condition = condition,
PageNumber = pageNumber,
PageSize = pageSize
}, ct);
return Json(new { data = result.Data, recordsTotal = result.RecordsTotal});
}
[HttpPost]
public async Task<IActionResult> CreateMRS([FromBody] CreateMRSRequest request,CancellationToken ct)
{
var result = await _mrs.CreateMRS(request, ct);
if (!result.success)
return BadRequest(new { success = false, message = result.message });
return Ok(new { success = true, message = result.message });
}
[HttpPost]
public async Task<IActionResult> ApproveMRS([FromBody] ApproveMRSRequest request,CancellationToken ct)
{
var result = await _mrs.ApproveMRS(request, ct);
if (!result.success)
return BadRequest(new { success = false, message = result.message });
return Ok(new { success = true, message = result.message });
}
[HttpPost]
public async Task<IActionResult> CancelMRS([FromBody] CancelMRSRequest request,CancellationToken ct)
{
var result = await _mrs.CancelMRS(request, ct);
if (!result.success)
return BadRequest(new { success = false, message = result.message });
return Ok(new { success = true, message = result.message });
}
}
}

View File

@ -1,63 +1,85 @@
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.Contracts.Reports;
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;
using FastReport;
using FastReport.Web;
using System.Threading.Tasks;
namespace CPRNIMS.WebApps.Controllers.Inventory
{
public class RISMgmtController : BaseMethod
{
private readonly IRIS _ris;
private readonly IReportBuilder _builder;
private readonly IWebHostEnvironment _env;
public RISMgmtController(ErrorLogHelper errorMessageService,
IWebHostEnvironment webHostEnvironment, TokenHelper tokenHelper
, IRIS ris, IAccount account)
, IRIS ris, IAccount account, IReportBuilder builder)
: base(errorMessageService, webHostEnvironment, tokenHelper, account)
{
_ris = ris;
_builder = builder;
_env = webHostEnvironment;
}
public IActionResult RISReport(DateTime? dateFrom, DateTime? dateTo, CancellationToken ct)
{
return View();
}
public async Task<IActionResult> RISReportPdf(DateTime? dateFrom, DateTime? dateTo, CancellationToken ct)
{
var from = dateFrom ?? DateTime.Today.AddDays(-15);
var to = dateTo ?? DateTime.Today;
var templatePath = Path.Combine(_env.WebRootPath, "Reports", "RIS_v2.frx");
var report = await _builder.RISBuildAsync(from, to, templatePath, ct);
report.Prepare();
using var pdf = new FastReport.Export.PdfSimple.PDFSimpleExport();
using var ms = new MemoryStream();
pdf.Export(report, ms);
return File(ms.ToArray(), "application/pdf",
$"RIS-Report-{from:yyyyMMdd}-{to:yyyyMMdd}.pdf");
}
[HttpPost]
public async Task<IActionResult> CreateRIS([FromBody] CreateRISRequest request,CancellationToken ct)
{
var result = await _ris.CreateRIS(request,ct);
if (!result.success)
return BadRequest(new { success = false, message = result.message });
return Json(new { success = true, message= $"RIS {result.RISNo} created successfully.", data = result });
return Ok(new { success = true, message = result.message });
}
[HttpPost]
public async Task<IActionResult> ApproveRIS([FromBody] ApproveRISRequest request,CancellationToken ct)
{
bool isSuccess = await _ris.ApproveRIS(request, ct);
var result = await _ris.ApproveRIS(request, ct);
if (!isSuccess)
return BadRequest(new { success = false, message = "RIS cancelled failed" });
if (!result.success)
return BadRequest(new { success = false, message = result.message });
return Ok(new
{
success = true,
message = $"RIS approved successfully."
});
return Ok(new { success = true, message = result.message });
}
[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);
return BadRequest(new{success = false,message = "A reason for cancellation is required."});
if (!isSuccess)
return BadRequest(new { success = false, message = "RIS cancelled failed" });
var result = await _ris.CancelRIS(request, ct);
return Ok(new
{
success = true,
message = "RIS cancelled and inventory restored."
});
if (!result.success)
return BadRequest(new { success = false, message = result.message });
return Ok(new { success = true, message = result.message });
}
[HttpGet]
@ -92,10 +114,5 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
disciplineList = result.DisciplineList
});
}
public async Task<IActionResult> GetRISById(int risId, CancellationToken ct)
{
var response = await _ris.GetRISById(risId,ct);
return GetResponse(response);
}
}
}

View File

@ -1,32 +1,35 @@
using CPRNIMS.Domain.UIServices.Updater;
using CPRNIMS.Domain.UIServices.Updater;
using CPRNIMS.WebApps.Common;
using Microsoft.AspNetCore.StaticFiles;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStaticWebAssets();
builder.Services.AddApplicationServices(builder);
builder.Services.AddFastReport();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
//app.UseRewriter(options);
//var provider = new FileExtensionContentTypeProvider();
//provider.Mappings[".js"] = "text/javascript";
//provider.Mappings[".css"] = "text/css";
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.MapHub<CartHub>("/cartHub");
app.UseAuthentication();
app.UseAuthorization();
app.UseFastReport();
app.MapHub<CartHub>("/cartHub");
app.MapControllerRoute(
name: "default",
//pattern: "{controller=ItemMgmt}/{action=Index}/{id?}");
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();

View File

@ -9,9 +9,11 @@ namespace CPRNIMS.WebApps.ViewComponents.Inventory
string viewName = InventoryTabPageId switch
{
1 => "~/Views/Components/Inventory/TabPage/Inventory.cshtml",
2 => "~/Views/Components/Inventory/TabPage/RISForApproval.cshtml",
2 => "~/Views/Components/Inventory/TabPage/RIS.cshtml",
3 => "~/Views/Components/Inventory/TabPage/MRS.cshtml",
_ => "~/Views/Components/Inventory/TabPage/InventoryTransaction.cshtml"
4 => "~/Views/Components/Inventory/TabPage/Reports/InventorySummaryReport.cshtml",
5 => "~/Views/Components/Inventory/TabPage/Reports/RISReport.cshtml",
_ => "~/Views/Components/Inventory/TabPage/Reports/MRSReport.cshtml"
};
return View(viewName);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,3 @@
@* ── 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">
@ -196,7 +191,6 @@
</div>
</div>
</div>
<style>
/* ── RIS card grid ── */
.ris-grid {
@ -418,6 +412,58 @@
font-weight: 600;
color: var(--teal-dark, #0d5c63);
}
/* ── Report trigger button ── */
.ris-report-trigger {
padding: 7px 14px;
border-radius: var(--radius-sm, 8px);
border: 1.5px solid var(--teal-mid, #0e7c86);
background: var(--teal-pale, #e6f7f8);
color: var(--teal-dark, #0d5c63);
font-family: 'DM Sans', sans-serif;
font-size: .82rem;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all .2s;
margin-left: 8px;
}
.ris-report-trigger:hover {
background: var(--teal-mid, #0e7c86);
color: #fff;
}
/* ── Preset chips ── */
.ris-rep-presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ris-preset {
padding: 6px 12px;
border-radius: 50px;
border: 1.5px solid var(--border, #d6eaec);
background: var(--card-bg, #fff);
color: var(--text-muted, #6b8890);
font-family: 'DM Sans', sans-serif;
font-size: .78rem;
font-weight: 600;
cursor: pointer;
transition: all .2s;
}
.ris-preset:hover {
border-color: var(--teal-mid, #0e7c86);
}
.ris-preset.active {
background: var(--teal-mid, #0e7c86);
color: #fff;
border-color: var(--teal-mid, #0e7c86);
}
</style>
<script>
@ -929,7 +975,95 @@
month: "short", day: "numeric", year: "numeric"
});
}
// ══════════════════════════════════════════════════════════════════════
// REPORT DATE-RANGE MODAL
// ══════════════════════════════════════════════════════════════════════
(function initReportModal() {
const trigger = document.getElementById("ris-report-btn");
const overlay = document.getElementById("ris-report-modal-overlay");
if (!trigger || !overlay) return;
const fromEl = document.getElementById("ris-rep-from");
const toEl = document.getElementById("ris-rep-to");
const errEl = document.getElementById("ris-rep-err");
const presets = document.getElementById("ris-rep-presets");
const dismiss = document.getElementById("ris-rep-dismiss");
const viewBtn = document.getElementById("ris-rep-view");
const pdfBtn = document.getElementById("ris-rep-pdf");
const fmt = d => d.toISOString().slice(0, 10); // yyyy-MM-dd
const today = () => new Date();
function applyPreset(btn) {
presets.querySelectorAll(".ris-preset")
.forEach(b => b.classList.remove("active"));
btn.classList.add("active");
const to = today();
let from = new Date();
if (btn.dataset.month) {
from = new Date(to.getFullYear(), to.getMonth(), 1); // 1st of month
} else {
from.setDate(to.getDate() - parseInt(btn.dataset.days, 10));
}
fromEl.value = fmt(from);
toEl.value = fmt(to);
errEl.style.display = "none";
}
// Default 15-day range on open
function openModal() {
const def = presets.querySelector('[data-days="15"]');
applyPreset(def);
overlay.style.display = "flex";
}
function closeModal() { overlay.style.display = "none"; }
trigger.addEventListener("click", openModal);
dismiss.addEventListener("click", closeModal);
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
presets.querySelectorAll(".ris-preset").forEach(b =>
b.addEventListener("click", () => applyPreset(b)));
// Manual edits clear the active preset
[fromEl, toEl].forEach(el =>
el.addEventListener("input", () => {
presets.querySelectorAll(".ris-preset")
.forEach(b => b.classList.remove("active"));
errEl.style.display = "none";
}));
function validRange() {
if (!fromEl.value || !toEl.value) return false;
if (new Date(fromEl.value) > new Date(toEl.value)) {
errEl.style.display = "block";
return false;
}
return true;
}
function buildUrl(action) {
const p = new URLSearchParams({
dateFrom: fromEl.value,
dateTo: toEl.value
});
return `/RISMgmt/${action}?${p}`;
}
viewBtn.addEventListener("click", () => {
if (!validRange()) return;
window.open(buildUrl("RISReport"), "_blank"); // viewer in new tab
closeModal();
});
pdfBtn.addEventListener("click", () => {
if (!validRange()) return;
window.open(buildUrl("RISReportPdf"), "_blank"); // PDF download
closeModal();
});
})();
// ── Initial load ───────────────────────────────────────────────────────
fetchData();
})();

View File

@ -0,0 +1,531 @@
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0,0,0,0)
}
.page {
background: var(--color-background-primary);
border: 0.5px solid var(--color-border-tertiary);
border-radius: var(--border-radius-lg);
margin-bottom: 24px;
overflow: hidden
}
.page-tab {
display: flex;
gap: 0;
border-bottom: 0.5px solid var(--color-border-tertiary);
background: var(--color-background-secondary)
}
.tab-btn {
padding: 10px 18px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
border: none;
background: none;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all .15s
}
.tab-btn.active {
color: var(--color-text-primary);
border-bottom-color: var(--color-text-primary);
background: var(--color-background-primary)
}
.report-page {
display: none;
padding: 0
}
.report-page.active {
display: block
}
/* Report header band */
.rpt-header {
padding: 20px 24px 16px;
border-bottom: 0.5px solid var(--color-border-tertiary)
}
.rpt-header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 14px
}
.rpt-company {
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 4px
}
.rpt-title {
font-size: 20px;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 2px
}
.rpt-subtitle {
font-size: 12px;
color: var(--color-text-secondary)
}
.rpt-logo {
width: 40px;
height: 40px;
border-radius: var(--border-radius-md);
background: var(--color-background-info);
display: flex;
align-items: center;
justify-content: center
}
.rpt-logo i {
font-size: 20px;
color: var(--color-text-info)
}
.rpt-meta {
display: flex;
gap: 20px
}
.rpt-meta-item {
display: flex;
flex-direction: column;
gap: 2px
}
.rpt-meta-lbl {
font-size: 10px;
font-weight: 500;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: .07em
}
.rpt-meta-val {
font-size: 12px;
font-weight: 500;
color: var(--color-text-primary)
}
/* KPI strip */
.kpi-strip {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 0;
border-bottom: 0.5px solid var(--color-border-tertiary)
}
.kpi-cell {
padding: 14px 18px;
border-right: 0.5px solid var(--color-border-tertiary)
}
.kpi-cell:last-child {
border-right: none
}
.kpi-lbl {
font-size: 10px;
font-weight: 500;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 5px
}
.kpi-lbl i {
font-size: 13px
}
.kpi-val {
font-size: 22px;
font-weight: 500;
color: var(--color-text-primary);
line-height: 1.1
}
.kpi-sub {
font-size: 11px;
color: var(--color-text-secondary);
margin-top: 3px
}
.kpi-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10px;
font-weight: 500;
padding: 2px 7px;
border-radius: var(--border-radius-md);
margin-top: 4px
}
.badge-up {
background: #EAF3DE;
color: #3B6D11
}
.badge-dn {
background: #FCEBEB;
color: #A32D2D
}
.badge-neu {
background: var(--color-background-secondary);
color: var(--color-text-secondary)
}
/* Section */
.rpt-section {
padding: 16px 24px
}
.rpt-section + .rpt-section {
border-top: 0.5px solid var(--color-border-tertiary)
}
.rpt-section-title {
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px
}
.rpt-section-title i {
font-size: 14px
}
/* Table */
.rpt-table {
width: 100%;
border-collapse: collapse;
font-size: 12px
}
.rpt-table th {
text-align: left;
font-size: 10px;
font-weight: 500;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: .07em;
padding: 6px 10px;
border-bottom: 0.5px solid var(--color-border-tertiary);
white-space: nowrap
}
.rpt-table td {
padding: 8px 10px;
border-bottom: 0.5px solid var(--color-border-tertiary);
color: var(--color-text-primary);
vertical-align: middle
}
.rpt-table tr:last-child td {
border-bottom: none
}
.rpt-table tr:hover td {
background: var(--color-background-secondary)
}
.rpt-table .num {
text-align: right;
font-variant-numeric: tabular-nums
}
.rpt-table .total-row td {
font-weight: 500;
background: var(--color-background-secondary)
}
/* Status pill */
.pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--border-radius-md);
font-size: 10px;
font-weight: 500
}
.pill-draft {
background: #F1EFE8;
color: #5F5E5A
}
.pill-approved {
background: #EAF3DE;
color: #3B6D11
}
.pill-cancelled {
background: #FCEBEB;
color: #A32D2D
}
/* Mini bar */
.mini-bar-wrap {
display: flex;
align-items: center;
gap: 8px
}
.mini-bar-track {
flex: 1;
height: 5px;
border-radius: 3px;
background: var(--color-border-tertiary);
overflow: hidden
}
.mini-bar-fill {
height: 100%;
border-radius: 3px
}
.fill-teal {
background: #1D9E75
}
.fill-coral {
background: #D85A30
}
.fill-amber {
background: #EF9F27
}
.fill-blue {
background: #378ADD
}
/* Signature block */
.sig-block {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 16px;
margin-top: 4px
}
.sig-item {
border-top: 0.5px solid var(--color-border-tertiary);
padding-top: 8px
}
.sig-role {
font-size: 10px;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 16px
}
.sig-name {
font-size: 11px;
font-weight: 500;
color: var(--color-text-primary)
}
/* 2-col layout */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-top: 0.5px solid var(--color-border-tertiary)
}
.col-left {
padding: 16px 24px;
border-right: 0.5px solid var(--color-border-tertiary)
}
.col-right {
padding: 16px 24px
}
/* Timeline strip for MRS condition */
.cond-row {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 0;
border-bottom: 0.5px solid var(--color-border-tertiary)
}
.cond-row:last-child {
border-bottom: none
}
.cond-name {
font-size: 12px;
color: var(--color-text-primary);
min-width: 70px
}
.cond-bar-track {
flex: 1;
height: 6px;
border-radius: 3px;
background: var(--color-border-tertiary);
overflow: hidden
}
.cond-bar-fill {
height: 100%;
border-radius: 3px
}
.cond-count {
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
min-width: 40px;
text-align: right
}
/* Footer */
.rpt-footer {
padding: 10px 24px;
border-top: 0.5px solid var(--color-border-tertiary);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--color-background-secondary)
}
.rpt-footer-lbl {
font-size: 10px;
color: var(--color-text-secondary)
}
/* Inventory specific */
.inv-row-hd {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 80px;
gap: 0
}
.stock-level {
display: flex;
flex-direction: column;
gap: 3px
}
.stock-pct {
font-size: 10px;
color: var(--color-text-secondary);
text-align: right
}
</style>
<h2 class="sr-only">Finance department inventory report designs — three report templates: RIS report, MRS report, and inventory summary</h2>
<div class="page-tab">
<button class="tab-btn active" onclick="showTab('ris')">Return Issuance Slip report</button>
<button class="tab-btn" onclick="showTab('mrs')">Material Return Slip report</button>
<button class="tab-btn" onclick="showTab('inv')">Inventory summary report</button>
</div>
<script>
(function () {
"use strict";
// ── Prevent re-initialization on cache restore ──
if (window.__invTab1Initialized) return;
window.__invTab1Initialized = true;
const tabContent = document.getElementById("inv-tab-content");
const tabBtns = document.querySelectorAll(".inv-tab-btn");
// Cache stores the raw HTML string per tab id
const cache = {};
let activeTabId = null;
async function loadTab(tabId) {
if (activeTabId === tabId) return;
activeTabId = tabId;
// Mark active button
tabBtns.forEach(b => b.classList.toggle("active", b.dataset.tabId === String(tabId)));
if (cache[tabId]) {
// ── FIX: inject cached HTML then re-run its <script> blocks ──────
injectHtml(tabContent, cache[tabId]);
return;
}
tabContent.innerHTML = `<div class="inv-tab-loading">
<div class="inv-spinner"></div><span>Loading…</span></div>`;
try {
const res = await fetch(`/InventoryMgmt/GetInventoryTabPage?id=${tabId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
cache[tabId] = html;
injectHtml(tabContent, html);
} catch (err) {
console.error("Tab load error:", err);
tabContent.innerHTML = `
<div class="inv-placeholder">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<h3>Failed to load</h3>
<p>Please try again or refresh the page.</p>
</div>`;
}
}
/**
* Set innerHTML then re-execute every <script> block so IIFEs inside
* ViewComponent views fire correctly — both on first load AND cache restore.
*/
function injectHtml(container, html) {
container.innerHTML = html;
container.querySelectorAll("script").forEach(oldScript => {
const newScript = document.createElement("script");
Array.from(oldScript.attributes).forEach(attr =>
newScript.setAttribute(attr.name, attr.value));
newScript.textContent = oldScript.textContent;
oldScript.replaceWith(newScript);
});
}
// Wire tab clicks
tabBtns.forEach(btn =>
btn.addEventListener("click", () => loadTab(parseInt(btn.dataset.tabId, 10)))
);
// Auto-load the first tab on page ready
loadTab(1);
})();
</script>

View File

@ -0,0 +1,710 @@
<style>
body {
font-family: 'DM Sans', Arial, sans-serif;
background: #fff;
color: #1a2e35;
padding: 24px;
}
@@media print {
body { padding: 0; margin: 0; background: #fff;}
body * { visibility: hidden;}
#inv-rpt-page, #inv-rpt-page * {
visibility: visible;
}
#inv-rpt-page {
position: absolute;
left: 0;
top: 0;
width: 100%;
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
}
.no-print {
display: none !important;
}
.rpt-table tr {
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
}
.rpt-page {
width: 100%;
margin: 0;
background: #fff;
border: 1px solid var(--border, #d6eaec);
border-radius: var(--radius-lg, 14px);
overflow: hidden;
}
.rpt-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #d6eaec;
}
/* ── FILTER BUTTONS (blended with shared theme) ── */
.rpt-date-group {
display: flex;
align-items: center;
gap: 8px;
border: 1.5px solid var(--border, #d6eaec);
border-radius: var(--radius-sm, 8px);
padding: 0 12px;
background: #fff;
}
.rpt-date-lbl {
display: flex;
align-items: center;
gap: 6px;
font-size: .8rem;
font-weight: 600;
color: var(--text-muted, #6b8890);
white-space: nowrap;
}
.rpt-date-input {
border: none;
outline: none;
background: transparent;
padding: 9px 0;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark, #1a2e35);
}
.rpt-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 18px;
border-radius: var(--radius-sm, 8px);
border: none;
font-family: 'DM Sans', sans-serif;
font-size: .84rem;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
white-space: nowrap;
}
.rpt-btn-primary {
background: var(--teal-mid, #0e7c86);
color: #fff;
box-shadow: 0 2px 8px rgba(14,124,134,.3);
}
.rpt-btn-primary:hover {
background: var(--teal-dark, #0d5c63);
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(14,124,134,.35);
}
.rpt-btn-outline {
background: #fff;
color: var(--teal-dark, #0d5c63);
border: 1.5px solid var(--teal-mid, #0e7c86);
}
.rpt-btn-outline:hover {
background: var(--teal-pale, #e6f7f8);
border-color: var(--teal-dark, #0d5c63);
}
.rpt-header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 14px;
}
.rpt-company {
font-size: 11px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 4px;
}
.rpt-title {
font-size: 20px;
font-weight: 700;
color: #1a2e35;
margin-bottom: 2px;
}
.rpt-subtitle {
font-size: 12px;
color: #6b8890;
}
.rpt-logo {
width: 40px;
height: 40px;
border-radius: 8px;
background: #EAF3DE;
display: flex;
align-items: center;
justify-content: center;
}
.rpt-logo i {
font-size: 20px;
color: #3B6D11;
}
.rpt-meta {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.rpt-meta-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.rpt-meta-lbl {
font-size: 10px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
}
.rpt-meta-val {
font-size: 12px;
font-weight: 600;
color: #1a2e35;
}
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border-bottom: 1px solid #d6eaec;
}
.kpi-cell {
padding: 14px 18px;
border-right: 1px solid #d6eaec;
}
.kpi-cell:last-child {
border-right: none;
}
.kpi-lbl {
font-size: 10px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 4px;
}
.kpi-val {
font-size: 22px;
font-weight: 700;
color: #1a2e35;
}
.rpt-section {
padding: 16px 24px;
}
.rpt-section + .rpt-section {
border-top: 1px solid #d6eaec;
}
.rpt-section-title {
font-size: 11px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 12px;
}
.rpt-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.rpt-table thead tr {
background: linear-gradient(135deg, #1a3a4a, #1e5468);
}
.rpt-table th {
text-align: left;
font-size: 10px;
font-weight: 700;
color: rgba(255,255,255,.85);
text-transform: uppercase;
letter-spacing: .06em;
padding: 9px 12px;
white-space: nowrap;
}
.rpt-table td {
padding: 9px 12px;
border-bottom: 1px solid #d6eaec;
vertical-align: middle;
}
.rpt-table tr:last-child td {
border-bottom: none;
}
.rpt-table .num {
text-align: right;
}
.rpt-table .total-row td {
font-weight: 700;
background: #f0f6f7;
}
.pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 9px;
border-radius: 50px;
font-size: 10px;
font-weight: 600;
}
.mini-bar-track {
flex: 1;
height: 5px;
border-radius: 3px;
background: #d6eaec;
overflow: hidden;
}
.mini-bar-fill {
height: 100%;
border-radius: 3px;
}
.fill-teal {
background: #1D9E75;
}
.fill-blue {
background: #378ADD;
}
.fill-amber {
background: #EF9F27;
}
.fill-coral {
background: #D85A30;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-top: 1px solid #d6eaec;
}
.col-left {
padding: 16px 24px;
border-right: 1px solid #d6eaec;
}
.col-right {
padding: 16px 24px;
}
.sig-block {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.sig-item {
border-top: 1px solid #d6eaec;
padding-top: 8px;
}
.sig-role {
font-size: 10px;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 16px;
}
.sig-name {
font-size: 11px;
font-weight: 600;
color: #1a2e35;
}
.rpt-footer {
padding: 10px 24px;
border-top: 1px solid #d6eaec;
display: flex;
align-items: center;
justify-content: space-between;
background: #f0f6f7;
}
.rpt-footer-lbl {
font-size: 10px;
color: #6b8890;
}
.loading-state {
text-align: center;
padding: 80px 20px;
color: #6b8890;
}
</style>
@await Html.PartialAsync("PagesView/Inventory/_InventoryReportHelper")
<script>
(function () {
"use strict";
const fromEl = document.getElementById("inv-rpt-from");
const toEl = document.getElementById("inv-rpt-to");
const genBtn = document.getElementById("inv-rpt-generate");
const csvBtn = document.getElementById("inv-rpt-csv");
const xlsxBtn = document.getElementById("inv-rpt-excel");
const container = document.getElementById("inv-rpt-container");
if (!fromEl || !toEl || !container) {
console.error("Inventory report subtab init failed — missing elements.");
return;
}
// default: first day of current month -> today
const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1);
toEl.value = today.toISOString().slice(0, 10);
fromEl.value = first.toISOString().slice(0, 10);
let lastData = null; // cache for export
function buildParams() {
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
}
csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender);
async function fetchAndRender() {
container.innerHTML = `<div class="inv-tab-loading">
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
try {
const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
lastData = json.data ?? json;
renderReport(lastData);
} catch (err) {
console.error("Inventory report fetch error:", err);
container.innerHTML = `
<div class="inv-placeholder">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<h3>Failed to load report</h3>
<p>Please try again.</p>
</div>`;
}
}
// ── CSV export (client-side, no backend needed) ──
function exportCsv() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCat = lastData.byCategory ?? [];
const alerts = lastData.alerts ?? [];
const summary = lastData.summary ?? {};
const headers = ["Item Name","Item No.","Category","Lot No.","Qty In","Qty Out","On Hand","Stock %"];
const csvLines = [
`Inventory Summary Report`,
`Company,${csvCell(lastData.companyName)}`,
`Report No,${csvCell(lastData.reportNo)}`,
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
``,
// ── Inventory detail ──
headers.map(csvCell).join(","),
...rows.map(r => [
r.itemName, r.itemNo, r.itemCategoryName, r.lotNo,
r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct
].map(csvCell).join(",")),
["Total","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, ""].map(csvCell).join(","),
``,
// ── Stock level by category ──
`Stock Level by Category`,
["Category","Avg Stock %"].map(csvCell).join(","),
...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")),
``,
// ── Items requiring attention ──
`Items Requiring Attention`,
["Item","On Hand","Alert"].map(csvCell).join(","),
...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(","))
];
downloadBlob(
"\uFEFF" + csvLines.join("\r\n"),
`Inventory_Report_${fromEl.value}_to_${toEl.value}.csv`,
"text/csv;charset=utf-8;"
);
}
function csvCell(v) {
const s = String(v ?? "");
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename;
document.body.appendChild(a); a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function renderReport(data) {
const summary = data.summary ?? {};
const rows = data.rows ?? [];
const byCat = data.byCategory ?? [];
const alerts = data.alerts ?? [];
const catFillColors = ["fill-teal", "fill-blue", "fill-amber", "fill-coral"];
const maxCatPct = Math.max(1, ...byCat.map(c => c.avgStockPct));
container.innerHTML = `
<div class="rpt-page" id="inv-rpt-page">
<div class="rpt-header">
<div class="rpt-header-top">
<div>
<div class="rpt-company">${_esc(data.companyName ?? "")}</div>
<div class="rpt-title">Inventory summary Report</div>
<div class="rpt-subtitle">As of ${_fmtDate(data.asOf)} · All departments · All categories</div>
</div>
<div class="rpt-logo" style="background:#EAF3DE">
<i class="fas fa-boxes" style="color:#3B6D11"></i>
</div>
</div>
<div class="rpt-meta">
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Prepared by</span>
<span class="rpt-meta-val">${_esc(data.preparedBy ?? "Finance Department")}</span>
</div>
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Print date</span>
<span class="rpt-meta-val">${_fmtDate(new Date().toISOString())}</span>
</div>
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Report no.</span>
<span class="rpt-meta-val">${_esc(data.reportNo ?? "")}</span>
</div>
</div>
</div>
<div class="kpi-strip" style="grid-template-columns:repeat(4,1fr)">
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-box"></i> Total SKUs</div>
<div class="kpi-val">${summary.totalSKUs ?? 0}</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-layer-group"></i> Total on hand</div>
<div class="kpi-val">${(summary.totalOnHand ?? 0).toLocaleString()}</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-exclamation-triangle"></i> Low stock</div>
<div class="kpi-val" style="color:#A32D2D">${summary.lowStockCount ?? 0}</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-ban"></i> Out of stock</div>
<div class="kpi-val" style="color:#A32D2D">${summary.outOfStockCount ?? 0}</div>
</div>
</div>
<div class="rpt-section">
<div class="rpt-section-title"><i class="fas fa-list"></i> Inventory detail by item</div>
<table class="rpt-table">
<thead>
<tr>
<th>Item Name</th><th>Item No.</th><th>Category</th><th>Lot No.</th>
<th class="num">Qty In</th><th class="num">Qty Out</th>
<th class="num">On Hand</th><th>Stock Level</th>
</tr>
</thead>
<tbody>
${rows.map(r => `
<tr>
<td style="font-weight:600">${_esc(r.itemName)}</td>
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td>
<td style="color:var(--text-muted,#6b8890)">${_esc(r.lotNo)}</td>
<td class="num">${r.qtyIn}</td>
<td class="num">${r.qtyOut}</td>
<td class="num" style="font-weight:600">${r.qtyOnHand}</td>
<td>${_stockBar(r.stockPct)}</td>
</tr>`).join("")}
<tr class="total-row">
<td colspan="4">Total</td>
<td class="num">${summary.totalQtyIn ?? 0}</td>
<td class="num">${summary.totalQtyOut ?? 0}</td>
<td class="num">${summary.totalOnHand ?? 0}</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="two-col">
<div class="col-left">
<div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Stock level by category</div>
<div style="display:flex;flex-direction:column;gap:8px">
${byCat.map((c, i) => `
<div>
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
<span>${_esc(c.categoryName)}</span>
<span style="font-weight:600">${c.avgStockPct}% avg</span>
</div>
<div class="mini-bar-track">
<div class="mini-bar-fill ${catFillColors[i % catFillColors.length]}"
style="width:${c.avgStockPct}%"></div>
</div>
</div>`).join("")}
</div>
</div>
<div class="col-right">
<div class="rpt-section-title"><i class="fas fa-exclamation-triangle"></i> Items requiring attention</div>
<table class="rpt-table">
<thead><tr><th>Item</th><th class="num">On Hand</th><th>Alert</th></tr></thead>
<tbody>
${alerts.map(a => `
<tr>
<td>${_esc(a.itemName)}</td>
<td class="num" style="color:#A32D2D">${a.qtyOnHand}</td>
<td>${_alertPill(a.severity)}</td>
</tr>`).join("")}
</tbody>
</table>
</div>
</div>
<div class="rpt-section">
<div class="sig-block">
<div class="sig-item"><div class="sig-role">Prepared by</div><div class="sig-name">Finance Officer</div></div>
<div class="sig-item"><div class="sig-role">Reviewed by</div><div class="sig-name">Finance Manager</div></div>
<div class="sig-item"><div class="sig-role">Approved by</div><div class="sig-name">Finance Director</div></div>
</div>
</div>
<div class="rpt-footer">
<span class="rpt-footer-lbl">${_esc(data.companyName ?? "")} — Confidential — For internal use only</span>
</div>
</div>`;
}
function _stockBar(pct) {
const cls = pct < 20 ? "fill-coral" : pct < 50 ? "fill-amber" : "fill-teal";
const color = pct < 20 ? "#A32D2D" : pct < 50 ? "#854F0B" : "inherit";
return `<div style="display:flex;flex-direction:column;gap:3px">
<div class="mini-bar-track"><div class="mini-bar-fill ${cls}" style="width:${pct}%"></div></div>
<span style="font-size:10px;color:${color};text-align:right">${pct}%${pct < 20 ? " ⚠" : ""}</span>
</div>`;
}
function _alertPill(severity) {
const map = {
"Critical": { bg: "#FCEBEB", fg: "#A32D2D" },
"Low": { bg: "#FAEEDA", fg: "#854F0B" }
};
const c = map[severity] ?? map["Low"];
return `<span class="pill" style="background:${c.bg};color:${c.fg}">${_esc(severity)}</span>`;
}
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
function exportExcel() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCat = lastData.byCategory ?? [];
const alerts = lastData.alerts ?? [];
const summary = lastData.summary ?? {};
const headerCells = ["Item Name","Item No.","Category","Lot No.","Qty In","Qty Out","On Hand","Stock %"]
.map(h => `<th>${_esc(h)}</th>`).join("");
const bodyRows = rows.map(r => `
<tr>
<td>${_esc(r.itemName)}</td>
<td>${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td>
<td>${_esc(r.lotNo)}</td>
<td>${r.qtyIn}</td>
<td>${r.qtyOut}</td>
<td>${r.qtyOnHand}</td>
<td>${r.stockPct}</td>
</tr>`).join("");
const catRows = byCat.map(c => `
<tr><td>${_esc(c.categoryName)}</td><td>${c.avgStockPct}</td></tr>`).join("");
const alertRows = alerts.map(a => `
<tr><td>${_esc(a.itemName)}</td><td>${a.qtyOnHand}</td><td>${_esc(a.severity)}</td></tr>`).join("");
const html = `
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40">
<head><meta charset="utf-8"></head>
<body>
<table border="1">
<tr><td colspan="8"><b>Inventory Summary Report</b></td></tr>
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr></tr>
<tr>${headerCells}</tr>
${bodyRows}
<tr><td colspan="4"><b>Total</b></td><td>${summary.totalQtyIn ?? 0}</td><td>${summary.totalQtyOut ?? 0}</td><td>${summary.totalOnHand ?? 0}</td><td></td></tr>
</table>
<br/>
<table border="1">
<tr><td colspan="2"><b>Stock Level by Category</b></td></tr>
<tr><th>Category</th><th>Avg Stock %</th></tr>
${catRows}
</table>
<br/>
<table border="1">
<tr><td colspan="3"><b>Items Requiring Attention</b></td></tr>
<tr><th>Item</th><th>On Hand</th><th>Alert</th></tr>
${alertRows}
</table>
</body></html>`;
downloadBlob(html, `Inventory_Report_${fromEl.value}_to_${toEl.value}.xls`,
"application/vnd.ms-excel");
}
xlsxBtn.addEventListener("click", exportExcel);
fetchAndRender();
})();
</script>

View File

@ -0,0 +1,694 @@
<style>
body {
font-family: 'DM Sans', Arial, sans-serif;
background: #fff;
color: #1a2e35;
padding: 24px;
}
@@media print {
body {
padding: 0;
margin: 0;
background: #fff;
}
body * {
visibility: hidden;
}
#mrs-rpt-page, #mrs-rpt-page * {
visibility: visible;
}
#mrs-rpt-page {
position: absolute;
left: 0;
top: 0;
width: 100%;
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
}
.no-print {
display: none !important;
}
.rpt-table tr {
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
}
.rpt-page {
width: 100%;
margin: 0;
background: #fff;
border: 1px solid var(--border, #d6eaec);
border-radius: var(--radius-lg, 14px);
overflow: hidden;
}
.rpt-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #d6eaec;
}
/* ── FILTER BUTTONS (blended with shared theme) ── */
.rpt-date-group {
display: flex;
align-items: center;
gap: 8px;
border: 1.5px solid var(--border, #d6eaec);
border-radius: var(--radius-sm, 8px);
padding: 0 12px;
background: #fff;
}
.rpt-date-lbl {
display: flex;
align-items: center;
gap: 6px;
font-size: .8rem;
font-weight: 600;
color: var(--text-muted, #6b8890);
white-space: nowrap;
}
.rpt-date-input {
border: none;
outline: none;
background: transparent;
padding: 9px 0;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark, #1a2e35);
}
.rpt-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 18px;
border-radius: var(--radius-sm, 8px);
border: none;
font-family: 'DM Sans', sans-serif;
font-size: .84rem;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
white-space: nowrap;
}
.rpt-btn-primary {
background: var(--teal-mid, #0e7c86);
color: #fff;
box-shadow: 0 2px 8px rgba(14,124,134,.3);
}
.rpt-btn-primary:hover {
background: var(--teal-dark, #0d5c63);
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(14,124,134,.35);
}
.rpt-btn-outline {
background: #fff;
color: var(--teal-dark, #0d5c63);
border: 1.5px solid var(--teal-mid, #0e7c86);
}
.rpt-btn-outline:hover {
background: var(--teal-pale, #e6f7f8);
border-color: var(--teal-dark, #0d5c63);
}
.rpt-header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 14px;
}
.rpt-company {
font-size: 11px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 4px;
}
.rpt-title {
font-size: 20px;
font-weight: 700;
color: #1a2e35;
margin-bottom: 2px;
}
.rpt-subtitle {
font-size: 12px;
color: #6b8890;
}
.rpt-logo {
width: 40px;
height: 40px;
border-radius: 8px;
background: #EAF3DE;
display: flex;
align-items: center;
justify-content: center;
}
.rpt-logo i {
font-size: 20px;
color: #3B6D11;
}
.rpt-meta {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.rpt-meta-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.rpt-meta-lbl {
font-size: 10px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
}
.rpt-meta-val {
font-size: 12px;
font-weight: 600;
color: #1a2e35;
}
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border-bottom: 1px solid #d6eaec;
}
.kpi-cell {
padding: 14px 18px;
border-right: 1px solid #d6eaec;
}
.kpi-cell:last-child {
border-right: none;
}
.kpi-lbl {
font-size: 10px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 4px;
}
.kpi-val {
font-size: 22px;
font-weight: 700;
color: #1a2e35;
}
.rpt-section {
padding: 16px 24px;
}
.rpt-section + .rpt-section {
border-top: 1px solid #d6eaec;
}
.rpt-section-title {
font-size: 11px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 12px;
}
.rpt-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.rpt-table thead tr {
background: linear-gradient(135deg, #1a3a4a, #1e5468);
}
.rpt-table th {
text-align: left;
font-size: 10px;
font-weight: 700;
color: rgba(255,255,255,.85);
text-transform: uppercase;
letter-spacing: .06em;
padding: 9px 12px;
white-space: nowrap;
}
.rpt-table td {
padding: 9px 12px;
border-bottom: 1px solid #d6eaec;
vertical-align: middle;
}
.rpt-table tr:last-child td {
border-bottom: none;
}
.rpt-table .num {
text-align: right;
}
.rpt-table .total-row td {
font-weight: 700;
background: #f0f6f7;
}
.pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 9px;
border-radius: 50px;
font-size: 10px;
font-weight: 600;
}
.mini-bar-track {
flex: 1;
height: 5px;
border-radius: 3px;
background: #d6eaec;
overflow: hidden;
}
.mini-bar-fill {
height: 100%;
border-radius: 3px;
}
.fill-teal {
background: #1D9E75;
}
.fill-blue {
background: #378ADD;
}
.fill-amber {
background: #EF9F27;
}
.fill-coral {
background: #D85A30;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-top: 1px solid #d6eaec;
}
.col-left {
padding: 16px 24px;
border-right: 1px solid #d6eaec;
}
.col-right {
padding: 16px 24px;
}
.sig-block {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.sig-item {
border-top: 1px solid #d6eaec;
padding-top: 8px;
}
.sig-role {
font-size: 10px;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 16px;
}
.sig-name {
font-size: 11px;
font-weight: 600;
color: #1a2e35;
}
.rpt-footer {
padding: 10px 24px;
border-top: 1px solid #d6eaec;
display: flex;
align-items: center;
justify-content: space-between;
background: #f0f6f7;
}
.rpt-footer-lbl {
font-size: 10px;
color: #6b8890;
}
.loading-state {
text-align: center;
padding: 80px 20px;
color: #6b8890;
}
</style>
@await Html.PartialAsync("PagesView/Inventory/_InventoryReportHelper")
<script>
(function () {
"use strict";
const fromEl = document.getElementById("inv-rpt-from");
const toEl = document.getElementById("inv-rpt-to");
const genBtn = document.getElementById("inv-rpt-generate");
const csvBtn = document.getElementById("inv-rpt-csv");
const xlsxBtn = document.getElementById("inv-rpt-excel");
const container = document.getElementById("inv-rpt-container");
if (!fromEl || !toEl || !container) {
console.error("Inventory report subtab init failed — missing elements.");
return;
}
// default: first day of current month -> today
const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1);
toEl.value = today.toISOString().slice(0, 10);
fromEl.value = first.toISOString().slice(0, 10);
let lastData = null; // cache for export
function buildParams() {
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
}
async function fetchAndRender() {
container.innerHTML = `<div class="inv-tab-loading">
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
try {
const res = await fetch(`/InventoryReports/GetMRSReport?${buildParams()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
lastData = json.data ?? json;
renderReport(lastData);
} catch (err) {
console.error("Inventory report fetch error:", err);
container.innerHTML = `
<div class="inv-placeholder">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<h3>Failed to load report</h3>
<p>Please try again.</p>
</div>`;
}
}
function renderReport(data) {
const summary = data.summary ?? {};
const rows = data.rows ?? [];
const byCond = data.byCondition ?? [];
const condFillMap = { "Good": "fill-teal", "Partial": "fill-amber", "Damaged": "fill-coral" };
const maxCondQty = Math.max(1, ...byCond.map(c => c.totalQty));
container.innerHTML = `
<div class="rpt-page" id="mrs-rpt-page">
<div class="rpt-header">
<div class="rpt-header-top">
<div>
<div class="rpt-company">${_esc(data.companyName ?? "")}</div>
<div class="rpt-title">Material Return Slip Report</div>
<div class="rpt-subtitle">
Period: ${_fmtDate(data.dateFrom)} ${_fmtDate(data.dateTo)} · All departments
</div>
</div>
<div class="rpt-logo" style="background:var(--bg-page,#f0f6f7)">
<i class="fas fa-file-import" style="color:#185FA5"></i>
</div>
</div>
<div class="rpt-meta">
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Prepared by</span>
<span class="rpt-meta-val">${_esc(data.preparedBy ?? "Finance Department")}</span>
</div>
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Print date</span>
<span class="rpt-meta-val">${_fmtDate(new Date().toISOString())}</span>
</div>
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Report no.</span>
<span class="rpt-meta-val">${_esc(data.reportNo ?? "")}</span>
</div>
</div>
</div>
<div class="kpi-strip">
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-file-import"></i> Total MRS</div>
<div class="kpi-val">${summary.totalMRS ?? 0}</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-undo"></i> Total qty returned</div>
<div class="kpi-val">${summary.totalQtyReturned ?? 0}</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-tag"></i> Good condition</div>
<div class="kpi-val">${summary.goodConditionPct ?? 0}%</div>
<div class="kpi-sub">of returned items</div>
</div>
</div>
<div class="rpt-section">
<div class="rpt-section-title"><i class="fas fa-list"></i> Return detail</div>
<table class="rpt-table">
<thead>
<tr>
<th>MRS No.</th><th>Date</th><th>Against RIS</th><th>Item</th>
<th>Returned By</th><th class="num">Qty Returned</th>
<th>Condition</th><th>Status</th>
</tr>
</thead>
<tbody>
${rows.map(r => `
<tr>
<td style="font-weight:600;color:#185FA5">${_esc(r.mrsNo)}</td>
<td>${_fmtDate(r.createdDate)}</td>
<td style="color:var(--text-muted,#6b8890)">${_esc(r.risNo)}</td>
<td>${_esc(r.itemName)}</td>
<td>${_esc(r.returnedBy)}</td>
<td class="num">${r.qtyReturned}</td>
<td>${_condPill(r.condition)}</td>
<td>${_statusPill(r.status, r.statusLabel)}</td>
</tr>`).join("")}
<tr class="total-row">
<td colspan="5">Total</td>
<td class="num">${summary.totalQtyReturned ?? 0}</td>
<td colspan="2"></td>
</tr>
</tbody>
</table>
</div>
<div class="two-col">
<div class="col-left">
<div class="rpt-section-title"><i class="fas fa-tag"></i> Returns by condition</div>
<div style="display:flex;flex-direction:column;gap:8px">
${byCond.map(c => `
<div>
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
<span>${_esc(c.condition)}</span>
<span style="font-weight:600">${c.totalQty} pcs</span>
</div>
<div class="mini-bar-track">
<div class="mini-bar-fill ${condFillMap[c.condition] ?? 'fill-teal'}"
style="width:${Math.round((c.totalQty / maxCondQty) * 100)}%"></div>
</div>
</div>`).join("")}
</div>
</div>
<div class="col-right">
<div class="rpt-section-title"><i class="fas fa-exchange-alt"></i> RIS vs MRS comparison</div>
<table class="rpt-table">
<thead><tr><th>Metric</th><th class="num">Value</th></tr></thead>
<tbody>
<tr><td>Total qty issued (RIS)</td><td class="num">${summary.totalQtyIssuedRIS ?? 0}</td></tr>
<tr><td>Total qty returned (MRS)</td><td class="num">${summary.totalQtyReturned ?? 0}</td></tr>
<tr><td style="font-weight:600">Net qty consumed</td><td class="num" style="font-weight:600">${summary.netQtyConsumed ?? 0}</td></tr>
<tr><td>Return rate</td><td class="num">${summary.returnRatePct ?? 0}%</td></tr>
</tbody>
</table>
</div>
</div>
<div class="rpt-section">
<div class="sig-block">
<div class="sig-item"><div class="sig-role">Prepared by</div><div class="sig-name">Finance Officer</div></div>
<div class="sig-item"><div class="sig-role">Reviewed by</div><div class="sig-name">Finance Manager</div></div>
<div class="sig-item"><div class="sig-role">Approved by</div><div class="sig-name">Finance Director</div></div>
</div>
</div>
<div class="rpt-footer">
<span class="rpt-footer-lbl">${_esc(data.companyName ?? "")} — Confidential — For internal use only</span>
</div>
</div>`;
}
csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender);
function _condPill(cond) {
const map = {
"Good": { bg: "#EAF3DE", fg: "#3B6D11", icon: "fa-check" },
"Partial": { bg: "#FAEEDA", fg: "#854F0B", icon: "fa-adjust" },
"Damaged": { bg: "#FCEBEB", fg: "#A32D2D", icon: "fa-exclamation-circle" }
};
const c = map[cond] ?? map["Good"];
return `<span class="pill" style="background:${c.bg};color:${c.fg}">
<i class="fas ${c.icon}"></i> ${_esc(cond)}</span>`;
}
function _statusPill(status, label) {
const cls = status === 1 ? "pill-approved" : status === 2 ? "pill-cancelled" : "pill-draft";
const icon = status === 1 ? "fa-check" : status === 2 ? "fa-ban" : "fa-clock";
return `<span class="pill ${cls}"><i class="fas ${icon}"></i> ${_esc(label)}</span>`;
}
// ── CSV export (MRS schema) ──
function exportCsv() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCond = lastData.byCondition ?? [];
const summary = lastData.summary ?? {};
const headers = ["MRS No.","Date","Against RIS","Item","Returned By","Qty Returned","Condition","Status"];
const csvLines = [
`Material Return Slip Report`,
`Company,${csvCell(lastData.companyName)}`,
`Report No,${csvCell(lastData.reportNo)}`,
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
``,
headers.map(csvCell).join(","),
...rows.map(r => [
r.mrsNo, r.createdDate, r.risNo, r.itemName,
r.returnedBy, r.qtyReturned, r.condition, r.statusLabel
].map(csvCell).join(",")),
["Total","","","","", summary.totalQtyReturned ?? 0, "", ""].map(csvCell).join(","),
``,
`Returns by Condition`,
["Condition","Total Qty"].map(csvCell).join(","),
...byCond.map(c => [c.condition, c.totalQty].map(csvCell).join(",")),
``,
`RIS vs MRS Comparison`,
["Metric","Value"].map(csvCell).join(","),
["Total qty issued (RIS)", summary.totalQtyIssuedRIS ?? 0].map(csvCell).join(","),
["Total qty returned (MRS)", summary.totalQtyReturned ?? 0].map(csvCell).join(","),
["Net qty consumed", summary.netQtyConsumed ?? 0].map(csvCell).join(","),
["Return rate %", summary.returnRatePct ?? 0].map(csvCell).join(",")
];
downloadBlob(
"\uFEFF" + csvLines.join("\r\n"),
`MRS_Report_${fromEl.value}_to_${toEl.value}.csv`,
"text/csv;charset=utf-8;"
);
}
// ── Excel export (MRS schema, HTML-table .xls) ──
function exportExcel() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCond = lastData.byCondition ?? [];
const summary = lastData.summary ?? {};
const headerCells = ["MRS No.","Date","Against RIS","Item","Returned By","Qty Returned","Condition","Status"]
.map(h => `<th>${_esc(h)}</th>`).join("");
const bodyRows = rows.map(r => `
<tr>
<td>${_esc(r.mrsNo)}</td>
<td>${_esc(_fmtDate(r.createdDate))}</td>
<td>${_esc(r.risNo)}</td>
<td>${_esc(r.itemName)}</td>
<td>${_esc(r.returnedBy)}</td>
<td>${r.qtyReturned}</td>
<td>${_esc(r.condition)}</td>
<td>${_esc(r.statusLabel)}</td>
</tr>`).join("");
const condRows = byCond.map(c => `
<tr><td>${_esc(c.condition)}</td><td>${c.totalQty}</td></tr>`).join("");
const html = `
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40">
<head><meta charset="utf-8"></head>
<body>
<table border="1">
<tr><td colspan="8"><b>Material Return Slip Report</b></td></tr>
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr></tr>
<tr>${headerCells}</tr>
${bodyRows}
<tr><td colspan="5"><b>Total</b></td><td>${summary.totalQtyReturned ?? 0}</td><td colspan="2"></td></tr>
</table>
<br/>
<table border="1">
<tr><td colspan="2"><b>Returns by Condition</b></td></tr>
<tr><th>Condition</th><th>Total Qty</th></tr>
${condRows}
</table>
<br/>
<table border="1">
<tr><td colspan="2"><b>RIS vs MRS Comparison</b></td></tr>
<tr><th>Metric</th><th>Value</th></tr>
<tr><td>Total qty issued (RIS)</td><td>${summary.totalQtyIssuedRIS ?? 0}</td></tr>
<tr><td>Total qty returned (MRS)</td><td>${summary.totalQtyReturned ?? 0}</td></tr>
<tr><td>Net qty consumed</td><td>${summary.netQtyConsumed ?? 0}</td></tr>
<tr><td>Return rate %</td><td>${summary.returnRatePct ?? 0}</td></tr>
</table>
</body></html>`;
downloadBlob(html, `MRS_Report_${fromEl.value}_to_${toEl.value}.xls`,
"application/vnd.ms-excel");
}
xlsxBtn.addEventListener("click", exportExcel);
fetchAndRender();
})();
</script>

View File

@ -0,0 +1,700 @@
<style>
body {
font-family: 'DM Sans', Arial, sans-serif;
background: #fff;
color: #1a2e35;
padding: 24px;
}
@@media print {
body {
padding: 0;
margin: 0;
background: #fff;
}
body * {
visibility: hidden;
}
#ris-rpt-page, #ris-rpt-page * {
visibility: visible;
}
#ris-rpt-page {
position: absolute;
left: 0;
top: 0;
width: 100%;
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
}
.no-print {
display: none !important;
}
.rpt-table tr {
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
}
.rpt-page {
width: 100%;
margin: 0;
background: #fff;
border: 1px solid var(--border, #d6eaec);
border-radius: var(--radius-lg, 14px);
overflow: hidden;
}
.rpt-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #d6eaec;
}
/* ── FILTER BUTTONS (blended with shared theme) ── */
.rpt-date-group {
display: flex;
align-items: center;
gap: 8px;
border: 1.5px solid var(--border, #d6eaec);
border-radius: var(--radius-sm, 8px);
padding: 0 12px;
background: #fff;
}
.rpt-date-lbl {
display: flex;
align-items: center;
gap: 6px;
font-size: .8rem;
font-weight: 600;
color: var(--text-muted, #6b8890);
white-space: nowrap;
}
.rpt-date-input {
border: none;
outline: none;
background: transparent;
padding: 9px 0;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark, #1a2e35);
}
.rpt-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 18px;
border-radius: var(--radius-sm, 8px);
border: none;
font-family: 'DM Sans', sans-serif;
font-size: .84rem;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
white-space: nowrap;
}
.rpt-btn-primary {
background: var(--teal-mid, #0e7c86);
color: #fff;
box-shadow: 0 2px 8px rgba(14,124,134,.3);
}
.rpt-btn-primary:hover {
background: var(--teal-dark, #0d5c63);
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(14,124,134,.35);
}
.rpt-btn-outline {
background: #fff;
color: var(--teal-dark, #0d5c63);
border: 1.5px solid var(--teal-mid, #0e7c86);
}
.rpt-btn-outline:hover {
background: var(--teal-pale, #e6f7f8);
border-color: var(--teal-dark, #0d5c63);
}
.rpt-header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 14px;
}
.rpt-company {
font-size: 11px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 4px;
}
.rpt-title {
font-size: 20px;
font-weight: 700;
color: #1a2e35;
margin-bottom: 2px;
}
.rpt-subtitle {
font-size: 12px;
color: #6b8890;
}
.rpt-logo {
width: 40px;
height: 40px;
border-radius: 8px;
background: #EAF3DE;
display: flex;
align-items: center;
justify-content: center;
}
.rpt-logo i {
font-size: 20px;
color: #3B6D11;
}
.rpt-meta {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.rpt-meta-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.rpt-meta-lbl {
font-size: 10px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
}
.rpt-meta-val {
font-size: 12px;
font-weight: 600;
color: #1a2e35;
}
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border-bottom: 1px solid #d6eaec;
}
.kpi-cell {
padding: 14px 18px;
border-right: 1px solid #d6eaec;
}
.kpi-cell:last-child {
border-right: none;
}
.kpi-lbl {
font-size: 10px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 4px;
}
.kpi-val {
font-size: 22px;
font-weight: 700;
color: #1a2e35;
}
.rpt-section {
padding: 16px 24px;
}
.rpt-section + .rpt-section {
border-top: 1px solid #d6eaec;
}
.rpt-section-title {
font-size: 11px;
font-weight: 600;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .08em;
margin-bottom: 12px;
}
.rpt-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.rpt-table thead tr {
background: linear-gradient(135deg, #1a3a4a, #1e5468);
}
.rpt-table th {
text-align: left;
font-size: 10px;
font-weight: 700;
color: rgba(255,255,255,.85);
text-transform: uppercase;
letter-spacing: .06em;
padding: 9px 12px;
white-space: nowrap;
}
.rpt-table td {
padding: 9px 12px;
border-bottom: 1px solid #d6eaec;
vertical-align: middle;
}
.rpt-table tr:last-child td {
border-bottom: none;
}
.rpt-table .num {
text-align: right;
}
.rpt-table .total-row td {
font-weight: 700;
background: #f0f6f7;
}
.pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 9px;
border-radius: 50px;
font-size: 10px;
font-weight: 600;
}
.mini-bar-track {
flex: 1;
height: 5px;
border-radius: 3px;
background: #d6eaec;
overflow: hidden;
}
.mini-bar-fill {
height: 100%;
border-radius: 3px;
}
.fill-teal {
background: #1D9E75;
}
.fill-blue {
background: #378ADD;
}
.fill-amber {
background: #EF9F27;
}
.fill-coral {
background: #D85A30;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-top: 1px solid #d6eaec;
}
.col-left {
padding: 16px 24px;
border-right: 1px solid #d6eaec;
}
.col-right {
padding: 16px 24px;
}
.sig-block {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.sig-item {
border-top: 1px solid #d6eaec;
padding-top: 8px;
}
.sig-role {
font-size: 10px;
color: #6b8890;
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 16px;
}
.sig-name {
font-size: 11px;
font-weight: 600;
color: #1a2e35;
}
.rpt-footer {
padding: 10px 24px;
border-top: 1px solid #d6eaec;
display: flex;
align-items: center;
justify-content: space-between;
background: #f0f6f7;
}
.rpt-footer-lbl {
font-size: 10px;
color: #6b8890;
}
.loading-state {
text-align: center;
padding: 80px 20px;
color: #6b8890;
}
</style>
@await Html.PartialAsync("PagesView/Inventory/_InventoryReportHelper")
<script>
(function () {
"use strict";
const fromEl = document.getElementById("inv-rpt-from");
const toEl = document.getElementById("inv-rpt-to");
const genBtn = document.getElementById("inv-rpt-generate");
const csvBtn = document.getElementById("inv-rpt-csv");
const xlsxBtn = document.getElementById("inv-rpt-excel");
const container = document.getElementById("inv-rpt-container");
if (!fromEl || !toEl || !container) {
console.error("Inventory report subtab init failed — missing elements.");
return;
}
// default: first day of current month -> today
const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1);
toEl.value = today.toISOString().slice(0, 10);
fromEl.value = first.toISOString().slice(0, 10);
let lastData = null; // cache for export
function buildParams() {
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
}
async function fetchAndRender() {
container.innerHTML = `<div class="inv-tab-loading">
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
try {
const res = await fetch(`/InventoryReports/GetRISReport?${buildParams()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
lastData = json.data ?? json;
renderReport(lastData);
} catch (err) {
console.error("Inventory report fetch error:", err);
container.innerHTML = `
<div class="inv-placeholder">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<h3>Failed to load report</h3>
<p>Please try again.</p>
</div>`;
}
}
csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender);
// ── CSV export (client-side, no backend needed) ──
function exportCsv() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byDisc = lastData.byDiscipline ?? [];
const topRecv = lastData.topRecipients ?? [];
const summary = lastData.summary ?? {};
const headers = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"];
const csvLines = [
`Return Issuance Slip Report`,
`Company,${csvCell(lastData.companyName)}`,
`Report No,${csvCell(lastData.reportNo)}`,
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
``,
headers.map(csvCell).join(","),
...rows.map(r => [
r.risNo, r.createdDate, r.itemName, r.itemNo, r.disciplineName,
r.issuedTo, r.qtyIssued, r.totalReturned, r.netIssued, r.statusLabel
].map(csvCell).join(",")),
["Total","","","","","", summary.totalQtyIssued ?? 0, summary.totalQtyReturned ?? 0, summary.totalNetIssued ?? 0, ""].map(csvCell).join(","),
``,
`Issuance by Discipline`,
["Discipline","Slips"].map(csvCell).join(","),
...byDisc.map(d => [d.disciplineName, d.count].map(csvCell).join(",")),
``,
`Top Recipients`,
["Name","Slips","Qty Out"].map(csvCell).join(","),
...topRecv.map(t => [t.issuedTo, t.slipCount, t.totalQty].map(csvCell).join(","))
];
downloadBlob(
"\uFEFF" + csvLines.join("\r\n"),
`RIS_Report_${fromEl.value}_to_${toEl.value}.csv`,
"text/csv;charset=utf-8;"
);
}
function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename;
document.body.appendChild(a); a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function renderReport(data) {
const summary = data.summary ?? {};
const rows = data.rows ?? [];
const byDisc = data.byDiscipline ?? [];
const topRecv = data.topRecipients ?? [];
const maxDiscCount = Math.max(1, ...byDisc.map(d => d.count));
const fillColors = ["fill-teal", "fill-blue", "fill-amber", "fill-coral"];
container.innerHTML = `
<div class="rpt-page" id="ris-rpt-page">
<div class="rpt-header">
<div class="rpt-header-top">
<div>
<div class="rpt-company">${_esc(data.companyName ?? "")}</div>
<div class="rpt-title">Return Issuance Slip Report</div>
<div class="rpt-subtitle">
Period: ${_fmtDate(data.dateFrom)} ${_fmtDate(data.dateTo)} · All departments · All disciplines
</div>
</div>
<div class="rpt-logo"><i class="fas fa-file-export"></i></div>
</div>
<div class="rpt-meta">
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Prepared by</span>
<span class="rpt-meta-val">${_esc(data.preparedBy ?? "Finance Department")}</span>
</div>
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Print date</span>
<span class="rpt-meta-val">${_fmtDate(new Date().toISOString())}</span>
</div>
<div class="rpt-meta-item">
<span class="rpt-meta-lbl">Report no.</span>
<span class="rpt-meta-val">${_esc(data.reportNo ?? "")}</span>
</div>
</div>
</div>
<div class="kpi-strip">
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-file-export"></i> Total RIS issued</div>
<div class="kpi-val">${summary.totalRIS ?? 0}</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-check-circle"></i> Approved</div>
<div class="kpi-val">${summary.totalApproved ?? 0}</div>
<div class="kpi-sub">${summary.approvalRatePct ?? 0}% approval rate</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-clock"></i> Pending / Cancelled</div>
<div class="kpi-val">${summary.totalPending ?? 0} / ${summary.totalCancelled ?? 0}</div>
</div>
</div>
<div class="rpt-section">
<div class="rpt-section-title"><i class="fas fa-list"></i> Issuance detail</div>
<table class="rpt-table">
<thead>
<tr>
<th>RIS No.</th><th>Date</th><th>Item</th><th>Item No.</th>
<th>Discipline</th><th>Issued To</th>
<th class="num">Qty Issued</th><th class="num">Qty Returned</th>
<th class="num">Net Out</th><th>Status</th>
</tr>
</thead>
<tbody>
${rows.map(r => `
<tr>
<td style="font-weight:600;color:var(--teal-dark,#0d5c63)">${_esc(r.risNo)}</td>
<td>${_fmtDate(r.createdDate)}</td>
<td>${_esc(r.itemName)}</td>
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
<td>${_esc(r.disciplineName)}</td>
<td>${_esc(r.issuedTo)}</td>
<td class="num">${r.qtyIssued}</td>
<td class="num">${r.totalReturned}</td>
<td class="num" style="font-weight:600">${r.netIssued}</td>
<td>${_statusPill(r.status, r.statusLabel)}</td>
</tr>`).join("")}
<tr class="total-row">
<td colspan="6">Total</td>
<td class="num">${summary.totalQtyIssued ?? 0}</td>
<td class="num">${summary.totalQtyReturned ?? 0}</td>
<td class="num">${summary.totalNetIssued ?? 0}</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="two-col">
<div class="col-left">
<div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Issuance by discipline</div>
<div style="display:flex;flex-direction:column;gap:8px">
${byDisc.map((d, i) => `
<div>
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
<span>${_esc(d.disciplineName)}</span>
<span style="font-weight:600">${d.count} slips</span>
</div>
<div class="mini-bar-track">
<div class="mini-bar-fill ${fillColors[i % fillColors.length]}"
style="width:${Math.round((d.count / maxDiscCount) * 100)}%"></div>
</div>
</div>`).join("")}
</div>
</div>
<div class="col-right">
<div class="rpt-section-title"><i class="fas fa-users"></i> Top recipients</div>
<table class="rpt-table">
<thead><tr><th>Name</th><th class="num">Slips</th><th class="num">Qty Out</th></tr></thead>
<tbody>
${topRecv.map(t => `
<tr>
<td>${_esc(t.issuedTo)}</td>
<td class="num">${t.slipCount}</td>
<td class="num">${t.totalQty}</td>
</tr>`).join("")}
</tbody>
</table>
</div>
</div>
<div class="rpt-section">
<div class="sig-block">
<div class="sig-item"><div class="sig-role">Prepared by</div><div class="sig-name">Finance Officer</div></div>
<div class="sig-item"><div class="sig-role">Reviewed by</div><div class="sig-name">Finance Manager</div></div>
<div class="sig-item"><div class="sig-role">Approved by</div><div class="sig-name">Finance Director</div></div>
</div>
</div>
<div class="rpt-footer">
<span class="rpt-footer-lbl">${_esc(data.companyName ?? "")} — Confidential — For internal use only</span>
</div>
</div>`;
}
function _statusPill(status, label) {
const cls = status === 1 ? "pill-approved" : status === 2 ? "pill-cancelled" : "pill-draft";
const icon = status === 1 ? "fa-check" : status === 2 ? "fa-ban" : "fa-clock";
return `<span class="pill ${cls}"><i class="fas ${icon}"></i> ${_esc(label)}</span>`;
}
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
function exportExcel() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byDisc = lastData.byDiscipline ?? [];
const topRecv = lastData.topRecipients ?? [];
const summary = lastData.summary ?? {};
const headerCells = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"]
.map(h => `<th>${_esc(h)}</th>`).join("");
const bodyRows = rows.map(r => `
<tr>
<td>${_esc(r.risNo)}</td>
<td>${_esc(_fmtDate(r.createdDate))}</td>
<td>${_esc(r.itemName)}</td>
<td>${_esc(r.itemNo)}</td>
<td>${_esc(r.disciplineName)}</td>
<td>${_esc(r.issuedTo)}</td>
<td>${r.qtyIssued}</td>
<td>${r.totalReturned}</td>
<td>${r.netIssued}</td>
<td>${_esc(r.statusLabel)}</td>
</tr>`).join("");
const discRows = byDisc.map(d => `
<tr><td>${_esc(d.disciplineName)}</td><td>${d.count}</td></tr>`).join("");
const recvRows = topRecv.map(t => `
<tr><td>${_esc(t.issuedTo)}</td><td>${t.slipCount}</td><td>${t.totalQty}</td></tr>`).join("");
const html = `
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40">
<head><meta charset="utf-8"></head>
<body>
<table border="1">
<tr><td colspan="10"><b>Return Issuance Slip Report</b></td></tr>
<tr><td>Company</td><td colspan="9">${_esc(lastData.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="9">${_esc(lastData.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="9">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr></tr>
<tr>${headerCells}</tr>
${bodyRows}
<tr><td colspan="6"><b>Total</b></td><td>${summary.totalQtyIssued ?? 0}</td><td>${summary.totalQtyReturned ?? 0}</td><td>${summary.totalNetIssued ?? 0}</td><td></td></tr>
</table>
<br/>
<table border="1">
<tr><td colspan="2"><b>Issuance by Discipline</b></td></tr>
<tr><th>Discipline</th><th>Slips</th></tr>
${discRows}
</table>
<br/>
<table border="1">
<tr><td colspan="3"><b>Top Recipients</b></td></tr>
<tr><th>Name</th><th>Slips</th><th>Qty Out</th></tr>
${recvRows}
</table>
</body></html>`;
downloadBlob(html, `RIS_Report_${fromEl.value}_to_${toEl.value}.xls`,
"application/vnd.ms-excel");
}
xlsxBtn.addEventListener("click", exportExcel);
// ── Initial load ──
fetchAndRender();
})();
</script>

View File

@ -7,7 +7,7 @@
<link href="~/lib/font-awesome/css/all.min.css" rel="stylesheet" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link href="~/css/spinner.css" rel="stylesheet" />
<link href="~/css/toast-notifications.css" rel="stylesheet" />
<link href="~/css/toast-notificationsV2.css" rel="stylesheet" />
<link href="~/css/loginpageinteractive.css" rel="stylesheet" />
</head>
<body>

View File

@ -19,18 +19,20 @@
<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
<i class="fas fa-file-export"></i>RIS
</button>
<button class="inv-tab-btn" data-tab-id="3" role="tab">
<i class="fas fa-id-badge"></i> Return Issuance Slip (Report)
<i class="fas fa-store"></i> MRS
</button>
<button class="inv-tab-btn" data-tab-id="4" role="tab">
<i class="fas fa-store"></i> Material Return Slip (Report)
<i class="fa-solid fa-list-check"></i> Inventory Reports
</button>
<button class="inv-tab-btn" data-tab-id="5" role="tab">
<i class="fas fa-check-circle"></i> Transaction History
<i class="fa-solid fa-file-export"></i> RIS Reports
</button>
<button class="inv-tab-btn" data-tab-id="6" role="tab">
<i class="fa-solid fa-arrow-rotate-left"></i> MRS Reports
</button>
</div>
<div id="inv-tab-content">
@ -76,6 +78,7 @@
const html = await res.text();
cache[tabId] = html;
injectHtml(tabContent, html);
} catch (err) {
console.error("Tab load error:", err);
tabContent.innerHTML = `

View File

@ -0,0 +1,84 @@
@model FastReport.Web.WebReport
@{
Layout = null;
var from = (DateTime)ViewBag.DateFrom;
var to = (DateTime)ViewBag.DateTo;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RIS Report — @from.ToString("MMM d") to @to.ToString("MMM d, yyyy")</title>
@* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" /> *@
<style>
body {
margin: 0;
font-family: 'DM Sans', system-ui, sans-serif;
background: #f0f6f7;
}
.rep-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: linear-gradient(135deg, #0d5c63, #0e7c86);
color: #fff;
}
.rep-bar h1 {
font-size: 1rem;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.rep-bar .period {
font-size: .8rem;
color: rgba(255,255,255,.7);
margin-top: 2px;
}
.rep-back {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border-radius: 8px;
background: rgba(255,255,255,.15);
color: #fff;
text-decoration: none;
font-size: .85rem;
font-weight: 600;
}
.rep-back:hover {
background: rgba(255,255,255,.28);
}
.rep-body {
padding: 16px;
}
</style>
</head>
<body>
<div class="rep-bar">
<div>
<h1><i class="fas fa-file-lines"></i> Return Issuance Slip Report</h1>
<div class="period">
Period: @from.ToString("MMMM d, yyyy") @to.ToString("MMMM d, yyyy")
</div>
</div>
<a href="javascript:window.close()" class="rep-back">
<i class="fas fa-xmark"></i> Close
</a>
</div>
<div class="rep-body">
@await Model.Render()
</div>
</body>
</html>

View File

@ -0,0 +1,93 @@
<!-- ReportDesigner.cshtml -->
<link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet">
<script src="https://unpkg.com/grapesjs"></script>
<div style="display:flex;gap:8px;padding:10px">
<button id="btn-save">Save Template</button>
<button id="btn-preview">Preview with Sample Data</button>
<select id="report-type">
<option value="ris">RIS Report</option>
<option value="mrs">MRS Report</option>
<option value="inventory">Inventory Report</option>
</select>
</div>
<div id="gjs-editor"></div>
<script>
const editor = grapesjs.init({
container: '#gjs-editor',
height: '100vh',
fromElement: false,
storageManager: false, // we handle save/load ourselves
// ── Block manager: drag-drop components your team can use ──
blockManager: {
blocks: [
{
id: 'data-field',
label: 'Data Field',
content: '<span class="df-token">{{FieldName}}</span>',
category: 'Report Fields'
},
{
id: 'table-block',
label: 'Data Table',
content: `<table class="rpt-table">
<thead><tr><th>Column 1</th><th>Column 2</th></tr></thead>
<tbody><tr><td>{{Field1}}</td><td>{{Field2}}</td></tr></tbody>
</table>`,
category: 'Report Fields'
},
{
id: 'kpi-card',
label: 'KPI Card',
content: `<div class="kpi-cell">
<div class="kpi-lbl">Label</div>
<div class="kpi-val">{{KpiValue}}</div>
</div>`,
category: 'Report Fields'
}
]
}
});
// ── Load existing template when report type changes ──
document.getElementById('report-type').addEventListener('change', async (e) => {
const res = await fetch(`/ReportDesigner/GetTemplate?type=${e.target.value}`);
const data = await res.json();
editor.setComponents(data.html || '');
editor.setStyle(data.css || '');
});
// ── Save template ──
document.getElementById('btn-save').addEventListener('click', async () => {
const html = editor.getHtml();
const css = editor.getCss();
const type = document.getElementById('report-type').value;
await fetch('/ReportDesigner/SaveTemplate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reportType: type, html, css })
});
alert('Template saved.');
});
// ── Preview with real sample data ──
document.getElementById('btn-preview').addEventListener('click', async () => {
const html = editor.getHtml();
const css = editor.getCss();
const type = document.getElementById('report-type').value;
const res = await fetch('/ReportDesigner/PreviewPdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reportType: type, html, css })
});
const blob = await res.blob();
window.open(URL.createObjectURL(blob), '_blank');
});
</script>

View File

@ -0,0 +1,120 @@
<div class="inv-filters no-print">
<div class="rpt-date-group">
<label class="rpt-date-lbl"><i class="fas fa-calendar-check"></i> From</label>
<input type="date" id="inv-rpt-from" class="rpt-date-input">
</div>
<div class="rpt-date-group">
<label class="rpt-date-lbl"><i class="fas fa-calendar-check"></i> To</label>
<input type="date" id="inv-rpt-to" class="rpt-date-input">
</div>
<button id="inv-rpt-generate" class="rpt-btn rpt-btn-primary">
<i class="fas fa-sync"></i> Generate
</button>
<button id="inv-rpt-print" class="rpt-btn rpt-btn-outline" onclick="window.print()">
<i class="fas fa-print"></i> Print / PDF
</button>
<button id="inv-rpt-csv" class="rpt-btn rpt-btn-outline">
<i class="fas fa-file-csv"></i> CSV
</button>
<button id="inv-rpt-excel" class="rpt-btn rpt-btn-outline">
<i class="fas fa-file-excel"></i> Excel
</button>
</div>
<div id="inv-rpt-container">
<div class="inv-tab-loading">
<div class="inv-spinner"></div>
<span>Loading report…</span>
</div>
</div>
<style>
/* ── Tablet: tabs in a 3-per-row grid for tidier wrapping ── */
@@media (max-width: 1024px) {
.inv-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.inv-tab-btn {
min-width: 0; /* let grid control the width */
}
}
/* ── Mobile: 2 per row, smaller text/padding ── */
@@media (max-width: 600px) {
.inv-tabs {
grid-template-columns: repeat(2, 1fr);
}
.inv-tab-btn {
padding: 10px 8px;
font-size: .8rem;
gap: 5px;
}
/* optional: hide icons on very small screens to save space */
.inv-tab-btn i {
display: none;
}
}
/* ── Very small phones: stack one per row ── */
@@media (max-width: 380px) {
.inv-tabs {
grid-template-columns: 1fr;
}
}
@@media (max-width: 768px) {
.rpt-section .rpt-table, .two-col .rpt-table
{
display: block;
overflow-x: auto;
white-space: nowrap;
}
/* stack the two-column section vertically on mobile */
.two-col {
grid-template-columns: 1fr;
}
.col-left {
border-right: none;
border-bottom: 1px solid #d6eaec;
}
/* KPI strip: 2 per row instead of 4 */
.kpi-strip {
grid-template-columns: repeat(2, 1fr);
}
.kpi-cell:nth-child(2) {
border-right: none;
}
}
</style>
<script>
// ── shared helpers ──
function csvCell(v) {
const s = String(v ?? "");
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename;
document.body.appendChild(a); a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function _esc(s) {
return String(s ?? "").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}
function _fmtDate(raw) {
if (!raw) return "—";
const d = new Date(raw);
return isNaN(d) ? raw : d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
</script>

View File

@ -96,6 +96,7 @@
.inv-tabs {
display: flex;
gap: 6px;
flex-wrap: wrap;
background: #fff;
border-radius: var(--radius-lg);
padding: 6px;
@ -104,7 +105,8 @@
}
.inv-tab-btn {
flex: 1;
flex: 1 1 auto; /* grow, but allow shrinking and wrapping */
min-width: 140px; /* keep each tab readable before it wraps */
display: flex;
align-items: center;
justify-content: center;

View File

@ -1,4 +1,4 @@
<script src="~/js/cachebuster.js"></script>
@* <script src="~/js/cachebuster.js"></script> *@
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery/dist/jquery371.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

View File

@ -1,3 +1,5 @@
@using CPRNIMS.WebApps
@using CPRNIMS.WebApps.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, FastReport.Web
@using FastReport.Web