CRUD for RIS, MRS, Inventory reports are working properly

This commit is contained in:
rowell_m_soriano 2026-06-25 08:57:30 +08:00
parent 440cdfcdb7
commit 3c0be5eae2
17 changed files with 1206 additions and 385 deletions

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Reports; using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -9,8 +10,8 @@ namespace CPRNIMS.Domain.Contracts.Inventory
{ {
public interface IInventoryReports public interface IInventoryReports
{ {
Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = ""); Task<RISReportDto> GetRISReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct);
Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = ""); Task<MRSReportDto> GetMRSReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct);
Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = ""); Task<InventoryReportDto> GetInventoryReportAsync(InventoryReportsRequest request,string userName, int? departmentId, CancellationToken ct);
} }
} }

View File

@ -1,8 +1,11 @@
using CPRNIMS.Domain.Contracts.Inventory; using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Infrastructure.Database; using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports; using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Drawing.Printing;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CPRNIMS.Domain.Services.Inventory namespace CPRNIMS.Domain.Services.Inventory
{ {
@ -11,22 +14,23 @@ namespace CPRNIMS.Domain.Services.Inventory
private readonly NonInventoryDbContext _db; private readonly NonInventoryDbContext _db;
public InventoryReports(NonInventoryDbContext db) => _db = db; public InventoryReports(NonInventoryDbContext db) => _db = db;
public async Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, public async Task<RISReportDto> GetRISReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct)
int? departmentId = null, string? userName = "")
{ {
var endDate = dateTo.Date.AddDays(1); var endDate = request.DateTo.Date.AddDays(1);
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" }; var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName) bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase); && allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
var dateToInclusive = dateTo.AddDays(1); var dateToInclusive = request.DateTo.AddDays(1);
var query = _db.RIS var query = _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)
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < dateToInclusive); .Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Where(r => r.CreatedDate >= request.DateFrom && r.CreatedDate < dateToInclusive);
if (departmentId.HasValue && !seeAllDepartments) if (departmentId.HasValue && !seeAllDepartments)
{ {
@ -94,8 +98,8 @@ namespace CPRNIMS.Domain.Services.Inventory
return new RISReportDto return new RISReportDto
{ {
ReportNo = $"RPT-RIS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}", ReportNo = $"RPT-RIS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
DateFrom = dateFrom, DateFrom = request.DateFrom,
DateTo = dateTo, DateTo = request.DateTo,
Summary = summary, Summary = summary,
Rows = rows, Rows = rows,
ByDiscipline = byDiscipline, ByDiscipline = byDiscipline,
@ -103,10 +107,9 @@ namespace CPRNIMS.Domain.Services.Inventory
}; };
} }
public async Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, public async Task<MRSReportDto> GetMRSReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct)
int? departmentId = null, string? userName = "")
{ {
var endDate = dateTo.Date.AddDays(1); var endDate = request.DateTo.Date.AddDays(1);
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" }; var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName) bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
@ -116,7 +119,9 @@ namespace CPRNIMS.Domain.Services.Inventory
.Include(m => m.RIS) .Include(m => m.RIS)
.Include(m => m.Inventory) .Include(m => m.Inventory)
.ThenInclude(i => i.User) .ThenInclude(i => i.User)
.Where(m => m.CreatedDate >= dateFrom && .Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Where(m => m.CreatedDate >= request.DateFrom &&
m.CreatedDate < endDate); m.CreatedDate < endDate);
if (departmentId.HasValue && !seeAllDepartments) if (departmentId.HasValue && !seeAllDepartments)
@ -146,7 +151,7 @@ namespace CPRNIMS.Domain.Services.Inventory
// Total RIS qty issued in the same period (for the comparison panel) // Total RIS qty issued in the same period (for the comparison panel)
var totalRISQty = await _db.RIS var totalRISQty = await _db.RIS
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < endDate .Where(r => r.CreatedDate >= request.DateFrom && r.CreatedDate < endDate
&& r.Status != 2) && r.Status != 2)
.SumAsync(r => (int?)r.QtyIssued, ct) ?? 0; .SumAsync(r => (int?)r.QtyIssued, ct) ?? 0;
@ -177,49 +182,62 @@ namespace CPRNIMS.Domain.Services.Inventory
return new MRSReportDto return new MRSReportDto
{ {
ReportNo = $"RPT-MRS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}", ReportNo = $"RPT-MRS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
DateFrom = dateFrom, DateFrom = request.DateFrom,
DateTo = dateTo, DateTo = request.DateTo,
Summary = summary, Summary = summary,
Rows = rows, Rows = rows,
ByCondition = byCondition ByCondition = byCondition
}; };
} }
public async Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, public async Task<InventoryReportDto> GetInventoryReportAsync(InventoryReportsRequest request, string userName, int? departmentId, CancellationToken ct)
int? departmentId = null, string? userName = "")
{ {
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" }; var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName) bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase); && allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
var endDate = request.DateTo.Date.AddDays(1);
var dto = new InventoryReportDto var dto = new InventoryReportDto
{ {
ReportNo = $"RPT-INV-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}", ReportNo = $"RPT-INV-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
AsOf = dateTo.Date, AsOf = endDate,
Rows = new List<InventoryReportRow>(), Rows = new List<InventoryReportRow>(),
ByCategory = new List<CategoryStockLevel>(), ByCategory = new List<CategoryStockLevel>(),
Alerts = new List<InventoryAlert>(), Alerts = new List<InventoryAlert>(),
Summary = new InventoryReportSummary() Summary = new InventoryReportSummary(),
Departments = new List<string>(),
Page = request.Page,
PageSize = request.PageSize
}; };
var conn = _db.Database.GetDbConnection(); var conn = _db.Database.GetDbConnection();
await using var cmd = conn.CreateCommand(); await using var cmd = conn.CreateCommand();
cmd.CommandText = "dbo.GetInventoryReport"; cmd.CommandText = "dbo.GetInventoryReport";
cmd.CommandType = System.Data.CommandType.StoredProcedure; cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@DateFrom", (object?)request.DateFrom ?? DBNull.Value));
cmd.Parameters.Add(new SqlParameter("@DateFrom", dateFrom)); cmd.Parameters.Add(new SqlParameter("@DateTo", (object?)request.DateTo ?? DBNull.Value));
cmd.Parameters.Add(new SqlParameter("@DateTo", dateTo)); cmd.Parameters.Add(new SqlParameter("@Department", (object?)request.Department ?? DBNull.Value));
cmd.Parameters.Add(new SqlParameter("@DepartmentId", (object?)departmentId ?? DBNull.Value)); cmd.Parameters.Add(new SqlParameter("@DepartmentId", (object?)departmentId ?? DBNull.Value));
cmd.Parameters.Add(new SqlParameter("@SeeAllDepartments", seeAllDepartments ? 1 : 0)); cmd.Parameters.Add(new SqlParameter("@SeeAllDepartments", seeAllDepartments ? 1 : 0));
cmd.Parameters.Add(new SqlParameter("@Page", request.Page));
cmd.Parameters.Add(new SqlParameter("@PageSize", request.PageSize));
cmd.Parameters.Add(new SqlParameter("@Paginate", request.Paginate ? 1 : 0));
if (conn.State != System.Data.ConnectionState.Open) bool openedHere = conn.State != System.Data.ConnectionState.Open;
await conn.OpenAsync(ct); if (openedHere) await conn.OpenAsync(ct);
try try
{ {
await using var reader = await cmd.ExecuteReaderAsync(ct); await using var reader = await cmd.ExecuteReaderAsync(ct);
// 0: departments
while (await reader.ReadAsync(ct))
dto.Departments.Add(reader.GetString(reader.GetOrdinal("Department")));
// 1: detail rows // 1: detail rows
if (await reader.NextResultAsync(ct))
{
while (await reader.ReadAsync(ct)) while (await reader.ReadAsync(ct))
{ {
dto.Rows.Add(new InventoryReportRow dto.Rows.Add(new InventoryReportRow
@ -227,8 +245,9 @@ namespace CPRNIMS.Domain.Services.Inventory
ItemName = reader.GetString(reader.GetOrdinal("ItemName")), ItemName = reader.GetString(reader.GetOrdinal("ItemName")),
ItemNo = reader.GetInt64(reader.GetOrdinal("ItemNo")), ItemNo = reader.GetInt64(reader.GetOrdinal("ItemNo")),
ItemCategoryName = reader.GetString(reader.GetOrdinal("ItemCategoryName")), ItemCategoryName = reader.GetString(reader.GetOrdinal("ItemCategoryName")),
LotNo = reader.IsDBNull(reader.GetOrdinal("LotNo")) LotNo = reader.IsDBNull(reader.GetOrdinal("LotNo")) ? null : reader.GetString(reader.GetOrdinal("LotNo")),
? null : reader.GetString(reader.GetOrdinal("LotNo")), Department = reader.IsDBNull(reader.GetOrdinal("Department")) ? null : reader.GetString(reader.GetOrdinal("Department")),
CurrencyCode = reader.IsDBNull(reader.GetOrdinal("CurrencyCode")) ? null : reader.GetString(reader.GetOrdinal("CurrencyCode")),
QtyIn = reader.GetDecimal(reader.GetOrdinal("QtyIn")), QtyIn = reader.GetDecimal(reader.GetOrdinal("QtyIn")),
QtyOut = reader.GetDecimal(reader.GetOrdinal("QtyOut")), QtyOut = reader.GetDecimal(reader.GetOrdinal("QtyOut")),
QtyOnHand = reader.GetDecimal(reader.GetOrdinal("QtyOnHand")), QtyOnHand = reader.GetDecimal(reader.GetOrdinal("QtyOnHand")),
@ -236,6 +255,7 @@ namespace CPRNIMS.Domain.Services.Inventory
StockPct = reader.GetInt32(reader.GetOrdinal("StockPct")) StockPct = reader.GetInt32(reader.GetOrdinal("StockPct"))
}); });
} }
}
// 2: summary // 2: summary
if (await reader.NextResultAsync(ct) && await reader.ReadAsync(ct)) if (await reader.NextResultAsync(ct) && await reader.ReadAsync(ct))
@ -254,30 +274,26 @@ namespace CPRNIMS.Domain.Services.Inventory
// 3: by category // 3: by category
if (await reader.NextResultAsync(ct)) if (await reader.NextResultAsync(ct))
{
while (await reader.ReadAsync(ct)) while (await reader.ReadAsync(ct))
{
dto.ByCategory.Add(new CategoryStockLevel dto.ByCategory.Add(new CategoryStockLevel
{ {
CategoryName = reader.GetString(reader.GetOrdinal("CategoryName")), CategoryName = reader.GetString(reader.GetOrdinal("CategoryName")),
AvgStockPct = reader.GetInt32(reader.GetOrdinal("AvgStockPct")) AvgStockPct = reader.GetInt32(reader.GetOrdinal("AvgStockPct"))
}); });
}
}
// 4: alerts // 4: alerts
if (await reader.NextResultAsync(ct)) if (await reader.NextResultAsync(ct))
{
while (await reader.ReadAsync(ct)) while (await reader.ReadAsync(ct))
{
dto.Alerts.Add(new InventoryAlert dto.Alerts.Add(new InventoryAlert
{ {
ItemName = reader.GetString(reader.GetOrdinal("ItemName")), ItemName = reader.GetString(reader.GetOrdinal("ItemName")),
QtyOnHand = reader.GetDecimal(reader.GetOrdinal("QtyOnHand")), QtyOnHand = reader.GetDecimal(reader.GetOrdinal("QtyOnHand")),
Severity = reader.GetString(reader.GetOrdinal("Severity")) Severity = reader.GetString(reader.GetOrdinal("Severity"))
}); });
}
} // 5: total row count
if (await reader.NextResultAsync(ct) && await reader.ReadAsync(ct))
dto.TotalRows = reader.GetInt32(reader.GetOrdinal("TotalRows"));
} }
finally finally
{ {

View File

@ -1,5 +1,5 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Reports; using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response; using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -11,8 +11,8 @@ namespace CPRNIMS.Domain.UIContracts.Inventory
public interface IInventoryReports public interface IInventoryReports
{ {
Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct); Task<RISReportDto> GetRISReportAsync(InventoryReportsRequest request, CancellationToken ct);
Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct); Task<MRSReportDto> GetMRSReportAsync(InventoryReportsRequest request, CancellationToken ct);
Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct); Task<InventoryReportDto> GetInventoryReportAsync(InventoryReportsRequest request, CancellationToken ct);
} }
} }

