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="AutoMapper" Version="16.1.1" />
<PackageReference Include="CaptchaGen.NetCore" Version="1.1.2" /> <PackageReference Include="CaptchaGen.NetCore" Version="1.1.2" />
<PackageReference Include="Dapper" Version="2.1.66" /> <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="Google.Apis.Drive.v3" Version="1.67.0.3373" />
<PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.4" /> <PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.2.0" /> <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 public interface IMRS
{ {
Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter); Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter, CancellationToken ct,int? departmentId = null, string? userName = "");
Task<MRS?> GetByIdAsync(long mrsId); Task<MRS?> GetByIdAsync(long mrsId, CancellationToken ct);
Task<MRS> CreateAsync(CreateMRSRequest dto, string createdBy); Task<MRS> CreateAsync(CreateMRSRequest dto, string createdBy, CancellationToken ct);
Task ApproveAsync(long mrsId, string approvedBy); 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 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<RISResponse?> GetByIdAsync(long risId, CancellationToken ct);
Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct); Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct);
Task ApproveAsync(ApproveRISRequest request, string approvedBy, 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(ClaimTypes.NameIdentifier, user.Id),
new Claim("FullName", user.FullName ?? ""), new Claim("FullName", user.FullName ?? ""),
new Claim("Company", user.Company ?? ""), new Claim("Company", user.Company ?? ""),
new Claim("DepartmentId", Convert.ToString(user.DepartmentId)),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 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.Database;
using CPRNIMS.Infrastructure.Dto.Inventory; using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request; using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Entities.Inventory; using CPRNIMS.Infrastructure.Entities.Inventory;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.Services.Inventory namespace CPRNIMS.Domain.Services.Inventory
{ {
public class MRS : IMRS public class MRS : IMRS
{ {
private readonly NonInventoryDbContext _db; private readonly NonInventoryDbContext _db;
public MRS(NonInventoryDbContext db) => _db = db; private readonly ITransactionFacade _transactionFacade;
public MRS(NonInventoryDbContext db, ITransactionFacade transactionFacade)
public async Task ApproveAsync(long mrsId, string approvedBy) {
_db = db;
_transactionFacade = transactionFacade;
}
public async Task ApproveAsync(long mrsId, string approvedBy, CancellationToken ct)
{ {
var rms = await _db.MRS.FindAsync(mrsId) var rms = await _db.MRS.FindAsync(mrsId)
?? throw new InvalidOperationException("MRS not found."); ?? throw new InvalidOperationException("MRS not found.");
@ -30,73 +29,108 @@ namespace CPRNIMS.Domain.Services.Inventory
rms.ApprovedBy = approvedBy; rms.ApprovedBy = approvedBy;
rms.ApprovedDate = DateTime.Now; 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)
{ {
var ris = await _db.RIS await _transactionFacade.ExecuteAsync(async () =>
.Include(r => r.Inventory)
.FirstOrDefaultAsync(r => r.RISId == dto.RISId)
?? throw new InvalidOperationException("Referenced RIS not found.");
if (dto.QtyReturned > ris.QtyIssued)
throw new InvalidOperationException(
$"Cannot return more than issued. Issued: {ris.QtyIssued}.");
var mrsNo = await GenerateMRSNoAsync();
var mrs = new Infrastructure.Entities.Inventory.MRS
{ {
MRSNo = mrsNo, var mrs = await _db.MRS
RISId = dto.RISId, .Include(m => m.Inventory)
InventoryId = ris.InventoryId, .FirstOrDefaultAsync(m => m.MRSId == request.MRSId)
ReturnedBy = dto.ReturnedBy, ?? throw new InvalidOperationException("MRS not found.");
QtyReturned = dto.QtyReturned,
Condition = dto.Condition,
Remarks = dto.Remarks,
Status = 0,
CreatedBy = createdBy,
CreatedDate = DateTime.Now
};
_db.MRS.Add(mrs);
var inventory = ris.Inventory; if (mrs.Status == 2)
inventory.QtyOut = Math.Max(0m, inventory.QtyOut - dto.QtyReturned); throw new InvalidOperationException("MRS is already cancelled.");
inventory.QtyOnHand = inventory.QtyIn - inventory.QtyOut; // Reverse the return: deduct qty back out
mrs.Inventory.QtyOut = Math.Max(0m, mrs.Inventory.QtyOut) + mrs.QtyReturned;
var trans = await _db.InventTrans mrs.Inventory.QtyOnHand = mrs.Inventory.QtyIn - mrs.Inventory.QtyOut;
.FirstOrDefaultAsync(t => t.InventoryId == ris.InventoryId && t.IsActive == true)!; mrs.Reason = request.Reason;
mrs.Status = 2;
_db.InventTransDetails.Add(new InventTransDetail mrs.CanceledDate = DateTime.Now;
{ mrs.CanceledBy = canceledBy;
InventTransId = trans.InventTransId, }, ct);
TransTypeId = 6,
QtyIn = dto.QtyReturned,
CreatedDate = DateTime.Now,
Remarks = $"MRS: {mrsNo} — return from MRS: {ris.RISNo}",
IsActive = true
});
await _db.SaveChangesAsync();
return mrs;
} }
public async Task<Infrastructure.Entities.Inventory.MRS?> GetByIdAsync(long mrsId) 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, ct)
?? throw new InvalidOperationException("Referenced RIS not found.");
if (dto.QtyReturned > ris.QtyIssued)
throw new InvalidOperationException(
$"Cannot return more than issued. Issued: {ris.QtyIssued}.");
var mrsNo = await GenerateMRSNoAsync();
var mrs = new Infrastructure.Entities.Inventory.MRS
{
MRSNo = mrsNo,
RISId = dto.RISId,
InventoryId = ris.InventoryId,
ReturnedBy = dto.ReturnedBy,
QtyReturned = dto.QtyReturned,
Condition = dto.Condition,
Remarks = dto.Remarks,
Status = 0,
CreatedBy = createdBy,
CreatedDate = DateTime.Now
};
_db.MRS.Add(mrs);
var inventory = ris.Inventory;
inventory.QtyOut = Math.Max(0m, inventory.QtyOut - dto.QtyReturned);
inventory.QtyOnHand = inventory.QtyIn - inventory.QtyOut;
var trans = await _db.InventTrans
.FirstOrDefaultAsync(t => t.InventoryId == ris.InventoryId && t.IsActive == true, ct)!;
_db.InventTransDetails.Add(new InventTransDetail
{
InventTransId = trans.InventTransId,
TransTypeId = 6,
QtyIn = dto.QtyReturned,
CreatedDate = DateTime.Now,
Remarks = $"MRS: {mrsNo} — return from MRS: {ris.RISNo}",
IsActive = true
});
return mrs;
}, ct);
}
public async Task<Infrastructure.Entities.Inventory.MRS?> GetByIdAsync(long mrsId, CancellationToken ct)
=> await _db.MRS => await _db.MRS
.Include(r => r.Inventory) .Include(r => r.Inventory)
.Include(r => r.RIS) .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 var q = _db.MRS
.Include(m => m.RIS) .Include(m => m.RIS)
.Include(m => m.Inventory) .Include(m => m.Inventory)
.AsQueryable(); .AsQueryable();
if (departmentId.HasValue && !seeAllDepartments)
{
q = q.Where(itd =>
itd.Inventory.User.DepartmentId == departmentId.Value);
}
if (!string.IsNullOrWhiteSpace(filter.SearchMRSNo)) if (!string.IsNullOrWhiteSpace(filter.SearchMRSNo))
q = q.Where(m => m.MRSNo.Contains(filter.SearchMRSNo)); q = q.Where(m => m.MRSNo.Contains(filter.SearchMRSNo));
@ -112,13 +146,13 @@ namespace CPRNIMS.Domain.Services.Inventory
if (filter.DateTo.HasValue) if (filter.DateTo.HasValue)
q = q.Where(m => m.CreatedDate <= filter.DateTo.Value.AddDays(1)); 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 var data = await q
.OrderByDescending(m => m.CreatedDate) .OrderByDescending(m => m.CreatedDate)
.Skip((filter.Page - 1) * filter.PageSize) .Skip((filter.PageNumber - 1) * filter.PageSize)
.Take(filter.PageSize) .Take(filter.PageSize)
.Select(m => new MRSResponse .Select(m => new MRSPagedDto
{ {
MRSId = m.MRSId, MRSId = m.MRSId,
MRSNo = m.MRSNo, MRSNo = m.MRSNo,
@ -139,7 +173,7 @@ namespace CPRNIMS.Domain.Services.Inventory
ApprovedBy = m.ApprovedBy, ApprovedBy = m.ApprovedBy,
ApprovedDate = m.ApprovedDate ApprovedDate = m.ApprovedDate
}) })
.ToListAsync(); .ToListAsync(ct);
return new MRSPagedResult { Data = data, RecordsTotal = total }; return new MRSPagedResult { Data = data, RecordsTotal = total };
} }