View File

@ -1,8 +1,11 @@
using CPRNIMS.Domain.UIContracts.Common; using Azure.Core;
using CPRNIMS.Domain.UIContracts.Common;
using CPRNIMS.Domain.UIContracts.Inventory; using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports; using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response; using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper; using CPRNIMS.Infrastructure.Helper;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -24,7 +27,7 @@ namespace CPRNIMS.Domain.UIServices.Inventory
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
}; };
public async Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<InventoryReportDto> GetInventoryReportAsync(InventoryReportsRequest request, CancellationToken ct)
{ {
var token = await _tokenHelper.GetValidTokenAsync(); var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -33,11 +36,20 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetInventoryReport"] var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetInventoryReport"]
?? throw new InvalidOperationException("GetInventoryReport endpoint is not configured."); ?? throw new InvalidOperationException("GetInventoryReport endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?'); var qs = new Dictionary<string, string?>
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}"); {
["dateFrom"] = request.DateFrom.ToString("yyyy-MM-dd"),
["dateTo"] = request.DateTo.ToString("yyyy-MM-dd"),
["department"] = request.Department,
["page"] = request.Page.ToString(),
["pageSize"] = request.PageSize.ToString(),
["paginate"] = request.Paginate.ToString().ToLowerInvariant()
};
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token); var url = QueryHelpers.AddQueryString(baseEndpoint, qs);
var response = await http.GetAsync(qs.ToString(), ct);
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync(url, ct);
var json = await response.Content.ReadAsStringAsync(ct); var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
@ -47,7 +59,7 @@ namespace CPRNIMS.Domain.UIServices.Inventory
return result ?? new InventoryReportDto(); return result ?? new InventoryReportDto();
} }
public async Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<MRSReportDto> GetMRSReportAsync(InventoryReportsRequest request, CancellationToken ct)
{ {
var token = await _tokenHelper.GetValidTokenAsync(); var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -56,11 +68,20 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetMRSReport"] var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetMRSReport"]
?? throw new InvalidOperationException("GetMRS endpoint is not configured."); ?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?'); var qs = new Dictionary<string, string?>
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}"); {
["dateFrom"] = request.DateFrom.ToString("yyyy-MM-dd"),
["dateTo"] = request.DateTo.ToString("yyyy-MM-dd"),
["department"] = request.Department,
["page"] = request.Page.ToString(),
["pageSize"] = request.PageSize.ToString(),
["paginate"] = request.Paginate.ToString().ToLowerInvariant()
};
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token); var url = QueryHelpers.AddQueryString(baseEndpoint, qs);
var response = await http.GetAsync(qs.ToString(), ct);
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync(url, ct);
var json = await response.Content.ReadAsStringAsync(ct); var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
@ -70,7 +91,7 @@ namespace CPRNIMS.Domain.UIServices.Inventory
return result ?? new MRSReportDto(); return result ?? new MRSReportDto();
} }
public async Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<RISReportDto> GetRISReportAsync(InventoryReportsRequest request, CancellationToken ct)
{ {
var token = await _tokenHelper.GetValidTokenAsync(); var token = await _tokenHelper.GetValidTokenAsync();
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -79,11 +100,20 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRISReport"] var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRISReport"]
?? throw new InvalidOperationException("GetMRS endpoint is not configured."); ?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?'); var qs = new Dictionary<string, string?>
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}"); {
["dateFrom"] = request.DateFrom.ToString("yyyy-MM-dd"),
["dateTo"] = request.DateTo.ToString("yyyy-MM-dd"),
["department"] = request.Department,
["page"] = request.Page.ToString(),
["pageSize"] = request.PageSize.ToString(),
["paginate"] = request.Paginate.ToString().ToLowerInvariant()
};
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token); var url = QueryHelpers.AddQueryString(baseEndpoint, qs);
var response = await http.GetAsync(qs.ToString(), ct);
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync(url, ct);
var json = await response.Content.ReadAsStringAsync(ct); var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@ -108,7 +108,11 @@ namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports
public string PreparedBy { get; set; } = "Finance Department"; public string PreparedBy { get; set; } = "Finance Department";
public string ReportNo { get; set; } = string.Empty; public string ReportNo { get; set; } = string.Empty;
public DateTime AsOf { get; set; } public DateTime AsOf { get; set; }
public List<string> Departments { get; set; } = new();
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalRows { get; set; }
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling(TotalRows / (double)PageSize) : 0;
public InventoryReportSummary Summary { get; set; } = new(); public InventoryReportSummary Summary { get; set; } = new();
public List<InventoryReportRow> Rows { get; set; } = []; public List<InventoryReportRow> Rows { get; set; } = [];
public List<CategoryStockLevel> ByCategory { get; set; } = []; public List<CategoryStockLevel> ByCategory { get; set; } = [];
@ -137,6 +141,8 @@ namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports
public decimal QtyOnHand { get; set; } public decimal QtyOnHand { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public int StockPct { get; set; } public int StockPct { get; set; }
public string? Department { get; set; }
public string? CurrencyCode { get; set; }
} }
public class CategoryStockLevel public class CategoryStockLevel

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
{
public class InventoryReportsRequest
{
public DateTime DateFrom { get; set; }
public DateTime DateTo { get; set; }
public string? Department { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public bool Paginate { get; set; }
}
}

View File

@ -1,6 +1,8 @@
using CPRNIMS.Domain.Contracts.Inventory; using Azure.Core;
using CPRNIMS.Domain.Contracts.Inventory;
using CPRNIMS.Domain.Contracts.Reports; using CPRNIMS.Domain.Contracts.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports; using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.WebApi.Security; using CPRNIMS.WebApi.Security;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -19,35 +21,35 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
} }
[HttpGet("GetRISReport")] [HttpGet("GetRISReport")]
public async Task<IActionResult> GetRISReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<IActionResult> GetRISReport([FromQuery] InventoryReportsRequest request, CancellationToken ct)
{ {
var currentUser = User.ToUserClaims(); var currentUser = User.ToUserClaims();
if (currentUser == null) if (currentUser == null)
return BadRequest(); return BadRequest();
var data = await _reports.GetRISReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName); var data = await _reports.GetRISReportAsync(request, currentUser.UserName,currentUser.DepartmentId, ct);
return Ok(data); return Ok(data);
} }
[HttpGet("GetMRSReport")] [HttpGet("GetMRSReport")]
public async Task<IActionResult> GetMRSReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<IActionResult> GetMRSReport([FromQuery] InventoryReportsRequest request, CancellationToken ct)
{ {
var currentUser = User.ToUserClaims(); var currentUser = User.ToUserClaims();
if (currentUser == null) if (currentUser == null)
return BadRequest(); return BadRequest();
var data = await _reports.GetMRSReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName); var data = await _reports.GetMRSReportAsync(request, currentUser.UserName, currentUser.DepartmentId, ct);
return Ok(data); return Ok(data);
} }
[HttpGet("GetInventoryReport")] [HttpGet("GetInventoryReport")]
public async Task<IActionResult> GetInventoryReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<IActionResult> GetInventoryReport([FromQuery] InventoryReportsRequest request, CancellationToken ct)
{ {
var currentUser = User.ToUserClaims(); var currentUser = User.ToUserClaims();
if (currentUser == null) if (currentUser == null)
return BadRequest(); return BadRequest();
var data = await _reports.GetInventoryReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName); var data = await _reports.GetInventoryReportAsync(request, currentUser.UserName, currentUser.DepartmentId, ct);
return Ok(data); return Ok(data);
} }

View File

@ -1,40 +1,71 @@
using CPRNIMS.Domain.UIContracts.Inventory; using CPRNIMS.Domain.Services.Account;
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.UIContracts.Inventory;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.WebApps.Controllers.Base;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Drawing.Printing;
namespace CPRNIMS.WebApps.Controllers.Inventory namespace CPRNIMS.WebApps.Controllers.Inventory
{ {
public class InventoryReportsController : Controller public class InventoryReportsController : BaseMethod
{ {
private readonly IInventoryReports _reports; private readonly IInventoryReports _reports;
public InventoryReportsController(IInventoryReports reports) public InventoryReportsController(ErrorLogHelper errorMessageService,
{ IWebHostEnvironment webHostEnvironment, TokenHelper tokenHelper, IInventoryReports reports, IAccount account)
_reports=reports; : base(errorMessageService, webHostEnvironment, tokenHelper, account) => _reports = reports;
}
[HttpGet] [HttpGet]
public async Task<IActionResult> GetInventoryReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<IActionResult> GetInventoryReport(
DateTime dateFrom, DateTime dateTo, string? department,int page = 1, int pageSize = 10, bool paginate = true, CancellationToken ct = default)
{ {
var response = await _reports.GetInventoryReportAsync(dateFrom, dateTo, ct); var dto = new InventoryReportsRequest
{
DateFrom = dateFrom,
DateTo = dateTo,
Department = department,
Page = page,
PageSize = pageSize,
Paginate = paginate
};
GetUser();
var response = await _reports.GetInventoryReportAsync(dto,ct);
return GetResponse(response); return GetResponse(response);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetRISReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<IActionResult> GetRISReport(
DateTime dateFrom, DateTime dateTo, string? department, int page = 1, int pageSize = 10, bool paginate = true, CancellationToken ct = default)
{ {
var response = await _reports.GetRISReportAsync(dateFrom, dateTo, ct); var dto = new InventoryReportsRequest
{
DateFrom = dateFrom,
DateTo = dateTo,
Department = department,
Page = page,
PageSize = pageSize,
Paginate = paginate
};
GetUser();
var response = await _reports.GetRISReportAsync(dto, ct);
return GetResponse(response); return GetResponse(response);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetMRSReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct) public async Task<IActionResult> GetMRSReport(
DateTime dateFrom, DateTime dateTo, string? department, int page = 1, int pageSize = 10, bool paginate = true, CancellationToken ct = default)
{ {
var response = await _reports.GetMRSReportAsync(dateFrom, dateTo, ct); var dto = new InventoryReportsRequest
{
DateFrom = dateFrom,
DateTo = dateTo,
Department = department,
Page = page,
PageSize = pageSize,
Paginate = paginate
};
GetUser();
var response = await _reports.GetMRSReportAsync(dto, ct);
return GetResponse(response); return GetResponse(response);
} }
protected IActionResult GetResponse<T>(T response)
{
return Json(new
{
success = response != null,
data = response ?? Activator.CreateInstance<T>()
});
}
} }
} }

View File

@ -25,7 +25,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
"2" => 2, "2" => 2,
_ => null _ => null
}; };
GetUser();
var result = await _mrs.GetMRSPaged(new MRSPagedRequest var result = await _mrs.GetMRSPaged(new MRSPagedRequest
{ {
SearchMRSNo = searchMRSNo, SearchMRSNo = searchMRSNo,
@ -43,6 +43,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpGet] [HttpGet]
public async Task<IActionResult> SearchRIS([FromQuery] int? searchProjectCodeId, string? searchRISNo , CancellationToken ct = default) public async Task<IActionResult> SearchRIS([FromQuery] int? searchProjectCodeId, string? searchRISNo , CancellationToken ct = default)
{ {
GetUser();
var result = await _mrs.SearchRIS(new SearchRISProjectCodeRequest var result = await _mrs.SearchRIS(new SearchRISProjectCodeRequest
{ {
SearchRISNo = searchRISNo, SearchRISNo = searchRISNo,
@ -54,6 +55,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpGet] [HttpGet]
public async Task<IActionResult> SearchProjects([FromQuery] string? searchProjectCode, CancellationToken ct = default) public async Task<IActionResult> SearchProjects([FromQuery] string? searchProjectCode, CancellationToken ct = default)
{ {
GetUser();
var result = await _mrs.SearchProjects(new SearchRISProjectCodeRequest var result = await _mrs.SearchProjects(new SearchRISProjectCodeRequest
{ SearchProjectCode = searchProjectCode,}, ct); { SearchProjectCode = searchProjectCode,}, ct);
@ -62,6 +64,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost] [HttpPost]
public async Task<IActionResult> CreateMRS([FromBody] CreateMRSRequest request,CancellationToken ct) public async Task<IActionResult> CreateMRS([FromBody] CreateMRSRequest request,CancellationToken ct)
{ {
GetUser();
var result = await _mrs.CreateMRS(request, ct); var result = await _mrs.CreateMRS(request, ct);
if (!result.success) if (!result.success)
@ -73,6 +76,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost] [HttpPost]
public async Task<IActionResult> ApproveMRS([FromBody] ApproveMRSRequest request,CancellationToken ct) public async Task<IActionResult> ApproveMRS([FromBody] ApproveMRSRequest request,CancellationToken ct)
{ {
GetUser();
var result = await _mrs.ApproveMRS(request, ct); var result = await _mrs.ApproveMRS(request, ct);
if (!result.success) if (!result.success)
@ -84,6 +88,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost] [HttpPost]
public async Task<IActionResult> CancelMRS([FromBody] CancelMRSRequest request,CancellationToken ct) public async Task<IActionResult> CancelMRS([FromBody] CancelMRSRequest request,CancellationToken ct)
{ {
GetUser();
var result = await _mrs.CancelMRS(request, ct); var result = await _mrs.CancelMRS(request, ct);
if (!result.success) if (!result.success)

View File

@ -5,9 +5,6 @@ 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
{ {
@ -27,30 +24,10 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
} }
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)
{ {
GetUser();
var result = await _ris.CreateRIS(request,ct); var result = await _ris.CreateRIS(request,ct);
if (!result.success) if (!result.success)
return BadRequest(new { success = false, message = result.message }); return BadRequest(new { success = false, message = result.message });
@ -60,6 +37,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost] [HttpPost]
public async Task<IActionResult> ApproveRIS([FromBody] ApproveRISRequest request,CancellationToken ct) public async Task<IActionResult> ApproveRIS([FromBody] ApproveRISRequest request,CancellationToken ct)
{ {
GetUser();
var result = await _ris.ApproveRIS(request, ct); var result = await _ris.ApproveRIS(request, ct);
if (!result.success) if (!result.success)
@ -71,6 +49,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost] [HttpPost]
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request,CancellationToken ct) public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request,CancellationToken ct)
{ {
GetUser();
if (string.IsNullOrWhiteSpace(request.Reason)) if (string.IsNullOrWhiteSpace(request.Reason))
return BadRequest(new{success = false,message = "A reason for cancellation is required."}); return BadRequest(new{success = false,message = "A reason for cancellation is required."});
@ -94,7 +73,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
"2" => 2, "2" => 2,
_ => null _ => null
}; };
GetUser();
var result = await _ris.GetRISPaged(new RISPagedRequest var result = await _ris.GetRISPaged(new RISPagedRequest
{ {
SearchRISNo = searchRISNo, SearchRISNo = searchRISNo,

View File

@ -5,26 +5,41 @@
color: #1a2e35; color: #1a2e35;
padding: 24px; padding: 24px;
} }
@@media print {
body { padding: 0; margin: 0; background: #fff;}
body * { visibility: hidden;}
#inv-rpt-page, #inv-rpt-page * {
visibility: visible; @@media print {
body > *:not(#print-mount) {
display: none !important;
} }
#inv-rpt-page { #print-mount {
position: absolute; display: block !important;
left: 0; position: static !important;
top: 0;
width: 100%; width: 100%;
}
#print-mount .rpt-page {
box-shadow: none !important; box-shadow: none !important;
border: none !important; border: none !important;
border-radius: 0 !important; border-radius: 0 !important;
overflow: visible !important;
} }
.no-print { .rpt-section {
display: none !important; page-break-inside: auto;
}
.rpt-table {
table-layout: fixed;
width: 100%;
font-size: 9px;
page-break-inside: auto;
}
.rpt-table th, .rpt-table td {
padding: 5px 6px;
white-space: normal;
word-break: break-word;
} }
.rpt-table tr { .rpt-table tr {
@ -35,8 +50,11 @@
display: table-header-group; display: table-header-group;
} }
@@page {
size: A4 landscape;
margin: 5mm;
}
} }
.rpt-page { .rpt-page {
width: 100%; width: 100%;
margin: 0; margin: 0;
@ -45,6 +63,7 @@
border-radius: var(--radius-lg, 14px); border-radius: var(--radius-lg, 14px);
overflow: hidden; overflow: hidden;
} }
.rpt-header { .rpt-header {
padding: 20px 24px 16px; padding: 20px 24px 16px;
border-bottom: 1px solid #d6eaec; border-bottom: 1px solid #d6eaec;
@ -117,6 +136,7 @@
background: var(--teal-pale, #e6f7f8); background: var(--teal-pale, #e6f7f8);
border-color: var(--teal-dark, #0d5c63); border-color: var(--teal-dark, #0d5c63);
} }
.rpt-header-top { .rpt-header-top {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -387,76 +407,256 @@
const xlsxBtn = document.getElementById("inv-rpt-excel"); const xlsxBtn = document.getElementById("inv-rpt-excel");
const container = document.getElementById("inv-rpt-container"); const container = document.getElementById("inv-rpt-container");
if (!fromEl || !toEl || !container) { // Department
console.error("Inventory report subtab init failed — missing elements."); const deptWrap = document.getElementById("inv-rpt-dept-wrap");
const deptTrigger = document.getElementById("inv-rpt-dept-trigger");
const deptDropdown= document.getElementById("inv-rpt-dept-dropdown");
const deptList = document.getElementById("inv-rpt-dept-list");
const deptLbl = document.getElementById("inv-rpt-dept-lbl");
const deptSearch = document.getElementById("inv-rpt-dept-search");
let showAll = false;
let selectedDept = ""; // "" = All
let allDepartments = []; // [{ name }]
// open/close
deptTrigger.addEventListener("click", () => {
const open = deptDropdown.classList.toggle("open");
deptTrigger.classList.toggle("open", open);
if (open) { deptSearch.value = ""; renderDeptOptions(""); deptSearch.focus(); }
});
// close on outside click
document.addEventListener("click", (e) => {
if (!deptWrap.contains(e.target)) {
deptDropdown.classList.remove("open");
deptTrigger.classList.remove("open");
}
});
// live filter
deptSearch.addEventListener("input", () => renderDeptOptions(deptSearch.value.trim().toLowerCase()));
function renderDeptOptions(filter) {
const items = [{ name: "All Departments", value: "" }]
.concat(allDepartments.map(d => ({ name: d, value: d })));
const filtered = items.filter(o =>
o.value === "" || o.name.toLowerCase().includes(filter));
if (filtered.length === 0) {
deptList.innerHTML = `<div class="inv-dep-empty">No departments found</div>`;
return; return;
} }
deptList.innerHTML = filtered.map(o => `
<div class="inv-dep-opt ${o.value === selectedDept ? "active" : ""}" data-value="${_esc(o.value)}">
<i class="fas ${o.value === "" ? "fa-th" : "fa-building"}"></i>
<span>${_esc(o.name)}</span>
</div>`).join("");
deptList.querySelectorAll(".inv-dep-opt").forEach(el => {
el.addEventListener("click", () => {
selectedDept = el.getAttribute("data-value");
deptLbl.textContent = el.querySelector("span").textContent;
deptDropdown.classList.remove("open");
deptTrigger.classList.remove("open");
currentPage = 1; // reset to first page on filter change
fetchAndRender();
});
});
}
// default: first day of current month -> today // default: first day of current month -> today
const today = new Date(); const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1); const first = new Date(today.getFullYear(), today.getMonth(), 1);
const deptEl = document.getElementById("inv-rpt-dept");
toEl.value = today.toISOString().slice(0, 10); toEl.value = today.toISOString().slice(0, 10);
fromEl.value = first.toISOString().slice(0, 10); fromEl.value = first.toISOString().slice(0, 10);
let lastData = null; // cache for export let currentPage = 1;
let pageSize = 10;
function buildParams() { let lastData = null; // paged data (for display)
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
}
csvBtn.addEventListener("click", exportCsv); csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender); genBtn.addEventListener("click", () => { currentPage = 1; fetchAndRender(); });
async function fetchAndRender() { async function fetchAndRender() {
container.innerHTML = `<div class="inv-tab-loading"> container.innerHTML = `<div class="inv-tab-loading"><div class="inv-spinner"></div><span>Loading report…</span></div>`;
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
try { try {
const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`); const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
lastData = json.data ?? json; lastData = json.data ?? json;
// populate dropdown from SP's department result set (eager-loaded)
if (Array.isArray(lastData.departments)) {
allDepartments = lastData.departments;
renderDeptOptions("");
}
renderReport(lastData); renderReport(lastData);
renderPager(lastData);
} catch (err) { } catch (err) {
console.error("Inventory report fetch error:", err); console.error("Inventory report fetch error:", err);
container.innerHTML = ` 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>`;
<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 renderPager(data) {
const total = data.totalRows ?? 0;
if (total === 0) return;
const page = data.page ?? 1;
const size = showAll ? total : (data.pageSize ?? pageSize);
const pages = showAll ? 1 : Math.max(1, Math.ceil(total / size));
const from = showAll ? 1 : (page - 1) * size + 1;
const to = showAll ? total : Math.min(page * size, total);
// build a compact page-number window (max 5 numbers)
let nums = [];
const win = 2;
for (let i = Math.max(1, page - win); i <= Math.min(pages, page + win); i++) nums.push(i);
const pager = document.createElement("div");
pager.className = "inv-pagination no-print";
pager.innerHTML = `
<div class="inv-pg-left">
<span class="inv-pgsz-lbl">Show</span>
<select class="inv-pgsz-sel" id="inv-pgsz-sel">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">All</option>
</select>
<span class="inv-pg-info">Showing <b>${from}${to}</b> of <b>${total.toLocaleString()}</b> items</span>
</div>
<div class="inv-pg-btns">
<button class="inv-pg-btn" data-pg="${page - 1}" ${page <= 1 ? "disabled" : ""}>
<i class="fas fa-chevron-left"></i>
</button>
${nums[0] > 1 ? `<button class="inv-pg-btn" data-pg="1">1</button>${nums[0] > 2 ? `<span class="inv-pg-ellipsis">…</span>` : ""}` : ""}
${nums.map(n => `<button class="inv-pg-btn ${n === page ? "active" : ""}" data-pg="${n}">${n}</button>`).join("")}
${nums[nums.length-1] < pages ? `${nums[nums.length-1] < pages-1 ? `<span class="inv-pg-ellipsis">…</span>` : ""}<button class="inv-pg-btn" data-pg="${pages}">${pages}</button>` : ""}
<button class="inv-pg-btn" data-pg="${page + 1}" ${page >= pages ? "disabled" : ""}>
<i class="fas fa-chevron-right"></i>
</button>
</div>`;
container.appendChild(pager);
pager.querySelectorAll(".inv-pg-btn").forEach(btn => {
btn.addEventListener("click", () => {
const target = parseInt(btn.getAttribute("data-pg"), 10);
if (!isNaN(target) && target >= 1 && target <= pages && target !== page) {
currentPage = target;
fetchAndRender();
container.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
});
// page-size selector — must be inside renderPager so `pager` is in scope
const pgszSel = pager.querySelector("#inv-pgsz-sel");
if (pgszSel) {
pgszSel.value = showAll ? "all" : String(pageSize);
pgszSel.addEventListener("change", () => {
if (pgszSel.value === "all") {
showAll = true;
} else {
showAll = false;
pageSize = parseInt(pgszSel.value, 10) || 50;
}
currentPage = 1;
fetchAndRender();
});
}
}
function buildParams(extra = {}) {
const p = new URLSearchParams({
dateFrom: fromEl.value,
dateTo: toEl.value,
page: currentPage,
pageSize: pageSize,
paginate: !showAll
});
if (selectedDept) p.append("department", selectedDept);
Object.entries(extra).forEach(([k, v]) => p.set(k, v));
return p;
}
const printBtn = document.getElementById("inv-rpt-print");
printBtn?.addEventListener("click", async () => {
// 1. get the full dataset (all 145 rows)
const full = await fetchFullForExport();
renderReport(full);
// wait for the DOM to paint the re-rendered report
setTimeout(() => {
const report = document.getElementById("inv-rpt-page");
if (!report) { window.print(); return; }
// 2. clone the report to a body-level mount
let mount = document.getElementById("print-mount");
if (!mount) {
mount = document.createElement("div");
mount.id = "print-mount";
document.body.appendChild(mount);
}
mount.innerHTML = "";
mount.appendChild(report.cloneNode(true));
// 3. print, then clean up + restore paged view
const cleanup = () => {
mount.remove();
fetchAndRender();
window.removeEventListener("afterprint", cleanup);
};
window.addEventListener("afterprint", cleanup);
window.print();
}, 200);
});
async function fetchFullForExport() {
const params = buildParams({ paginate: false });
params.delete("page");
params.delete("pageSize");
const res = await fetch(`/InventoryReports/GetInventoryReport?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
return json.data ?? json;
}
// ── CSV export (client-side, no backend needed) ── // ── CSV export (client-side, no backend needed) ──
function exportCsv() { async function exportCsv() {
if (!lastData) return; const data = await fetchFullForExport();
const rows = lastData.rows ?? []; if (!data) return;
const byCat = lastData.byCategory ?? []; const rows = data.rows ?? [];
const alerts = lastData.alerts ?? []; const byCat = data.byCategory ?? [];
const summary = lastData.summary ?? {}; const alerts = data.alerts ?? [];
const headers = ["Item Name","Item No.","Category","Lot No.","Qty In","Qty Out","On Hand","Stock %"]; const summary = data.summary ?? {};
const headers = ["Item Name / UOM","Item No.","Category","Department","Lot No","Qty In","Qty Out","On Hand","Stock %"];
const csvLines = [ const csvLines = [
`Inventory Summary Report`, `Inventory Summary Report`,
`Company,${csvCell(lastData.companyName)}`, `Company,${csvCell(data.companyName)}`,
`Report No,${csvCell(lastData.reportNo)}`, `Report No,${csvCell(data.reportNo)}`,
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`, `Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
``, ``,
// ── Inventory detail ──
headers.map(csvCell).join(","), headers.map(csvCell).join(","),
...rows.map(r => [ ...rows.map(r => [
r.itemName, r.itemNo, r.itemCategoryName, r.lotNo, r.itemName, r.itemNo, r.itemCategoryName, r.department, r.lotNo,r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct
r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct
].map(csvCell).join(",")), ].map(csvCell).join(",")),
["Total","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, ""].map(csvCell).join(","), ["Total","","","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, ""].map(csvCell).join(","),
``, ``,
// ── Stock level by category ──
`Stock Level by Category`, `Stock Level by Category`,
["Category","Avg Stock %"].map(csvCell).join(","), ["Category","Avg Stock %"].map(csvCell).join(","),
...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")), ...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")),
``, ``,
// ── Items requiring attention ──
`Items Requiring Attention`, `Items Requiring Attention`,
["Item","On Hand","Alert"].map(csvCell).join(","), ["Item","On Hand","Alert"].map(csvCell).join(","),
...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(",")) ...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(","))
@ -468,10 +668,7 @@
"text/csv;charset=utf-8;" "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) { function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime }); const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@ -530,6 +727,7 @@
<div class="kpi-lbl"><i class="fas fa-layer-group"></i> Total on hand</div> <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 class="kpi-val">${(summary.totalOnHand ?? 0).toLocaleString()}</div>
</div> </div>
<div class="kpi-cell"> <div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-exclamation-triangle"></i> Low stock</div> <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 class="kpi-val" style="color:#A32D2D">${summary.lowStockCount ?? 0}</div>
@ -545,9 +743,14 @@
<table class="rpt-table"> <table class="rpt-table">
<thead> <thead>
<tr> <tr>
<th>Item Name</th><th>Item No.</th><th>Category</th><th>Lot No.</th> <th>Item Name / UOM</th>
<th>Item No.</th>
<th>Category</th>
<th>Department</th>
<th class="num">Lot No</th>
<th class="num">Qty In</th><th class="num">Qty Out</th> <th class="num">Qty In</th><th class="num">Qty Out</th>
<th class="num">On Hand</th><th>Stock Level</th> <th class="num">On Hand</th>
<th>Stock Level</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -556,14 +759,15 @@
<td style="font-weight:600">${_esc(r.itemName)}</td> <td style="font-weight:600">${_esc(r.itemName)}</td>
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td> <td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td> <td>${_esc(r.itemCategoryName)}</td>
<td style="color:var(--text-muted,#6b8890)">${_esc(r.lotNo)}</td> <td>${_esc(r.department ?? "—")}</td>
<td class="num">${r.lotNo}</td>
<td class="num">${r.qtyIn}</td> <td class="num">${r.qtyIn}</td>
<td class="num">${r.qtyOut}</td> <td class="num">${r.qtyOut}</td>
<td class="num" style="font-weight:600">${r.qtyOnHand}</td> <td class="num" style="font-weight:600">${r.qtyOnHand}</td>
<td>${_stockBar(r.stockPct)}</td> <td>${_stockBar(r.stockPct)}</td>
</tr>`).join("")} </tr>`).join("")}
<tr class="total-row"> <tr class="total-row">
<td colspan="4">Total</td> <td colspan="5">Total</td>
<td class="num">${summary.totalQtyIn ?? 0}</td> <td class="num">${summary.totalQtyIn ?? 0}</td>
<td class="num">${summary.totalQtyOut ?? 0}</td> <td class="num">${summary.totalQtyOut ?? 0}</td>
<td class="num">${summary.totalOnHand ?? 0}</td> <td class="num">${summary.totalOnHand ?? 0}</td>
@ -639,14 +843,15 @@
} }
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ── // ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
function exportExcel() { async function exportExcel() {
if (!lastData) return; const data = await fetchFullForExport();
const rows = lastData.rows ?? []; if (!data) return;
const byCat = lastData.byCategory ?? []; const rows = data.rows ?? [];
const alerts = lastData.alerts ?? []; const byCat = data.byCategory ?? [];
const summary = lastData.summary ?? {}; const alerts = data.alerts ?? [];
const summary = data.summary ?? {};
const headerCells = ["Item Name","Item No.","Category","Lot No.","Qty In","Qty Out","On Hand","Stock %"] const headerCells = ["Item Name / UOM","Item No.","Category","Department","Lot No","Qty In","Qty Out","On Hand","Stock %"]
.map(h => `<th>${_esc(h)}</th>`).join(""); .map(h => `<th>${_esc(h)}</th>`).join("");
const bodyRows = rows.map(r => ` const bodyRows = rows.map(r => `
@ -654,7 +859,8 @@
<td>${_esc(r.itemName)}</td> <td>${_esc(r.itemName)}</td>
<td>${_esc(r.itemNo)}</td> <td>${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td> <td>${_esc(r.itemCategoryName)}</td>
<td>${_esc(r.lotNo)}</td> <td>${_esc(r.department ?? "")}</td>
<td>${_esc(r.lotNo ?? "")}</td>
<td>${r.qtyIn}</td> <td>${r.qtyIn}</td>
<td>${r.qtyOut}</td> <td>${r.qtyOut}</td>
<td>${r.qtyOnHand}</td> <td>${r.qtyOnHand}</td>
@ -674,23 +880,27 @@
<head><meta charset="utf-8"></head> <head><meta charset="utf-8"></head>
<body> <body>
<table border="1"> <table border="1">
<tr><td colspan="8"><b>Inventory Summary Report</b></td></tr> <tr><td colspan="9"><b>Inventory Summary Report</b></td></tr>
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr> <tr><td>Company</td><td colspan="8">${_esc(data.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr> <tr><td>Report No</td><td colspan="8">${_esc(data.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr> <tr><td>Period</td><td colspan="8">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr></tr> <tr></tr>
<tr>${headerCells}</tr> <tr>${headerCells}</tr>
${bodyRows} ${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> <tr>
<td colspan="5"><b>Total</b></td>
<td>${summary.totalQtyIn ?? 0}</td>
<td>${summary.totalQtyOut ?? 0}</td>
<td>${summary.totalOnHand ?? 0}</td>
<td></td>
</tr>
</table> </table>
<br/> <br/>
<table border="1"> <table border="1">
<tr><td colspan="2"><b>Stock Level by Category</b></td></tr> <tr><td colspan="2"><b>Stock Level by Category</b></td></tr>
<tr><th>Category</th><th>Avg Stock %</th></tr> <tr><th>Category</th><th>Avg Stock %</th></tr>
${catRows} ${catRows}
</table> </table>
<br/> <br/>
<table border="1"> <table border="1">
<tr><td colspan="3"><b>Items Requiring Attention</b></td></tr> <tr><td colspan="3"><b>Items Requiring Attention</b></td></tr>

View File

@ -5,36 +5,31 @@
color: #1a2e35; color: #1a2e35;
padding: 24px; padding: 24px;
} }
@@media print { @@media print {
body { padding: 0; margin: 0; background: #fff;} body > *:not(#print-mount) {
body * { visibility: hidden;} display: none !important;
#inv-rpt-page, #inv-rpt-page * {
visibility: visible;
} }
#print-mount {
#inv-rpt-page { display: block !important;
position: absolute; position: static !important;
left: 0;
top: 0;
width: 100%; width: 100%;
}
#print-mount .rpt-page {
box-shadow: none !important; box-shadow: none !important;
border: none !important; border: none !important;
border-radius: 0 !important; border-radius: 0 !important;
overflow: visible !important;
} }
.rpt-section { page-break-inside: auto; }
.no-print { .rpt-table { table-layout: fixed; width: 100%; font-size: 9px; page-break-inside: auto; }
display: none !important; .rpt-table th, .rpt-table td { padding: 5px 6px; white-space: normal; word-break: break-word; }
.rpt-table tr { page-break-inside: avoid; }
thead { display: table-header-group; }
@@page {
size: A4 landscape;
margin: 5mm;
} }
.rpt-table tr {
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
} }
.rpt-page { .rpt-page {
@ -387,76 +382,258 @@
const xlsxBtn = document.getElementById("inv-rpt-excel"); const xlsxBtn = document.getElementById("inv-rpt-excel");
const container = document.getElementById("inv-rpt-container"); const container = document.getElementById("inv-rpt-container");
if (!fromEl || !toEl || !container) { // Department
console.error("Inventory report subtab init failed — missing elements."); const deptWrap = document.getElementById("inv-rpt-dept-wrap");
const deptTrigger = document.getElementById("inv-rpt-dept-trigger");
const deptDropdown= document.getElementById("inv-rpt-dept-dropdown");
const deptList = document.getElementById("inv-rpt-dept-list");
const deptLbl = document.getElementById("inv-rpt-dept-lbl");
const deptSearch = document.getElementById("inv-rpt-dept-search");
let showAll = false;
let selectedDept = ""; // "" = All
let allDepartments = []; // [{ name }]
// open/close
deptTrigger.addEventListener("click", () => {
const open = deptDropdown.classList.toggle("open");
deptTrigger.classList.toggle("open", open);
if (open) { deptSearch.value = ""; renderDeptOptions(""); deptSearch.focus(); }
});
// close on outside click
document.addEventListener("click", (e) => {
if (!deptWrap.contains(e.target)) {
deptDropdown.classList.remove("open");
deptTrigger.classList.remove("open");
}
});
// live filter
deptSearch.addEventListener("input", () => renderDeptOptions(deptSearch.value.trim().toLowerCase()));
function renderDeptOptions(filter) {
const items = [{ name: "All Departments", value: "" }]
.concat(allDepartments.map(d => ({ name: d, value: d })));
const filtered = items.filter(o =>
o.value === "" || o.name.toLowerCase().includes(filter));
if (filtered.length === 0) {
deptList.innerHTML = `<div class="inv-dep-empty">No departments found</div>`;
return; return;
} }
deptList.innerHTML = filtered.map(o => `
<div class="inv-dep-opt ${o.value === selectedDept ? "active" : ""}" data-value="${_esc(o.value)}">
<i class="fas ${o.value === "" ? "fa-th" : "fa-building"}"></i>
<span>${_esc(o.name)}</span>
</div>`).join("");
deptList.querySelectorAll(".inv-dep-opt").forEach(el => {
el.addEventListener("click", () => {
selectedDept = el.getAttribute("data-value");
deptLbl.textContent = el.querySelector("span").textContent;
deptDropdown.classList.remove("open");
deptTrigger.classList.remove("open");
currentPage = 1; // reset to first page on filter change
fetchAndRender();
});
});
}
// default: first day of current month -> today // default: first day of current month -> today
const today = new Date(); const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1); const first = new Date(today.getFullYear(), today.getMonth(), 1);
const deptEl = document.getElementById("inv-rpt-dept");
toEl.value = today.toISOString().slice(0, 10); toEl.value = today.toISOString().slice(0, 10);
fromEl.value = first.toISOString().slice(0, 10); fromEl.value = first.toISOString().slice(0, 10);
let lastData = null; // cache for export let currentPage = 1;
let pageSize = 10;
function buildParams() { let lastData = null; // paged data (for display)
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
}
csvBtn.addEventListener("click", exportCsv); csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender); genBtn.addEventListener("click", () => { currentPage = 1; fetchAndRender(); });
async function fetchAndRender() { async function fetchAndRender() {
container.innerHTML = `<div class="inv-tab-loading"> container.innerHTML = `<div class="inv-tab-loading"><div class="inv-spinner"></div><span>Loading report…</span></div>`;
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
try { try {
const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`); const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
lastData = json.data ?? json; lastData = json.data ?? json;
// populate dropdown from SP's department result set (eager-loaded)
if (Array.isArray(lastData.departments)) {
allDepartments = lastData.departments;
renderDeptOptions("");
}
renderReport(lastData); renderReport(lastData);
renderPager(lastData);
} catch (err) { } catch (err) {
console.error("Inventory report fetch error:", err); console.error("Inventory report fetch error:", err);
container.innerHTML = ` 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>`;
<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 renderPager(data) {
const total = data.totalRows ?? 0;
if (total === 0) return;
const page = data.page ?? 1;
const size = showAll ? total : (data.pageSize ?? pageSize);
const pages = showAll ? 1 : Math.max(1, Math.ceil(total / size));
const from = showAll ? 1 : (page - 1) * size + 1;
const to = showAll ? total : Math.min(page * size, total);
// build a compact page-number window (max 5 numbers)
let nums = [];
const win = 2;
for (let i = Math.max(1, page - win); i <= Math.min(pages, page + win); i++) nums.push(i);
const pager = document.createElement("div");
pager.className = "inv-pagination no-print";
pager.innerHTML = `
<div class="inv-pg-left">
<span class="inv-pgsz-lbl">Show</span>
<select class="inv-pgsz-sel" id="inv-pgsz-sel">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">All</option>
</select>
<span class="inv-pg-info">Showing <b>${from}${to}</b> of <b>${total.toLocaleString()}</b> items</span>
</div>
<div class="inv-pg-btns">
<button class="inv-pg-btn" data-pg="${page - 1}" ${page <= 1 ? "disabled" : ""}>
<i class="fas fa-chevron-left"></i>
</button>
${nums[0] > 1 ? `<button class="inv-pg-btn" data-pg="1">1</button>${nums[0] > 2 ? `<span class="inv-pg-ellipsis">…</span>` : ""}` : ""}
${nums.map(n => `<button class="inv-pg-btn ${n === page ? "active" : ""}" data-pg="${n}">${n}</button>`).join("")}
${nums[nums.length-1] < pages ? `${nums[nums.length-1] < pages-1 ? `<span class="inv-pg-ellipsis">…</span>` : ""}<button class="inv-pg-btn" data-pg="${pages}">${pages}</button>` : ""}
<button class="inv-pg-btn" data-pg="${page + 1}" ${page >= pages ? "disabled" : ""}>
<i class="fas fa-chevron-right"></i>
</button>
</div>`;
container.appendChild(pager);
pager.querySelectorAll(".inv-pg-btn").forEach(btn => {
btn.addEventListener("click", () => {
const target = parseInt(btn.getAttribute("data-pg"), 10);
if (!isNaN(target) && target >= 1 && target <= pages && target !== page) {
currentPage = target;
fetchAndRender();
container.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
});
// page-size selector — must be inside renderPager so `pager` is in scope
const pgszSel = pager.querySelector("#inv-pgsz-sel");
if (pgszSel) {
pgszSel.value = showAll ? "all" : String(pageSize);
pgszSel.addEventListener("change", () => {
if (pgszSel.value === "all") {
showAll = true;
} else {
showAll = false;
pageSize = parseInt(pgszSel.value, 10) || 50;
}
currentPage = 1;
fetchAndRender();
});
}
}
function buildParams(extra = {}) {
const p = new URLSearchParams({
dateFrom: fromEl.value,
dateTo: toEl.value,
page: currentPage,
pageSize: pageSize,
paginate: !showAll
});
if (selectedDept) p.append("department", selectedDept);
Object.entries(extra).forEach(([k, v]) => p.set(k, v));
return p;
}
const printBtn = document.getElementById("inv-rpt-print");
printBtn?.addEventListener("click", async () => {
// 1. get the full dataset (all 145 rows)
const full = await fetchFullForExport();
renderReport(full);
// wait for the DOM to paint the re-rendered report
setTimeout(() => {
const report = document.getElementById("inv-rpt-page");
if (!report) { window.print(); return; }
// 2. clone the report to a body-level mount
let mount = document.getElementById("print-mount");
if (!mount) {
mount = document.createElement("div");
mount.id = "print-mount";
document.body.appendChild(mount);
}
mount.innerHTML = "";
mount.appendChild(report.cloneNode(true));
// 3. print, then clean up + restore paged view
const cleanup = () => {
mount.remove();
fetchAndRender();
window.removeEventListener("afterprint", cleanup);
};
window.addEventListener("afterprint", cleanup);
window.print();
}, 200);
});
async function fetchFullForExport() {
const params = buildParams({ paginate: false });
params.delete("page");
params.delete("pageSize");
const res = await fetch(`/InventoryReports/GetInventoryReport?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
return json.data ?? json;
}
// ── CSV export (client-side, no backend needed) ── // ── CSV export (client-side, no backend needed) ──
function exportCsv() { async function exportCsv() {
if (!lastData) return; const data = await fetchFullForExport();
const rows = lastData.rows ?? []; if (!data) return;
const byCat = lastData.byCategory ?? []; const rows = data.rows ?? [];
const alerts = lastData.alerts ?? []; const byCat = data.byCategory ?? [];
const summary = lastData.summary ?? {}; const alerts = data.alerts ?? [];
const headers = ["Item Name","Item No.","Category","Unit Price","Qty In","Qty Out","On Hand","Stock %"]; const summary = data.summary ?? {};
const headers = ["Item Name / UOM","Item No.","Category","Department","Currency","Unit Price","Qty In","Qty Out","On Hand","Total Value","Stock %"];
const csvLines = [ const csvLines = [
`Inventory Summary Report`, `Inventory Summary Report`,
`Company,${csvCell(lastData.companyName)}`, `Company,${csvCell(data.companyName)}`,
`Report No,${csvCell(lastData.reportNo)}`, `Report No,${csvCell(data.reportNo)}`,
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`, `Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
``, ``,
// ── Inventory detail ──
headers.map(csvCell).join(","), headers.map(csvCell).join(","),
...rows.map(r => [ ...rows.map(r => [
r.itemName, r.itemNo, r.itemCategoryName, r.unitPrice, r.itemName, r.itemNo, r.itemCategoryName, r.department, r.currencyCode,
r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct r.unitPrice, r.qtyIn, r.qtyOut, r.qtyOnHand,
(Number(r.qtyOnHand) || 0) * (Number(r.unitPrice) || 0), r.stockPct
].map(csvCell).join(",")), ].map(csvCell).join(",")),
["Total","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, ""].map(csvCell).join(","), ["Total","","","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, summary.totalValue ?? 0, ""].map(csvCell).join(","),
``, ``,
// ── Stock level by category ──
`Stock Level by Category`, `Stock Level by Category`,
["Category","Avg Stock %"].map(csvCell).join(","), ["Category","Avg Stock %"].map(csvCell).join(","),
...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")), ...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")),
``, ``,
// ── Items requiring attention ──
`Items Requiring Attention`, `Items Requiring Attention`,
["Item","On Hand","Alert"].map(csvCell).join(","), ["Item","On Hand","Alert"].map(csvCell).join(","),
...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(",")) ...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(","))
@ -468,10 +645,7 @@
"text/csv;charset=utf-8;" "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) { function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime }); const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@ -549,7 +723,9 @@
<table class="rpt-table"> <table class="rpt-table">
<thead> <thead>
<tr> <tr>
<th>Item Name</th><th>Item No.</th><th>Category</th> <th>Item Name / UOM</th>
<th>Item No.</th><th>Category</th>
<th>Department</th>
<th class="num">Unit Price</th> <th class="num">Unit Price</th>
<th class="num">Qty In</th><th class="num">Qty Out</th> <th class="num">Qty In</th><th class="num">Qty Out</th>
<th class="num">On Hand</th> <th class="num">On Hand</th>
@ -563,15 +739,16 @@
<td style="font-weight:600">${_esc(r.itemName)}</td> <td style="font-weight:600">${_esc(r.itemName)}</td>
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td> <td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td> <td>${_esc(r.itemCategoryName)}</td>
<td class="num">${_money(r.unitPrice)}</td> <td>${_esc(r.department ?? "—")}</td>
<td class="num">${_money(r.unitPrice, r.currencyCode)}</td>
<td class="num">${r.qtyIn}</td> <td class="num">${r.qtyIn}</td>
<td class="num">${r.qtyOut}</td> <td class="num">${r.qtyOut}</td>
<td class="num" style="font-weight:600">${r.qtyOnHand}</td> <td class="num" style="font-weight:600">${r.qtyOnHand}</td>
<td class="num">${_money(r.qtyOnHand * r.unitPrice)}</td> <td class="num">${_money(r.qtyOnHand * r.unitPrice, r.currencyCode)}</td>
<td>${_stockBar(r.stockPct)}</td> <td>${_stockBar(r.stockPct)}</td>
</tr>`).join("")} </tr>`).join("")}
<tr class="total-row"> <tr class="total-row">
<td colspan="4">Total</td> <td colspan="5">Total</td>
<td class="num">${summary.totalQtyIn ?? 0}</td> <td class="num">${summary.totalQtyIn ?? 0}</td>
<td class="num">${summary.totalQtyOut ?? 0}</td> <td class="num">${summary.totalQtyOut ?? 0}</td>
<td class="num">${summary.totalOnHand ?? 0}</td> <td class="num">${summary.totalOnHand ?? 0}</td>
@ -628,13 +805,13 @@
</div> </div>
</div>`; </div>`;
} }
function _money(v) { function _money(v, currency) {
const n = Number(v) || 0; const n = Number(v) || 0;
return n.toLocaleString("en-PH", { const amount = n.toLocaleString("en-PH", {
style: "currency", minimumFractionDigits: 2,
currency: "PHP", maximumFractionDigits: 2
minimumFractionDigits: 2
}); });
return currency ? `${currency} ${amount}` : amount;
} }
function _stockBar(pct) { function _stockBar(pct) {
const cls = pct < 20 ? "fill-coral" : pct < 50 ? "fill-amber" : "fill-teal"; const cls = pct < 20 ? "fill-coral" : pct < 50 ? "fill-amber" : "fill-teal";
@ -655,14 +832,15 @@
} }
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ── // ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
function exportExcel() { async function exportExcel() {
if (!lastData) return; const data = await fetchFullForExport();
const rows = lastData.rows ?? []; if (!data) return;
const byCat = lastData.byCategory ?? []; const rows = data.rows ?? [];
const alerts = lastData.alerts ?? []; const byCat = data.byCategory ?? [];
const summary = lastData.summary ?? {}; const alerts = data.alerts ?? [];
const summary = data.summary ?? {};
const headerCells = ["Item Name","Item No.","Category","Unit Price","Qty In","Qty Out","On Hand","Stock %"] const headerCells = ["Item Name / UOM","Item No.","Category","Department","Currency","Unit Price","Qty In","Qty Out","On Hand","Total Value","Stock %"]
.map(h => `<th>${_esc(h)}</th>`).join(""); .map(h => `<th>${_esc(h)}</th>`).join("");
const bodyRows = rows.map(r => ` const bodyRows = rows.map(r => `
@ -670,10 +848,13 @@
<td>${_esc(r.itemName)}</td> <td>${_esc(r.itemName)}</td>
<td>${_esc(r.itemNo)}</td> <td>${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td> <td>${_esc(r.itemCategoryName)}</td>
<td>${_esc(r.unitPrice)}</td> <td>${_esc(r.department ?? "")}</td>
<td>${_esc(r.currencyCode ?? "")}</td>
<td>${r.unitPrice}</td>
<td>${r.qtyIn}</td> <td>${r.qtyIn}</td>
<td>${r.qtyOut}</td> <td>${r.qtyOut}</td>
<td>${r.qtyOnHand}</td> <td>${r.qtyOnHand}</td>
<td>${(Number(r.qtyOnHand)||0) * (Number(r.unitPrice)||0)}</td>
<td>${r.stockPct}</td> <td>${r.stockPct}</td>
</tr>`).join(""); </tr>`).join("");
@ -690,14 +871,14 @@
<head><meta charset="utf-8"></head> <head><meta charset="utf-8"></head>
<body> <body>
<table border="1"> <table border="1">
<tr><td colspan="8"><b>Inventory Summary Report</b></td></tr> <tr><td colspan="11"><b>Inventory Summary Report</b></td></tr>
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr> <tr><td>Company</td><td colspan="10">${_esc(data.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr> <tr><td>Report No</td><td colspan="10">${_esc(data.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr> <tr><td>Period</td><td colspan="10">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr></tr> <tr></tr>
<tr>${headerCells}</tr> <tr>${headerCells}</tr>
${bodyRows} ${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> <tr><td colspan="6"><b>Total</b></td><td>${summary.totalQtyIn ?? 0}</td><td>${summary.totalQtyOut ?? 0}</td><td>${summary.totalOnHand ?? 0}</td><td>${summary.totalValue ?? 0}</td><td></td></tr>
</table> </table>
<br/> <br/>

View File

@ -7,32 +7,38 @@
} }
@@media print { @@media print {
body { body > *:not(#print-mount) {
padding: 0; display: none !important;
margin: 0;
background: #fff;
} }
body * { #print-mount {
visibility: hidden; display: block !important;
} position: static !important;
#mrs-rpt-page, #mrs-rpt-page * {
visibility: visible;
}
#mrs-rpt-page {
position: absolute;
left: 0;
top: 0;
width: 100%; width: 100%;
}
#print-mount .rpt-page {
box-shadow: none !important; box-shadow: none !important;
border: none !important; border: none !important;
border-radius: 0 !important; border-radius: 0 !important;
overflow: visible !important;
} }
.no-print { .rpt-section {
display: none !important; page-break-inside: auto;
}
.rpt-table {
table-layout: fixed;
width: 100%;
font-size: 9px;
page-break-inside: auto;
}
.rpt-table th, .rpt-table td {
padding: 5px 6px;
white-space: normal;
word-break: break-word;
} }
.rpt-table tr { .rpt-table tr {
@ -42,6 +48,11 @@
thead { thead {
display: table-header-group; display: table-header-group;
} }
@@page {
size: A4 landscape;
margin: 5mm;
}
} }
.rpt-page { .rpt-page {
@ -197,7 +208,7 @@
.kpi-strip { .kpi-strip {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 0; gap: 0;
border-bottom: 1px solid #d6eaec; border-bottom: 1px solid #d6eaec;
} }
@ -584,9 +595,53 @@
return `<span class="pill ${cls}"><i class="fas ${icon}"></i> ${_esc(label)}</span>`; return `<span class="pill ${cls}"><i class="fas ${icon}"></i> ${_esc(label)}</span>`;
} }
const printBtn = document.getElementById("inv-rpt-print");
printBtn?.addEventListener("click", async () => {
// 1. get the full dataset (all 145 rows)
const full = await fetchFullForExport();
renderReport(full);
// wait for the DOM to paint the re-rendered report
setTimeout(() => {
const report = document.getElementById("mrs-rpt-page");
if (!report) { window.print(); return; }
// 2. clone the report to a body-level mount
let mount = document.getElementById("print-mount");
if (!mount) {
mount = document.createElement("div");
mount.id = "print-mount";
document.body.appendChild(mount);
}
mount.innerHTML = "";
mount.appendChild(report.cloneNode(true));
// 3. print, then clean up + restore paged view
const cleanup = () => {
mount.remove();
fetchAndRender();
window.removeEventListener("afterprint", cleanup);
};
window.addEventListener("afterprint", cleanup);
window.print();
}, 200);
});
async function fetchFullForExport() {
const params = buildParams({ paginate: false });
params.delete("page");
params.delete("pageSize");
const res = await fetch(`/InventoryReports/GetMRSReport?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
return json.data ?? json;
}
// ── CSV export (MRS schema) ── // ── CSV export (MRS schema) ──
function exportCsv() { async function exportCsv() {
const data = await fetchFullForExport();
if (!lastData) return; if (!lastData) return;
const rows = lastData.rows ?? []; const rows = lastData.rows ?? [];
const byCond = lastData.byCondition ?? []; const byCond = lastData.byCondition ?? [];
@ -626,7 +681,7 @@
} }
// ── Excel export (MRS schema, HTML-table .xls) ── // ── Excel export (MRS schema, HTML-table .xls) ──
function exportExcel() { async function exportExcel() {
if (!lastData) return; if (!lastData) return;
const rows = lastData.rows ?? []; const rows = lastData.rows ?? [];
const byCond = lastData.byCondition ?? []; const byCond = lastData.byCondition ?? [];

View File

@ -7,32 +7,38 @@
} }
@@media print { @@media print {
body { body > *:not(#print-mount) {
padding: 0; display: none !important;
margin: 0;
background: #fff;
} }
body * { #print-mount {
visibility: hidden; display: block !important;
} position: static !important;
#ris-rpt-page, #ris-rpt-page * {
visibility: visible;
}
#ris-rpt-page {
position: absolute;
left: 0;
top: 0;
width: 100%; width: 100%;
}
#print-mount .ris-page {
box-shadow: none !important; box-shadow: none !important;
border: none !important; border: none !important;
border-radius: 0 !important; border-radius: 0 !important;
overflow: visible !important;
} }
.no-print { .rpt-section {
display: none !important; page-break-inside: auto;
}
.rpt-table {
table-layout: fixed;
width: 100%;
font-size: 9px;
page-break-inside: auto;
}
.rpt-table th, .rpt-table td {
padding: 5px 6px;
white-space: normal;
word-break: break-word;
} }
.rpt-table tr { .rpt-table tr {
@ -42,6 +48,11 @@
thead { thead {
display: table-header-group; display: table-header-group;
} }
@@page {
size: A4 landscape;
margin: 5mm;
}
} }
.rpt-page { .rpt-page {
@ -197,7 +208,7 @@
.kpi-strip { .kpi-strip {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 0; gap: 0;
border-bottom: 1px solid #d6eaec; border-bottom: 1px solid #d6eaec;
} }
@ -434,14 +445,61 @@
} }
csvBtn.addEventListener("click", exportCsv); csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender); genBtn.addEventListener("click", fetchAndRender);
const printBtn = document.getElementById("inv-rpt-print");
printBtn?.addEventListener("click", async () => {
// 1. get the full dataset (all 145 rows)
const full = await fetchFullForExport();
renderReport(full);
// wait for the DOM to paint the re-rendered report
setTimeout(() => {
const report = document.getElementById("ris-rpt-page");
if (!report) { window.print(); return; }
// 2. clone the report to a body-level mount
let mount = document.getElementById("print-mount");
if (!mount) {
mount = document.createElement("div");
mount.id = "print-mount";
document.body.appendChild(mount);
}
mount.innerHTML = "";
mount.appendChild(report.cloneNode(true));
// 3. print, then clean up + restore paged view
const cleanup = () => {
mount.remove();
fetchAndRender();
window.removeEventListener("afterprint", cleanup);
};
window.addEventListener("afterprint", cleanup);
window.print();
}, 200);
});
async function fetchFullForExport() {
const params = buildParams({ paginate: false });
params.delete("page");
params.delete("pageSize");
const res = await fetch(`/InventoryReports/GetRISReport?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
return json.data ?? json;
}
// ── CSV export (client-side, no backend needed) ── // ── CSV export (client-side, no backend needed) ──
function exportCsv() { async function exportCsv() {
const data = await fetchFullForExport();
if (!lastData) return; if (!lastData) return;
const rows = lastData.rows ?? []; const rows = lastData.rows ?? [];
const byDisc = lastData.byDiscipline ?? []; const byDisc = lastData.byDiscipline ?? [];
const topRecv = lastData.topRecipients ?? []; const topRecv = lastData.topRecipients ?? [];
const summary = lastData.summary ?? {}; const summary = lastData.summary ?? {};
const headers = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"]; const headers = ["RIS No.","Date","Item","Item No.","Trade","Issued To","Qty Issued","Qty Returned","Net Out","Status"];
const csvLines = [ const csvLines = [
`Return Issuance Slip Report`, `Return Issuance Slip Report`,
@ -456,8 +514,8 @@
].map(csvCell).join(",")), ].map(csvCell).join(",")),
["Total","","","","","", summary.totalQtyIssued ?? 0, summary.totalQtyReturned ?? 0, summary.totalNetIssued ?? 0, ""].map(csvCell).join(","), ["Total","","","","","", summary.totalQtyIssued ?? 0, summary.totalQtyReturned ?? 0, summary.totalNetIssued ?? 0, ""].map(csvCell).join(","),
``, ``,
`Issuance by Discipline`, `Issuance by Trade`,
["Discipline","Slips"].map(csvCell).join(","), ["Trade","Slips"].map(csvCell).join(","),
...byDisc.map(d => [d.disciplineName, d.count].map(csvCell).join(",")), ...byDisc.map(d => [d.disciplineName, d.count].map(csvCell).join(",")),
``, ``,
`Top Recipients`, `Top Recipients`,
@ -501,7 +559,7 @@
<div class="rpt-company">${_esc(data.companyName ?? "")}</div> <div class="rpt-company">${_esc(data.companyName ?? "")}</div>
<div class="rpt-title">Return Issuance Slip Report</div> <div class="rpt-title">Return Issuance Slip Report</div>
<div class="rpt-subtitle"> <div class="rpt-subtitle">
Period: ${_fmtDate(data.dateFrom)} ${_fmtDate(data.dateTo)} · All departments · All disciplines Period: ${_fmtDate(data.dateFrom)} ${_fmtDate(data.dateTo)} · All departments · All trades
</div> </div>
</div> </div>
<div class="rpt-logo"><i class="fas fa-file-export"></i></div> <div class="rpt-logo"><i class="fas fa-file-export"></i></div>
@ -544,7 +602,7 @@
<thead> <thead>
<tr> <tr>
<th>RIS No.</th><th>Date</th><th>Item</th><th>Item No.</th> <th>RIS No.</th><th>Date</th><th>Item</th><th>Item No.</th>
<th>Discipline</th><th>Issued To</th> <th>Trade</th><th>Project Name</th>
<th class="num">Qty Issued</th><th class="num">Qty Returned</th> <th class="num">Qty Issued</th><th class="num">Qty Returned</th>
<th class="num">Net Out</th><th>Status</th> <th class="num">Net Out</th><th>Status</th>
</tr> </tr>
@ -576,7 +634,7 @@
<div class="two-col"> <div class="two-col">
<div class="col-left"> <div class="col-left">
<div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Issuance by discipline</div> <div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Issuance by trade</div>
<div style="display:flex;flex-direction:column;gap:8px"> <div style="display:flex;flex-direction:column;gap:8px">
${byDisc.map((d, i) => ` ${byDisc.map((d, i) => `
<div> <div>
@ -628,14 +686,14 @@
} }
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ── // ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
function exportExcel() { async function exportExcel() {
if (!lastData) return; if (!lastData) return;
const rows = lastData.rows ?? []; const rows = lastData.rows ?? [];
const byDisc = lastData.byDiscipline ?? []; const byDisc = lastData.byDiscipline ?? [];
const topRecv = lastData.topRecipients ?? []; const topRecv = lastData.topRecipients ?? [];
const summary = lastData.summary ?? {}; const summary = lastData.summary ?? {};
const headerCells = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"] const headerCells = ["RIS No.","Date","Item","Item No.","Trade","Issued To","Qty Issued","Qty Returned","Net Out","Status"]
.map(h => `<th>${_esc(h)}</th>`).join(""); .map(h => `<th>${_esc(h)}</th>`).join("");
const bodyRows = rows.map(r => ` const bodyRows = rows.map(r => `
@ -677,8 +735,8 @@
<br/> <br/>
<table border="1"> <table border="1">
<tr><td colspan="2"><b>Issuance by Discipline</b></td></tr> <tr><td colspan="2"><b>Issuance by Trade</b></td></tr>
<tr><th>Discipline</th><th>Slips</th></tr> <tr><th>Trade</th><th>Slips</th></tr>
${discRows} ${discRows}
</table> </table>

View File

@ -7,10 +7,27 @@
<label class="rpt-date-lbl"><i class="fas fa-calendar-check"></i> To</label> <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"> <input type="date" id="inv-rpt-to" class="rpt-date-input">
</div> </div>
<div class="inv-department-wrap no-print" id="inv-rpt-dept-wrap">
<div class="inv-dep-trigger" id="inv-rpt-dept-trigger">
<div class="inv-dep-left">
<i class="fas fa-building"></i>
<span class="inv-dep-lbl" id="inv-rpt-dept-lbl">All Department</span>
</div>
<i class="fas fa-chevron-down inv-dep-caret"></i>
</div>
<div class="inv-dep-dropdown" id="inv-rpt-dept-dropdown">
<div class="inv-dep-searchbox">
<i class="fas fa-search"></i>
<input type="text" id="inv-rpt-dept-search" placeholder="Search department name…">
</div>
<div class="inv-dep-list" id="inv-rpt-dept-list">
</div>
</div>
</div>
<button id="inv-rpt-generate" class="rpt-btn rpt-btn-primary"> <button id="inv-rpt-generate" class="rpt-btn rpt-btn-primary">
<i class="fas fa-sync"></i> Generate <i class="fas fa-sync"></i> Generate
</button> </button>
<button id="inv-rpt-print" class="rpt-btn rpt-btn-outline" onclick="window.print()"> <button id="inv-rpt-print" class="rpt-btn rpt-btn-outline no-print">
<i class="fas fa-print"></i> Print / PDF <i class="fas fa-print"></i> Print / PDF
</button> </button>
<button id="inv-rpt-csv" class="rpt-btn rpt-btn-outline"> <button id="inv-rpt-csv" class="rpt-btn rpt-btn-outline">

View File

@ -147,7 +147,6 @@ function clearCustomTable() {
// ===================================================================== // =====================================================================
// UTILITIES // UTILITIES
// ===================================================================== // =====================================================================
function getGrossFromTable() { function getGrossFromTable() {
let gross = 0; let gross = 0;
@ -171,16 +170,12 @@ function getChargesFromTable() {
return charges; return charges;
} }
function getDiscount() { function getDiscount() {
return parseFloat($('#discount').val()) || 0; return parseFloat($('#discount').val()) || 0;
} }
function getPoTypeId() { function getPoTypeId() {
return parseInt($('#poTypeId').val()) || 0; return parseInt($('#poTypeId').val()) || 0;
} }
function recalculateAll() { function recalculateAll() {
const poTypeId = getPoTypeId(); const poTypeId = getPoTypeId();
const grossAmount = getGrossFromTable(); const grossAmount = getGrossFromTable();

View File

@ -0,0 +1,217 @@
<?xml version="1.0" encoding="utf-8"?>
<Report ScriptLanguage="CSharp" ReportInfo.Created="06/16/2026 15:54:16" ReportInfo.Modified="06/17/2026 16:02:50" 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="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" Enabled="false" DataType="System.Int32"/>
<Column Name="NetIssued" DataType="System.Decimal"/>
<CommandParameter Name="DateFrom" DataType="22"/>
<CommandParameter Name="DateTo" DataType="22"/>
</TableDataSource>
<TableDataSource Name="TDisciplineAgg" Alias="TDisciplineAgg" DataType="System.Int32" Enabled="true">
<Column Name="DisciplineName" DataType="System.String"/>
<Column Name="SlipCount" DataType="System.Int32"/>
<CommandParameter Name="DateFrom" DataType="22"/>
<CommandParameter Name="DateTo" DataType="22"/>
</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"/>
<CommandParameter Name="DateFrom" DataType="22"/>
<CommandParameter Name="DateTo" DataType="22"/>
</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=""/>
</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="[Date]" 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="Table">
<TableObject Name="TRIS" 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="TTopRecipients" 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="TDisciplineAgg" 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>