View File

@ -1,5 +1,5 @@
using CPRNIMS.Domain.Contracts.Inventory; using CPRNIMS.Domain.Contracts.Common;
using CPRNIMS.Domain.UIServices.Inventory; using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Database; using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory; using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request; using CPRNIMS.Infrastructure.Dto.Inventory.Request;
@ -17,75 +17,63 @@ namespace CPRNIMS.Domain.Services.Inventory
public class RIS : IRIS public class RIS : IRIS
{ {
private readonly NonInventoryDbContext _db; private readonly NonInventoryDbContext _db;
private readonly ITransactionFacade _transactionFacade;
public RIS(NonInventoryDbContext db) => _db = db; public RIS(NonInventoryDbContext db, ITransactionFacade transactionFacade)
{
_db = db;
_transactionFacade = transactionFacade;
}
public async Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct) public async Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct)
{ {
var strategy = _db.Database.CreateExecutionStrategy(); return await _transactionFacade.ExecuteAsync(async () =>
return await strategy.ExecuteAsync(async () =>
{ {
await using var tx = await _db.Database.BeginTransactionAsync(ct); var inventory = await _db.Inventories
try
{
var inventory = await _db.Inventories
.FirstOrDefaultAsync(i => i.InventoryId == dto.InventoryId, ct) .FirstOrDefaultAsync(i => i.InventoryId == dto.InventoryId, ct)
?? throw new InvalidOperationException("Inventory record not found."); ?? throw new InvalidOperationException("Inventory record not found.");
if (inventory.QtyOnHand < dto.QtyIssued) if (inventory.QtyOnHand < dto.QtyIssued)
throw new InvalidOperationException( throw new InvalidOperationException(
$"Insufficient stock. On hand: {inventory.QtyOnHand}, requested: {dto.QtyIssued}."); $"Insufficient stock. On hand: {inventory.QtyOnHand}, requested: {dto.QtyIssued}.");
var risNo = await GenerateRISNoAsync(ct); var risNo = await GenerateRISNoAsync(ct);
var ris = new Infrastructure.Entities.Inventory.RIS var ris = new Infrastructure.Entities.Inventory.RIS
{
RISNo = risNo,
InventoryId = dto.InventoryId,
PRDetailId = dto.PRDetailId,
IssuedTo = dto.IssuedTo,
DisciplineId = dto.DisciplineId,
QtyIssued = dto.QtyIssued,
Remarks = dto.Remarks,
Status = 0,
CreatedBy = createdBy,
CreatedDate = DateTime.Now
};
_db.RIS.Add(ris);
await _db.SaveChangesAsync(ct);
var trans = await _db.InventTrans
.Where(t => t.InventoryId == dto.InventoryId && t.IsActive == true)
.FirstOrDefaultAsync(ct)
?? throw new InvalidOperationException(
"No active InventTrans found for this inventory record.");
_db.InventTransDetails.Add(new InventTransDetail
{
InventTransId = trans.InventTransId,
TransTypeId = 5,
PRDetailId = dto.PRDetailId,
QtyOut = dto.QtyIssued,
CreatedDate = DateTime.Now,
Remarks = $"RIS: {risNo}",
IsActive = true
});
inventory.QtyOut = Math.Max(0m, inventory.QtyOut) + dto.QtyIssued;
inventory.QtyOnHand = Math.Max(0m, inventory.QtyIn) - (decimal)inventory.QtyOut;
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return ris;
}
catch
{ {
await tx.RollbackAsync(ct); RISNo = risNo,
throw; InventoryId = dto.InventoryId,
} PRDetailId = dto.PRDetailId,
}); IssuedTo = dto.IssuedTo,
DisciplineId = dto.DisciplineId,
QtyIssued = dto.QtyIssued,
Remarks = dto.Remarks,
Status = 0,
CreatedBy = createdBy,
CreatedDate = DateTime.Now
};
_db.RIS.Add(ris);
var trans = await _db.InventTrans
.Where(t => t.InventoryId == dto.InventoryId && t.IsActive == true)
.FirstOrDefaultAsync(ct)
?? throw new InvalidOperationException(
"No active InventTrans found for this inventory record.");
_db.InventTransDetails.Add(new InventTransDetail
{
InventTransId = trans.InventTransId,
TransTypeId = 5,
PRDetailId = dto.PRDetailId,
QtyOut = dto.QtyIssued,
CreatedDate = DateTime.Now,
Remarks = $"RIS: {risNo}",
IsActive = true
});
inventory.QtyOut = Math.Max(0m, inventory.QtyOut) + dto.QtyIssued;
inventory.QtyOnHand = Math.Max(0m, inventory.QtyIn) - (decimal)inventory.QtyOut;
return ris;
}, ct);
} }
public async Task ApproveAsync(ApproveRISRequest request, string approvedBy, CancellationToken ct) public async Task ApproveAsync(ApproveRISRequest request, string approvedBy, CancellationToken ct)
@ -103,16 +91,11 @@ namespace CPRNIMS.Domain.Services.Inventory
await _db.SaveChangesAsync(ct); 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 _transactionFacade.ExecuteAsync(async () =>
await strategy.ExecuteAsync(async () =>
{ {
await using var tx = await _db.Database.BeginTransactionAsync(ct); var ris = await _db.RIS
try
{
var ris = await _db.RIS
.Include(r => r.Inventory) .Include(r => r.Inventory)
.FirstOrDefaultAsync(r => r.RISId == request.RISId, ct) .FirstOrDefaultAsync(r => r.RISId == request.RISId, ct)
?? throw new InvalidOperationException("RIS not found."); ?? throw new InvalidOperationException("RIS not found.");
@ -120,6 +103,23 @@ namespace CPRNIMS.Domain.Services.Inventory
if (ris.Status == 2) if (ris.Status == 2)
throw new InvalidOperationException("RIS is already cancelled."); 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.QtyOut = Math.Max(0m, ris.Inventory.QtyOut - ris.QtyIssued);
ris.Inventory.QtyOnHand = ris.Inventory.QtyIn - ris.Inventory.QtyOut; ris.Inventory.QtyOnHand = ris.Inventory.QtyIn - ris.Inventory.QtyOut;
ris.Reason = request.Reason; ris.Reason = request.Reason;
@ -141,31 +141,7 @@ namespace CPRNIMS.Domain.Services.Inventory
Remarks = request.Reason, Remarks = request.Reason,
IsActive = true IsActive = true
}); });
}, ct);
//var inventory = await _db.Inventories
// .FirstOrDefaultAsync(i => i.InventoryId == ris.InventoryId, ct)
// ?? throw new InvalidOperationException("Inventory record not found.");
//if (inventory.QtyOnHand < ris.QtyIssued)
// throw new InvalidOperationException(
// $"Insufficient stock. On hand: {inventory.QtyOnHand}, requested: {ris.QtyIssued}.");
////restore the QtyOnHand using ris.QtyIssued
//inventory.QtyOnHand = Math.Max(0m, inventory.QtyOnHand) + ris.QtyIssued;
////reduce the QtyOut using ris.QtyIssued as we cancel the return isuance slip
//inventory.QtyOut = Math.Max(0m, inventory.QtyOut) - ris.QtyIssued;
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return ris;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
});
} }
private async Task<string> GenerateRISNoAsync(CancellationToken ct) 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 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 var q = _db.RIS
.Include(r => r.Discipline) .Include(r => r.Discipline)
.Include(r => r.Inventory) .Include(r => r.Inventory)
.Include(r => r.MaterialReturns) .Include(r => r.MaterialReturns)
.AsQueryable(); .AsQueryable();
if (departmentId.HasValue && !seeAllDepartments)
{
q = q.Where(itd =>
itd.Inventory.User.DepartmentId == departmentId.Value);
}
// Status filter (default to Draft=0 if null) // Status filter (default to Draft=0 if null)
if (filter.Status.HasValue) if (filter.Status.HasValue)
q = q.Where(r => r.Status == filter.Status.Value); q = q.Where(r => r.Status == filter.Status.Value);
else //else
q = q.Where(r => r.Status == 0); // q = q.Where(r => r.Status == 0);
// RIS No // RIS No
if (!string.IsNullOrWhiteSpace(filter.SearchRISNo)) 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.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.ViewModel.Inventory; using CPRNIMS.Infrastructure.ViewModel.Inventory;
using System; using System;
@ -11,10 +12,9 @@ namespace CPRNIMS.Domain.UIContracts.Inventory
{ {
public interface IRIS public interface IRIS
{ {
Task<bool> ApproveRIS(ApproveRISRequest request, CancellationToken ct); Task<ApiResponse<object>> ApproveRIS(ApproveRISRequest request, CancellationToken ct);
Task<bool> CancelRIS(CancelRISRequest request, CancellationToken ct); Task<ApiResponse<object>> CancelRIS(CancelRISRequest request, CancellationToken ct);
Task<RISResponse> CreateRIS(CreateRISRequest request, CancellationToken ct); Task<ApiResponse<object>> CreateRIS(CreateRISRequest request, CancellationToken ct);
Task<RISPagedResponse> GetRISById(int risId, CancellationToken ct);
Task<RISPagedResponse> GetRISPaged(RISPagedRequest 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.Common;
using CPRNIMS.Infrastructure.Dto.Inventory.Request; using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.Inventory.Response; using CPRNIMS.Infrastructure.Dto.Inventory.Response;
using CPRNIMS.Infrastructure.Dto.PR.Response;
using CPRNIMS.Infrastructure.Helper; using CPRNIMS.Infrastructure.Helper;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System; using System;
@ -30,60 +31,51 @@ namespace CPRNIMS.Domain.UIServices.Inventory
#region Get #region Get
public async Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct) public async Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct)
{ {
try var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Token has been expired.");
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRIS"]
?? throw new InvalidOperationException("GetRIS endpoint is not configured.");
// ── Build query string — GET request, no body ──
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"pageNumber={request.PageNumber}");
qs.Append($"&pageSize={request.PageSize}");
if (!string.IsNullOrWhiteSpace(request.SearchRISNo))
qs.Append($"&searchRISNo={Uri.EscapeDataString(request.SearchRISNo)}");
if (!string.IsNullOrWhiteSpace(request.SearchItemName))
qs.Append($"&searchItemName={Uri.EscapeDataString(request.SearchItemName)}");
if (!string.IsNullOrWhiteSpace(request.SearchIssuedTo))
qs.Append($"&searchIssuedTo={Uri.EscapeDataString(request.SearchIssuedTo)}");
if (!string.IsNullOrWhiteSpace(request.Discipline))
qs.Append($"&discipline={Uri.EscapeDataString(request.Discipline)}");
if (request.Status.HasValue)
qs.Append($"&status={request.Status.Value}");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
// ── GET, no content body ──
var response = await httpClient.GetAsync(qs.ToString(), ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{ {
var token = await _tokenHelper.GetValidTokenAsync(); Console.WriteLine($"[GetRISPaged] Error {(int)response.StatusCode}: {json}");
if (string.IsNullOrEmpty(token)) return new RISPagedResponse { Data = [], RecordsTotal = 0 };
throw new InvalidOperationException("Token has been expired.");
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRIS"]
?? throw new InvalidOperationException("GetRIS endpoint is not configured.");
// ── Build query string — GET request, no body ──
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"pageNumber={request.PageNumber}");
qs.Append($"&pageSize={request.PageSize}");
if (!string.IsNullOrWhiteSpace(request.SearchRISNo))
qs.Append($"&searchRISNo={Uri.EscapeDataString(request.SearchRISNo)}");
if (!string.IsNullOrWhiteSpace(request.SearchItemName))
qs.Append($"&searchItemName={Uri.EscapeDataString(request.SearchItemName)}");
if (!string.IsNullOrWhiteSpace(request.SearchIssuedTo))
qs.Append($"&searchIssuedTo={Uri.EscapeDataString(request.SearchIssuedTo)}");
if (!string.IsNullOrWhiteSpace(request.Discipline))
qs.Append($"&discipline={Uri.EscapeDataString(request.Discipline)}");
if (request.Status.HasValue)
qs.Append($"&status={request.Status.Value}");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
// ── GET, no content body ──
var response = await httpClient.GetAsync(qs.ToString(), ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[GetRISPaged] Error {(int)response.StatusCode}: {json}");
return new RISPagedResponse { Data = [], RecordsTotal = 0 };
}
var result = JsonSerializer.Deserialize<RISPagedResponse>(json, _jsonOptions);
return result ?? new RISPagedResponse();
}
catch (Exception ex)
{
ex.ToString();
throw;
} }
var result = JsonSerializer.Deserialize<RISPagedResponse>(json, _jsonOptions);
return result ?? new RISPagedResponse();
} }
#endregion #endregion
#region Post Put #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(); var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -97,15 +89,16 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var response = await httpClient.PutAsync(endpoint, content, ct); var response = await httpClient.PutAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(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) if (!response.IsSuccessStatusCode)
{ result.success = false;
return false;
} return result;
return true;
} }
public async Task<bool> CancelRIS(CancelRISRequest request, CancellationToken ct) public async Task<ApiResponse<object>> CancelRIS(CancelRISRequest request, CancellationToken ct)
{ {
var token = await _tokenHelper.GetValidTokenAsync(); var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -115,19 +108,21 @@ namespace CPRNIMS.Domain.UIServices.Inventory
?? throw new InvalidOperationException("CancelRIS endpoint is not configured."); ?? throw new InvalidOperationException("CancelRIS endpoint is not configured.");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token); using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent( using var content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
JsonSerializer.Serialize(request),Encoding.UTF8,"application/json");
var response = await httpClient.PutAsync(endpoint, content, ct); var response = await httpClient.PutAsync(endpoint, content, ct);
var json = await response.Content.ReadAsStringAsync(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) if (!response.IsSuccessStatusCode)
{ result.success = false;
return 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(); var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -138,27 +133,19 @@ namespace CPRNIMS.Domain.UIServices.Inventory
throw new InvalidOperationException("CreateRIS endpoint is not configured."); throw new InvalidOperationException("CreateRIS endpoint is not configured.");
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token); using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
using var content = new StringContent( using var content = new StringContent(JsonSerializer.Serialize(request),Encoding.UTF8,"application/json");
JsonSerializer.Serialize(request),
Encoding.UTF8,
"application/json");
var response = await httpClient.PostAsync(endpoint, content,ct); var response = await httpClient.PostAsync(endpoint, content,ct);
if (!response.IsSuccessStatusCode)
{
return new RISResponse();
}
var json = await response.Content.ReadAsStringAsync(); var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ApiResponse<RISResponse>>(json, _jsonOptions); var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _jsonOptions)
return result?.data ?? new RISResponse(); ?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
}
public Task<RISPagedResponse> GetRISById(int risId, CancellationToken ct) if (!response.IsSuccessStatusCode)
{ result.success = false;
throw new NotImplementedException();
return result;
} }
private static readonly JsonSerializerOptions _jsonOptions = new() private static readonly JsonSerializerOptions _jsonOptions = new()

View File

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

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -20,6 +21,7 @@ namespace CPRNIMS.Infrastructure.Dto.Account
public string URLAttachment { get; set; } = string.Empty; public string URLAttachment { get; set; } = string.Empty;
public string? token { get; set; } public string? token { get; set; }
public string? company { get; set; } public string? company { get; set; }
public int? departmentId { get; set; }
public string? refreshToken { get; set; } public string? refreshToken { get; set; }
public DateTime expiresAt { get; set; } public DateTime expiresAt { get; set; }
public int expiresInSeconds { 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 FullName { get; init; } = default!;
public string Company { get; init; } = default!; public string Company { get; init; } = default!;
public IReadOnlyList<string> Roles { get; init; } = []; 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 class MRSFilterDto
{ {
public int Page { get; set; } = 1; public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 12; public int PageSize { get; set; } = 12;
public string? SearchMRSNo { get; set; } public string? SearchMRSNo { get; set; }
public long? RISId { 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 class MRSPagedResult
{ {
public IEnumerable<MRSResponse> Data { get; set; } = []; public IEnumerable<MRSPagedDto> Data { get; set; } = [];
public int RecordsTotal { get; set; } public int RecordsTotal { get; set; }
public List<string> DepartmentList { get; set; } = new List<string>(); public List<string> DepartmentList { get; set; } = new List<string>();
public List<string> DisciplineList { 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
{ {
public class MRSResponse public class MRSResponse
{ {
public long MRSId { get; set; } [JsonPropertyName("success")] public bool Success { get; set; }
public string MRSNo { get; set; } = string.Empty; [JsonPropertyName("message")] public string? Message { get; set; }
public long RISId { get; set; } [JsonPropertyName("data")] public MRSData? Data { get; set; }
public string RISNo { get; set; } = string.Empty;
public int InventoryId { get; set; }
public string ItemName { get; set; } = string.Empty;
public string ReturnedBy { get; set; } = string.Empty;
public decimal QtyReturned { get; set; }
public string? Condition { get; set; }
public string? Remarks { get; set; }
public short Status { get; set; }
public string StatusLabel => Status switch { 0 => "Draft", 1 => "Approved", 2 => "Cancelled", _ => "Unknown" };
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string? ApprovedBy { get; set; }
public DateTime? ApprovedDate { get; set; }
} }
} }

View File

@ -17,6 +17,8 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public long RRNo { get; set; } public long RRNo { get; set; }
public string CreatedBy { get; set; }=string.Empty; public string CreatedBy { get; set; }=string.Empty;
public bool IsActive { get; set; } public bool IsActive { get; set; }
public Inventory? Inventory { get; set; }
// public InventTransDetail? InventTransDetail { get; set; }
public ICollection<InventTransDetail> InventTransDetails { 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@ -24,5 +25,7 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public bool IsActive { get; set; } public bool IsActive { get; set; }
public long PRDetailId { get; set; } public long PRDetailId { get; set; }
public PRDetails? PRDetails { 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.Account;
using CPRNIMS.Infrastructure.Entities.Items;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@ -24,5 +25,6 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
public Lot? Lot { get; set; } public Lot? Lot { get; set; }
public ICollection<InventTrans> InventTrans { get; set; } = []; public ICollection<InventTrans> InventTrans { get; set; } = [];
public ApplicationUser? User { 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 RIS RIS { get; set; } = null!;
public Inventory Inventory { 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 CPRNIMS.Infrastructure.Entities.Common;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; 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 namespace CPRNIMS.Infrastructure.Entities.Items
{ {
@ -42,5 +36,6 @@ namespace CPRNIMS.Infrastructure.Entities.Items
public string? ItemAttachPath { get; set; } public string? ItemAttachPath { get; set; }
public bool IsMDLD { get; set; } public bool IsMDLD { get; set; }
public bool CheckBox { 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 byte RequestTypeId { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
public int CartItemCount { 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.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CPRNIMS.Infrastructure.Entities.Purchasing namespace CPRNIMS.Infrastructure.Entities.Purchasing
{ {

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Entities.Common; using CPRNIMS.Infrastructure.Entities.Common;
using CPRNIMS.Infrastructure.Entities.Items;
using CPRNIMS.Infrastructure.ViewModel.Items; using CPRNIMS.Infrastructure.ViewModel.Items;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -24,5 +25,6 @@ namespace CPRNIMS.Infrastructure.Entities.Purchasing
public decimal Qty { get; set; } public decimal Qty { get; set; }
public bool IsSearched { get; set; } public bool IsSearched { get; set; }
public PR? PRs { 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>
<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="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup> </ItemGroup>

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@
using CPRNIMS.Domain.Services; using CPRNIMS.Domain.Services;
using CPRNIMS.Infrastructure.Dto.Inventory; using CPRNIMS.Infrastructure.Dto.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request; using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Dto.PR;
using CPRNIMS.WebApi.Controllers.Base; using CPRNIMS.WebApi.Controllers.Base;
using Microsoft.AspNetCore.Mvc; 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 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.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.WebApi.Security; using CPRNIMS.WebApi.Security;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace CPRNIMS.WebApi.Controllers.Inventory namespace CPRNIMS.WebApi.Controllers.Inventory
{ {
@ -20,7 +19,11 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
[HttpGet("GetRIS")] [HttpGet("GetRIS")]
public async Task<IActionResult> GetRIS([FromQuery] RISFilterDto filter, CancellationToken ct) 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); return Ok(result);
} }
@ -48,12 +51,7 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
}); });
var ris = await _risRepo.CreateAsync(request, currentUser.UserName,ct); var ris = await _risRepo.CreateAsync(request, currentUser.UserName,ct);
return Ok(new return Ok(new { success = true, message = $"RIS# {ris.RISNo} created successfully." });
{
success = true,
message = $"RIS {ris.RISNo} created successfully.",
data= ris
});
} }
[HttpPut("ApproveRIS")] [HttpPut("ApproveRIS")]
@ -69,7 +67,11 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
[HttpPut("CancelRIS")] [HttpPut("CancelRIS")]
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request, CancellationToken ct) 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." }); 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); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices(builder); builder.Services.AddApplicationServices(builder);
builder.Services.AddFastReport();
builder.Services.AddAutoMapper(cfg => { }, typeof(SupplierRequestProfile)); builder.Services.AddAutoMapper(cfg => { }, typeof(SupplierRequestProfile));
@ -30,6 +31,8 @@ app.UseCors("AllowAnyOrigin");
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseFastReport();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

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

View File

@ -1,13 +1,13 @@
USE [CPRNIMS] USE [CPRNIMS]
GO 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 SET ANSI_NULLS ON
GO GO
SET QUOTED_IDENTIFIER ON SET QUOTED_IDENTIFIER ON
GO GO
ALTER PROCEDURE [dbo].[GetInventory] ALTER PROCEDURE [dbo].[GetInventory]
( (
@UserId VARCHAR(450)='89da2977-c70f-4df9-94d4-9a610aa999ea', @UserId VARCHAR(450)='16b37c00-131f-4205-b8f6-ad4d0f9f3a32',
@SearchPRNo VARCHAR(50) = '', @SearchPRNo VARCHAR(50) = '',
@SearchItemNo VARCHAR(50) = '', @SearchItemNo VARCHAR(50) = '',
@SearchItemName VARCHAR(100) = '', @SearchItemName VARCHAR(100) = '',
@ -20,6 +20,18 @@ AS
BEGIN BEGIN
SET NOCOUNT ON; SET NOCOUNT ON;
DECLARE @Offset INT = (@PageNumber - 1) * @PageSize; 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 ────────────── -- ── 1. Full department list — unaffected by any filter ──────────────
SELECT DISTINCT D.Department SELECT DISTINCT D.Department
@ -49,42 +61,30 @@ BEGIN
, ICAT.ItemCategoryName , ICAT.ItemCategoryName
, RRD.RemainingQty , RRD.RemainingQty
, ITD.CreatedDate , ITD.CreatedDate
,COALESCE(PCD.ProjectCode,'N/A') ProjectCode ,COALESCE(PC.ProjectCode,'N/A') ProjectCode
INTO #Inventory INTO #Inventory
FROM dbo.Inventory IV FROM dbo.Inventory IV
INNER JOIN dbo.InventTrans IT INNER JOIN dbo.InventTrans IT ON IV.InventoryId = IT.InventoryId
ON IV.InventoryId = IT.InventoryId INNER JOIN dbo.InventTransDetail ITD ON IT.InventTransId = ITD.InventTransId
INNER JOIN dbo.InventTransDetail ITD INNER JOIN dbo.PRDetails PRD ON ITD.PRDetailId = PRD.PRDetailsId AND PRD.IsActive = 1
ON IT.InventTransId = ITD.InventTransId INNER JOIN dbo.PR ON PR.PRId=PRD.PRId AND PR.IsActive = 1
INNER JOIN dbo.PRDetails PRD INNER JOIN dbo.RRDetails RRD ON PRD.PRDetailsId = RRD.PRDetailId AND RRD.IsActive = 1
ON ITD.PRDetailId = PRD.PRDetailsId INNER JOIN dbo.ItemCategories ICAT ON PRD.ItemCategoryId = ICAT.ItemCategoryId
AND PRD.IsActive = 1 LEFT JOIN dbo.Lot L ON IV.LotId = L.LotId
INNER JOIN dbo.RRDetails RRD LEFT JOIN dbo.LotType LT ON L.LotTypeId = LT.LotTypeId
ON PRD.PRDetailsId = RRD.PRDetailId LEFT JOIN dbo.ProjectCodes PC ON PR.ProjectCodeId = PC.ProjectCodeId
AND RRD.IsActive = 1 INNER JOIN dbo.Users U ON IV.UserId = U.Id
INNER JOIN dbo.ItemCategories ICAT INNER JOIN dbo.Departments D ON U.DepartmentId = D.DepartmentId
ON PRD.ItemCategoryId = ICAT.ItemCategoryId WHERE ITD.TransTypeId = 2 AND IV.IsActive = 1 AND ITD.IsActive = 1
INNER JOIN dbo.Lot L AND (
ON IV.LotId = L.LotId @HasFullAccess = 1
INNER JOIN dbo.LotType LT OR D.DepartmentId = @UserDepartmentId
ON L.LotTypeId = LT.LotTypeId )
INNER JOIN dbo.PR PR AND (@SearchPRNo = '' OR PR.PRNo = @SearchPRNo)
ON PR.UserId = IV.UserId AND (@SearchItemNo = '' OR PRD.ItemNo = @SearchItemNo)
AND PR.IsActive = 1 AND (@SearchItemName = '' OR PRD.ItemName LIKE '%' + @SearchItemName + '%')
INNER JOIN dbo.ProjectCodes PC AND (@SearchDept = '' OR D.Department LIKE '%' + @SearchDept + '%')
ON PR.ProjectCodeId = PC.ProjectCodeId AND (@SearchProjectCode = '' OR PC.ProjectCode LIKE '%' + @SearchProjectCode + '%')
INNER JOIN dbo.Users U
ON IV.UserId = U.Id
INNER JOIN dbo.Departments D
ON U.DepartmentId = D.DepartmentId
INNER JOIN dbo.ProjectCodes PCD
ON PR.ProjectCodeId = PCD.ProjectCodeId
WHERE ITD.TransTypeId=2 AND IV.IsActive=1 AND ITD.IsActive=1 AND
(@SearchPRNo = '' OR PR.PRNo = @SearchPRNo)
AND (@SearchItemNo = '' OR PRD.ItemNo = @SearchItemNo)
AND (@SearchItemName = '' OR PRD.ItemName LIKE '%' + @SearchItemName + '%')
AND (@SearchDept = '' OR D.Department LIKE '%' + @SearchDept + '%')
AND (@SearchProjectCode = '' OR PC.ProjectCode LIKE '%' + @SearchProjectCode + '%')
GROUP BY GROUP BY
IV.InventoryId IV.InventoryId
, IV.ItemNo , IV.ItemNo
@ -100,7 +100,7 @@ BEGIN
, ICAT.ItemCategoryName , ICAT.ItemCategoryName
, RRD.RemainingQty , RRD.RemainingQty
, ITD.CreatedDate , ITD.CreatedDate
,PCD.ProjectCode; ,PC.ProjectCode;
-- ── 3. Total count of filtered results ────────────────────────────── -- ── 3. Total count of filtered results ──────────────────────────────
SELECT COUNT(*) AS TotalCount 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> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<StaticWebAssetsEnabled>true</StaticWebAssetsEnabled>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Remove="Views\Components\Dashboard\DeptApprover.cshtml" /> <Content Remove="Views\Components\Dashboard\DeptApprover.cshtml" />
</ItemGroup> </ItemGroup>
@ -58,8 +60,8 @@
<ItemGroup> <ItemGroup>
<Folder Include="Common\Helper\" /> <Folder Include="Common\Helper\" />
<Folder Include="Properties\NewFolder\" />
<Folder Include="wwwroot\Content\Uploads\PRAttachment\" /> <Folder Include="wwwroot\Content\Uploads\PRAttachment\" />
<Folder Include="wwwroot\_content\FastReport.Web\js\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -68,8 +70,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CaptchaGen.NetCore" Version="1.1.2" /> <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.NewtonsoftJson" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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.Canvass;
using CPRNIMS.Domain.UIContracts.CaptCha; using CPRNIMS.Domain.UIContracts.CaptCha;
using CPRNIMS.Domain.UIContracts.Common; using CPRNIMS.Domain.UIContracts.Common;
@ -22,6 +23,7 @@ using CPRNIMS.Domain.UIServices.Receiving;
using CPRNIMS.Domain.UIServices.SMTP; using CPRNIMS.Domain.UIServices.SMTP;
using CPRNIMS.Infrastructure.Database; using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Helper; using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.Reports;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
@ -72,7 +74,7 @@ namespace CPRNIMS.WebApps.Common
private static void AddScopedServices(WebApplicationBuilder builder) private static void AddScopedServices(WebApplicationBuilder builder)
{ {
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<IApiConfigurationService, ApiConfigurationService>(); builder.Services.AddTransient<IApiConfigurationService, ApiConfigurationService>();
builder.Services.AddScoped<TokenHelper>(); builder.Services.AddScoped<TokenHelper>();
builder.Services.AddTransient<IItem, Item>(); builder.Services.AddTransient<IItem, Item>();
builder.Services.AddTransient<IPRequest, PRequest>(); builder.Services.AddTransient<IPRequest, PRequest>();
@ -88,6 +90,9 @@ namespace CPRNIMS.WebApps.Common
builder.Services.AddTransient<ICaptchaService, CaptchaService>(); builder.Services.AddTransient<ICaptchaService, CaptchaService>();
builder.Services.AddScoped<ErrorLogHelper>(); builder.Services.AddScoped<ErrorLogHelper>();
builder.Services.AddScoped<IRIS, RIS>(); 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) private static void AddSessionAndAuthentication(WebApplicationBuilder builder)
@ -170,7 +175,6 @@ namespace CPRNIMS.WebApps.Common
}; };
}); });
} }
private static void AddDbContext(WebApplicationBuilder builder) private static void AddDbContext(WebApplicationBuilder builder)
{ {
builder.Services.AddDbContext<NonInventoryDbContext>(options => builder.Services.AddDbContext<NonInventoryDbContext>(options =>

View File

@ -164,6 +164,7 @@ namespace CPRNIMS.WebApps.Controllers
new Claim(ClaimTypes.NameIdentifier, login.userId), new Claim(ClaimTypes.NameIdentifier, login.userId),
new Claim(ClaimTypes.Name, login.userName), new Claim(ClaimTypes.Name, login.userName),
new Claim("FullName", login.fullName), new Claim("FullName", login.fullName),
new Claim("DepartmentId", Convert.ToString(login.departmentId)),
new Claim("Company", login.company), new Claim("Company", login.company),
new Claim("Token", login.token), new Claim("Token", login.token),
new Claim("TokenExpiry", expirationTime.ToString("O")) 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.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request; using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper; 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.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request; using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper; using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.WebApps.Controllers.Base; using CPRNIMS.WebApps.Controllers.Base;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using FastReport;
using FastReport.Web;
using System.Threading.Tasks;
namespace CPRNIMS.WebApps.Controllers.Inventory namespace CPRNIMS.WebApps.Controllers.Inventory
{ {
public class RISMgmtController : BaseMethod public class RISMgmtController : BaseMethod
{ {
private readonly IRIS _ris; private readonly IRIS _ris;
private readonly IReportBuilder _builder;
private readonly IWebHostEnvironment _env;
public RISMgmtController(ErrorLogHelper errorMessageService, public RISMgmtController(ErrorLogHelper errorMessageService,
IWebHostEnvironment webHostEnvironment, TokenHelper tokenHelper IWebHostEnvironment webHostEnvironment, TokenHelper tokenHelper
, IRIS ris, IAccount account) , IRIS ris, IAccount account, IReportBuilder builder)
: base(errorMessageService, webHostEnvironment, tokenHelper, account) : base(errorMessageService, webHostEnvironment, tokenHelper, account)
{ {
_ris = ris; _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] [HttpPost]
public async Task<IActionResult> CreateRIS([FromBody] CreateRISRequest request,CancellationToken ct) public async Task<IActionResult> CreateRIS([FromBody] CreateRISRequest request,CancellationToken ct)
{ {
var result = await _ris.CreateRIS(request,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] [HttpPost]
public async Task<IActionResult> ApproveRIS([FromBody] ApproveRISRequest request,CancellationToken ct) 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) if (!result.success)
return BadRequest(new { success = false, message = "RIS cancelled failed" }); return BadRequest(new { success = false, message = result.message });
return Ok(new return Ok(new { success = true, message = result.message });
{
success = true,
message = $"RIS approved successfully."
});
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request,CancellationToken ct) public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request,CancellationToken ct)
{ {
if (string.IsNullOrWhiteSpace(request.Reason)) if (string.IsNullOrWhiteSpace(request.Reason))
return BadRequest(new return BadRequest(new{success = false,message = "A reason for cancellation is required."});
{
success = false,
message = "A reason for cancellation is required."
});
bool isSuccess = await _ris.CancelRIS(request,ct);
if (!isSuccess) var result = await _ris.CancelRIS(request, ct);
return BadRequest(new { success = false, message = "RIS cancelled failed" });
return Ok(new if (!result.success)
{ return BadRequest(new { success = false, message = result.message });
success = true,
message = "RIS cancelled and inventory restored." return Ok(new { success = true, message = result.message });
});
} }
[HttpGet] [HttpGet]
@ -92,10 +114,5 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
disciplineList = result.DisciplineList 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 CPRNIMS.WebApps.Common;
using Microsoft.AspNetCore.StaticFiles;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStaticWebAssets();
builder.Services.AddApplicationServices(builder); builder.Services.AddApplicationServices(builder);
builder.Services.AddFastReport();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Home/Error"); app.UseExceptionHandler("/Home/Error");
app.UseHsts(); app.UseHsts();
} }
//app.UseRewriter(options);
//var provider = new FileExtensionContentTypeProvider();
//provider.Mappings[".js"] = "text/javascript";
//provider.Mappings[".css"] = "text/css";
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseSession(); app.UseSession();
app.MapHub<CartHub>("/cartHub");
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseFastReport();
app.MapHub<CartHub>("/cartHub");
app.MapControllerRoute( app.MapControllerRoute(
name: "default", name: "default",
//pattern: "{controller=ItemMgmt}/{action=Index}/{id?}"); pattern: "{controller=Home}/{action=Index}/{id?}");
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run(); app.Run();

View File

@ -9,9 +9,11 @@ namespace CPRNIMS.WebApps.ViewComponents.Inventory
string viewName = InventoryTabPageId switch string viewName = InventoryTabPageId switch
{ {
1 => "~/Views/Components/Inventory/TabPage/Inventory.cshtml", 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", 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); 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 ── *@ @* ── FILTER BAR ── *@
<div class="inv-filters"> <div class="inv-filters">
<div class="inv-search-box"> <div class="inv-search-box">
@ -196,8 +191,7 @@
</div> </div>
</div> </div>
</div> </div>
<style>
<style>
/* ── RIS card grid ── */ /* ── RIS card grid ── */
.ris-grid { .ris-grid {
display: grid; display: grid;
@ -418,6 +412,58 @@
font-weight: 600; font-weight: 600;
color: var(--teal-dark, #0d5c63); 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> </style>
<script> <script>
@ -929,7 +975,95 @@
month: "short", day: "numeric", year: "numeric" 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 ─────────────────────────────────────────────────────── // ── Initial load ───────────────────────────────────────────────────────
fetchData(); 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 href="~/lib/font-awesome/css/all.min.css" rel="stylesheet" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link href="~/css/spinner.css" rel="stylesheet" /> <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" /> <link href="~/css/loginpageinteractive.css" rel="stylesheet" />
</head> </head>
<body> <body>

View File

@ -19,18 +19,20 @@
<i class="fas fa-user-tag"></i> Inventory <i class="fas fa-user-tag"></i> Inventory
</button> </button>
<button class="inv-tab-btn" data-tab-id="2" role="tab"> <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>
<button class="inv-tab-btn" data-tab-id="3" role="tab"> <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>
<button class="inv-tab-btn" data-tab-id="4" role="tab"> <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>
<button class="inv-tab-btn" data-tab-id="5" role="tab"> <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> </button>
</div> </div>
<div id="inv-tab-content"> <div id="inv-tab-content">
@ -76,6 +78,7 @@
const html = await res.text(); const html = await res.text();
cache[tabId] = html; cache[tabId] = html;
injectHtml(tabContent, html); injectHtml(tabContent, html);
} catch (err) { } catch (err) {
console.error("Tab load error:", err); console.error("Tab load error:", err);
tabContent.innerHTML = ` 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 { .inv-tabs {
display: flex; display: flex;
gap: 6px; gap: 6px;
flex-wrap: wrap;
background: #fff; background: #fff;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: 6px; padding: 6px;
@ -104,7 +105,8 @@
} }
.inv-tab-btn { .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; display: flex;
align-items: center; align-items: center;
justify-content: 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/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery/dist/jquery371.min.js"></script> <script src="~/lib/jquery/dist/jquery371.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script> <script src="~/js/site.js" asp-append-version="true"></script>

View File

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