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

View File

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

View File

@ -1,5 +1,5 @@
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using System;
using System.Collections.Generic;
using System.Linq;
@ -11,8 +11,8 @@ namespace CPRNIMS.Domain.UIContracts.Inventory
public interface IInventoryReports
{
Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
Task<RISReportDto> GetRISReportAsync(InventoryReportsRequest request, CancellationToken ct);
Task<MRSReportDto> GetMRSReportAsync(InventoryReportsRequest request, 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.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.Infrastructure.Helper;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using System.Text;
using System.Text.Json;
@ -24,7 +27,7 @@ namespace CPRNIMS.Domain.UIServices.Inventory
{
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();
if (string.IsNullOrEmpty(token))
@ -33,11 +36,20 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetInventoryReport"]
?? throw new InvalidOperationException("GetInventoryReport endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
var qs = new Dictionary<string, string?>
{
["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 response = await http.GetAsync(qs.ToString(), ct);
var url = QueryHelpers.AddQueryString(baseEndpoint, qs);
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync(url, ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
@ -47,7 +59,7 @@ namespace CPRNIMS.Domain.UIServices.Inventory
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();
if (string.IsNullOrEmpty(token))
@ -56,11 +68,20 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetMRSReport"]
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
var qs = new Dictionary<string, string?>
{
["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 response = await http.GetAsync(qs.ToString(), ct);
var url = QueryHelpers.AddQueryString(baseEndpoint, qs);
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync(url, ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
@ -70,7 +91,7 @@ namespace CPRNIMS.Domain.UIServices.Inventory
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();
if (string.IsNullOrEmpty(token))
@ -79,11 +100,20 @@ namespace CPRNIMS.Domain.UIServices.Inventory
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRISReport"]
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
var qs = new StringBuilder(baseEndpoint).Append('?');
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
var qs = new Dictionary<string, string?>
{
["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 response = await http.GetAsync(qs.ToString(), ct);
var url = QueryHelpers.AddQueryString(baseEndpoint, qs);
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
var response = await httpClient.GetAsync(url, ct);
var json = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)

View File

@ -108,7 +108,11 @@ namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports
public string PreparedBy { get; set; } = "Finance Department";
public string ReportNo { get; set; } = string.Empty;
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 List<InventoryReportRow> Rows { 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 UnitPrice { get; set; }
public int StockPct { get; set; }
public string? Department { get; set; }
public string? CurrencyCode { get; set; }
}
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.Infrastructure.Dto.Inventory.Reports;
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
using CPRNIMS.WebApi.Security;
using Microsoft.AspNetCore.Mvc;
@ -19,35 +21,35 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
}
[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();
if (currentUser == null)
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);
}
[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();
if (currentUser == null)
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);
}
[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();
if (currentUser == null)
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);
}

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 System.Drawing.Printing;
namespace CPRNIMS.WebApps.Controllers.Inventory
{
public class InventoryReportsController : Controller
public class InventoryReportsController : BaseMethod
{
private readonly IInventoryReports _reports;
public InventoryReportsController(IInventoryReports reports)
{
_reports=reports;
}
public InventoryReportsController(ErrorLogHelper errorMessageService,
IWebHostEnvironment webHostEnvironment, TokenHelper tokenHelper, IInventoryReports reports, IAccount account)
: base(errorMessageService, webHostEnvironment, tokenHelper, account) => _reports = reports;
[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);
}
[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);
}
[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);
}
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,
_ => null
};
GetUser();
var result = await _mrs.GetMRSPaged(new MRSPagedRequest
{
SearchMRSNo = searchMRSNo,
@ -43,6 +43,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpGet]
public async Task<IActionResult> SearchRIS([FromQuery] int? searchProjectCodeId, string? searchRISNo , CancellationToken ct = default)
{
GetUser();
var result = await _mrs.SearchRIS(new SearchRISProjectCodeRequest
{
SearchRISNo = searchRISNo,
@ -54,6 +55,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpGet]
public async Task<IActionResult> SearchProjects([FromQuery] string? searchProjectCode, CancellationToken ct = default)
{
GetUser();
var result = await _mrs.SearchProjects(new SearchRISProjectCodeRequest
{ SearchProjectCode = searchProjectCode,}, ct);
@ -62,6 +64,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost]
public async Task<IActionResult> CreateMRS([FromBody] CreateMRSRequest request,CancellationToken ct)
{
GetUser();
var result = await _mrs.CreateMRS(request, ct);
if (!result.success)
@ -73,6 +76,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost]
public async Task<IActionResult> ApproveMRS([FromBody] ApproveMRSRequest request,CancellationToken ct)
{
GetUser();
var result = await _mrs.ApproveMRS(request, ct);
if (!result.success)
@ -84,6 +88,7 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
[HttpPost]
public async Task<IActionResult> CancelMRS([FromBody] CancelMRSRequest request,CancellationToken ct)
{
GetUser();
var result = await _mrs.CancelMRS(request, ct);
if (!result.success)

View File

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

View File

@ -5,26 +5,41 @@
color: #1a2e35;
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 {
position: absolute;
left: 0;
top: 0;
#print-mount {
display: block !important;
position: static !important;
width: 100%;
}
#print-mount .rpt-page {
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
overflow: visible !important;
}
.no-print {
display: none !important;
.rpt-section {
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 {
@ -35,8 +50,11 @@
display: table-header-group;
}
@@page {
size: A4 landscape;
margin: 5mm;
}
}
.rpt-page {
width: 100%;
margin: 0;
@ -45,6 +63,7 @@
border-radius: var(--radius-lg, 14px);
overflow: hidden;
}
.rpt-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #d6eaec;
@ -117,6 +136,7 @@
background: var(--teal-pale, #e6f7f8);
border-color: var(--teal-dark, #0d5c63);
}
.rpt-header-top {
display: flex;
align-items: flex-start;
@ -387,76 +407,256 @@
const xlsxBtn = document.getElementById("inv-rpt-excel");
const container = document.getElementById("inv-rpt-container");
if (!fromEl || !toEl || !container) {
console.error("Inventory report subtab init failed — missing elements.");
// Department
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;
}
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
const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1);
const deptEl = document.getElementById("inv-rpt-dept");
toEl.value = today.toISOString().slice(0, 10);
fromEl.value = first.toISOString().slice(0, 10);
let lastData = null; // cache for export
function buildParams() {
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
}
let currentPage = 1;
let pageSize = 10;
let lastData = null; // paged data (for display)
csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender);
genBtn.addEventListener("click", () => { currentPage = 1; fetchAndRender(); });
async function fetchAndRender() {
container.innerHTML = `<div class="inv-tab-loading">
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
container.innerHTML = `<div class="inv-tab-loading"><div class="inv-spinner"></div><span>Loading report…</span></div>`;
try {
const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
lastData = json.data ?? json;
// populate dropdown from SP's department result set (eager-loaded)
if (Array.isArray(lastData.departments)) {
allDepartments = lastData.departments;
renderDeptOptions("");
}
renderReport(lastData);
renderPager(lastData);
} catch (err) {
console.error("Inventory report fetch error:", err);
container.innerHTML = `
<div class="inv-placeholder">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<h3>Failed to load report</h3>
<p>Please try again.</p>
</div>`;
container.innerHTML = `<div class="inv-placeholder"><i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i><h3>Failed to load report</h3><p>Please try again.</p></div>`;
}
}
function 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) ──
function exportCsv() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCat = lastData.byCategory ?? [];
const alerts = lastData.alerts ?? [];
const summary = lastData.summary ?? {};
const headers = ["Item Name","Item No.","Category","Lot No.","Qty In","Qty Out","On Hand","Stock %"];
async function exportCsv() {
const data = await fetchFullForExport();
if (!data) return;
const rows = data.rows ?? [];
const byCat = data.byCategory ?? [];
const alerts = data.alerts ?? [];
const summary = data.summary ?? {};
const headers = ["Item Name / UOM","Item No.","Category","Department","Lot No","Qty In","Qty Out","On Hand","Stock %"];
const csvLines = [
`Inventory Summary Report`,
`Company,${csvCell(lastData.companyName)}`,
`Report No,${csvCell(lastData.reportNo)}`,
`Company,${csvCell(data.companyName)}`,
`Report No,${csvCell(data.reportNo)}`,
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
``,
// ── Inventory detail ──
headers.map(csvCell).join(","),
...rows.map(r => [
r.itemName, r.itemNo, r.itemCategoryName, r.lotNo,
r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct
r.itemName, r.itemNo, r.itemCategoryName, r.department, r.lotNo,r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct
].map(csvCell).join(",")),
["Total","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, ""].map(csvCell).join(","),
["Total","","","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, ""].map(csvCell).join(","),
``,
// ── Stock level by category ──
`Stock Level by Category`,
["Category","Avg Stock %"].map(csvCell).join(","),
...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")),
``,
// ── Items requiring attention ──
`Items Requiring Attention`,
["Item","On Hand","Alert"].map(csvCell).join(","),
...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(","))
@ -468,10 +668,7 @@
"text/csv;charset=utf-8;"
);
}
function csvCell(v) {
const s = String(v ?? "");
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
@ -530,6 +727,7 @@
<div class="kpi-lbl"><i class="fas fa-layer-group"></i> Total on hand</div>
<div class="kpi-val">${(summary.totalOnHand ?? 0).toLocaleString()}</div>
</div>
<div class="kpi-cell">
<div class="kpi-lbl"><i class="fas fa-exclamation-triangle"></i> Low stock</div>
<div class="kpi-val" style="color:#A32D2D">${summary.lowStockCount ?? 0}</div>
@ -545,9 +743,14 @@
<table class="rpt-table">
<thead>
<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">On Hand</th><th>Stock Level</th>
<th class="num">On Hand</th>
<th>Stock Level</th>
</tr>
</thead>
<tbody>
@ -556,14 +759,15 @@
<td style="font-weight:600">${_esc(r.itemName)}</td>
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td>
<td style="color:var(--text-muted,#6b8890)">${_esc(r.lotNo)}</td>
<td>${_esc(r.department ?? "—")}</td>
<td class="num">${r.lotNo}</td>
<td class="num">${r.qtyIn}</td>
<td class="num">${r.qtyOut}</td>
<td class="num" style="font-weight:600">${r.qtyOnHand}</td>
<td>${_stockBar(r.stockPct)}</td>
</tr>`).join("")}
<tr class="total-row">
<td colspan="4">Total</td>
<td colspan="5">Total</td>
<td class="num">${summary.totalQtyIn ?? 0}</td>
<td class="num">${summary.totalQtyOut ?? 0}</td>
<td class="num">${summary.totalOnHand ?? 0}</td>
@ -639,14 +843,15 @@
}
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
function exportExcel() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCat = lastData.byCategory ?? [];
const alerts = lastData.alerts ?? [];
const summary = lastData.summary ?? {};
async function exportExcel() {
const data = await fetchFullForExport();
if (!data) return;
const rows = data.rows ?? [];
const byCat = data.byCategory ?? [];
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("");
const bodyRows = rows.map(r => `
@ -654,7 +859,8 @@
<td>${_esc(r.itemName)}</td>
<td>${_esc(r.itemNo)}</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.qtyOut}</td>
<td>${r.qtyOnHand}</td>
@ -674,23 +880,27 @@
<head><meta charset="utf-8"></head>
<body>
<table border="1">
<tr><td colspan="8"><b>Inventory Summary Report</b></td></tr>
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr><td colspan="9"><b>Inventory Summary Report</b></td></tr>
<tr><td>Company</td><td colspan="8">${_esc(data.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="8">${_esc(data.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="8">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr></tr>
<tr>${headerCells}</tr>
${bodyRows}
<tr><td colspan="4"><b>Total</b></td><td>${summary.totalQtyIn ?? 0}</td><td>${summary.totalQtyOut ?? 0}</td><td>${summary.totalOnHand ?? 0}</td><td></td></tr>
<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>
<br/>
<table border="1">
<tr><td colspan="2"><b>Stock Level by Category</b></td></tr>
<tr><th>Category</th><th>Avg Stock %</th></tr>
${catRows}
</table>
<br/>
<table border="1">
<tr><td colspan="3"><b>Items Requiring Attention</b></td></tr>

View File

@ -5,37 +5,32 @@
color: #1a2e35;
padding: 24px;
}
@@media print {
body { padding: 0; margin: 0; background: #fff;}
body * { visibility: hidden;}
#inv-rpt-page, #inv-rpt-page * {
visibility: visible;
body > *:not(#print-mount) {
display: none !important;
}
#inv-rpt-page {
position: absolute;
left: 0;
top: 0;
#print-mount {
display: block !important;
position: static !important;
width: 100%;
}
#print-mount .rpt-page {
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
overflow: visible !important;
}
.no-print {
display: none !important;
}
.rpt-table tr {
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
.rpt-section { 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 { page-break-inside: avoid; }
thead { display: table-header-group; }
@@page {
size: A4 landscape;
margin: 5mm;
}
}
.rpt-page {
width: 100%;
@ -387,76 +382,258 @@
const xlsxBtn = document.getElementById("inv-rpt-excel");
const container = document.getElementById("inv-rpt-container");
if (!fromEl || !toEl || !container) {
console.error("Inventory report subtab init failed — missing elements.");
// Department
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;
}
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
const today = new Date();
const first = new Date(today.getFullYear(), today.getMonth(), 1);
const deptEl = document.getElementById("inv-rpt-dept");
toEl.value = today.toISOString().slice(0, 10);
fromEl.value = first.toISOString().slice(0, 10);
let lastData = null; // cache for export
function buildParams() {
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
}
let currentPage = 1;
let pageSize = 10;
let lastData = null; // paged data (for display)
csvBtn.addEventListener("click", exportCsv);
genBtn.addEventListener("click", fetchAndRender);
genBtn.addEventListener("click", () => { currentPage = 1; fetchAndRender(); });
async function fetchAndRender() {
container.innerHTML = `<div class="inv-tab-loading">
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
container.innerHTML = `<div class="inv-tab-loading"><div class="inv-spinner"></div><span>Loading report…</span></div>`;
try {
const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
lastData = json.data ?? json;
// populate dropdown from SP's department result set (eager-loaded)
if (Array.isArray(lastData.departments)) {
allDepartments = lastData.departments;
renderDeptOptions("");
}
renderReport(lastData);
renderPager(lastData);
} catch (err) {
console.error("Inventory report fetch error:", err);
container.innerHTML = `
<div class="inv-placeholder">
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
<h3>Failed to load report</h3>
<p>Please try again.</p>
</div>`;
container.innerHTML = `<div class="inv-placeholder"><i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i><h3>Failed to load report</h3><p>Please try again.</p></div>`;
}
}
function 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) ──
function exportCsv() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCat = lastData.byCategory ?? [];
const alerts = lastData.alerts ?? [];
const summary = lastData.summary ?? {};
const headers = ["Item Name","Item No.","Category","Unit Price","Qty In","Qty Out","On Hand","Stock %"];
async function exportCsv() {
const data = await fetchFullForExport();
if (!data) return;
const rows = data.rows ?? [];
const byCat = data.byCategory ?? [];
const alerts = data.alerts ?? [];
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 = [
`Inventory Summary Report`,
`Company,${csvCell(lastData.companyName)}`,
`Report No,${csvCell(lastData.reportNo)}`,
`Company,${csvCell(data.companyName)}`,
`Report No,${csvCell(data.reportNo)}`,
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
``,
// ── Inventory detail ──
headers.map(csvCell).join(","),
...rows.map(r => [
r.itemName, r.itemNo, r.itemCategoryName, r.unitPrice,
r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct
r.itemName, r.itemNo, r.itemCategoryName, r.department, r.currencyCode,
r.unitPrice, r.qtyIn, r.qtyOut, r.qtyOnHand,
(Number(r.qtyOnHand) || 0) * (Number(r.unitPrice) || 0), r.stockPct
].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`,
["Category","Avg Stock %"].map(csvCell).join(","),
...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")),
``,
// ── Items requiring attention ──
`Items Requiring Attention`,
["Item","On Hand","Alert"].map(csvCell).join(","),
...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(","))
@ -468,10 +645,7 @@
"text/csv;charset=utf-8;"
);
}
function csvCell(v) {
const s = String(v ?? "");
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
@ -549,7 +723,9 @@
<table class="rpt-table">
<thead>
<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">Qty In</th><th class="num">Qty Out</th>
<th class="num">On Hand</th>
@ -563,15 +739,16 @@
<td style="font-weight:600">${_esc(r.itemName)}</td>
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
<td>${_esc(r.itemCategoryName)}</td>
<td 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.qtyOut}</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>
</tr>`).join("")}
<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.totalQtyOut ?? 0}</td>
<td class="num">${summary.totalOnHand ?? 0}</td>
@ -628,13 +805,13 @@
</div>
</div>`;
}
function _money(v) {
function _money(v, currency) {
const n = Number(v) || 0;
return n.toLocaleString("en-PH", {
style: "currency",
currency: "PHP",
minimumFractionDigits: 2
const amount = n.toLocaleString("en-PH", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
return currency ? `${currency} ${amount}` : amount;
}
function _stockBar(pct) {
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) ──
function exportExcel() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCat = lastData.byCategory ?? [];
const alerts = lastData.alerts ?? [];
const summary = lastData.summary ?? {};
async function exportExcel() {
const data = await fetchFullForExport();
if (!data) return;
const rows = data.rows ?? [];
const byCat = data.byCategory ?? [];
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("");
const bodyRows = rows.map(r => `
@ -670,10 +848,13 @@
<td>${_esc(r.itemName)}</td>
<td>${_esc(r.itemNo)}</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.qtyOut}</td>
<td>${r.qtyOnHand}</td>
<td>${(Number(r.qtyOnHand)||0) * (Number(r.unitPrice)||0)}</td>
<td>${r.stockPct}</td>
</tr>`).join("");
@ -690,14 +871,14 @@
<head><meta charset="utf-8"></head>
<body>
<table border="1">
<tr><td colspan="8"><b>Inventory Summary Report</b></td></tr>
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr><td colspan="11"><b>Inventory Summary Report</b></td></tr>
<tr><td>Company</td><td colspan="10">${_esc(data.companyName)}</td></tr>
<tr><td>Report No</td><td colspan="10">${_esc(data.reportNo)}</td></tr>
<tr><td>Period</td><td colspan="10">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
<tr></tr>
<tr>${headerCells}</tr>
${bodyRows}
<tr><td colspan="4"><b>Total</b></td><td>${summary.totalQtyIn ?? 0}</td><td>${summary.totalQtyOut ?? 0}</td><td>${summary.totalOnHand ?? 0}</td><td></td></tr>
<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>
<br/>

View File

@ -7,32 +7,38 @@
}
@@media print {
body {
padding: 0;
margin: 0;
background: #fff;
body > *:not(#print-mount) {
display: none !important;
}
body * {
visibility: hidden;
}
#mrs-rpt-page, #mrs-rpt-page * {
visibility: visible;
}
#mrs-rpt-page {
position: absolute;
left: 0;
top: 0;
#print-mount {
display: block !important;
position: static !important;
width: 100%;
}
#print-mount .rpt-page {
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
overflow: visible !important;
}
.no-print {
display: none !important;
.rpt-section {
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 {
@ -42,6 +48,11 @@
thead {
display: table-header-group;
}
@@page {
size: A4 landscape;
margin: 5mm;
}
}
.rpt-page {
@ -197,7 +208,7 @@
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 0;
border-bottom: 1px solid #d6eaec;
}
@ -584,9 +595,53 @@
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) ──
function exportCsv() {
async function exportCsv() {
const data = await fetchFullForExport();
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCond = lastData.byCondition ?? [];
@ -626,7 +681,7 @@
}
// ── Excel export (MRS schema, HTML-table .xls) ──
function exportExcel() {
async function exportExcel() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byCond = lastData.byCondition ?? [];

View File

@ -7,32 +7,38 @@
}
@@media print {
body {
padding: 0;
margin: 0;
background: #fff;
body > *:not(#print-mount) {
display: none !important;
}
body * {
visibility: hidden;
}
#ris-rpt-page, #ris-rpt-page * {
visibility: visible;
}
#ris-rpt-page {
position: absolute;
left: 0;
top: 0;
#print-mount {
display: block !important;
position: static !important;
width: 100%;
}
#print-mount .ris-page {
box-shadow: none !important;
border: none !important;
border-radius: 0 !important;
overflow: visible !important;
}
.no-print {
display: none !important;
.rpt-section {
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 {
@ -42,6 +48,11 @@
thead {
display: table-header-group;
}
@@page {
size: A4 landscape;
margin: 5mm;
}
}
.rpt-page {
@ -197,7 +208,7 @@
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 0;
border-bottom: 1px solid #d6eaec;
}
@ -434,14 +445,61 @@
}
csvBtn.addEventListener("click", exportCsv);
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) ──
function exportCsv() {
async function exportCsv() {
const data = await fetchFullForExport();
if (!lastData) return;
const rows = lastData.rows ?? [];
const byDisc = lastData.byDiscipline ?? [];
const topRecv = lastData.topRecipients ?? [];
const summary = lastData.summary ?? {};
const headers = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"];
const headers = ["RIS No.","Date","Item","Item No.","Trade","Issued To","Qty Issued","Qty Returned","Net Out","Status"];
const csvLines = [
`Return Issuance Slip Report`,
@ -456,8 +514,8 @@
].map(csvCell).join(",")),
["Total","","","","","", summary.totalQtyIssued ?? 0, summary.totalQtyReturned ?? 0, summary.totalNetIssued ?? 0, ""].map(csvCell).join(","),
``,
`Issuance by Discipline`,
["Discipline","Slips"].map(csvCell).join(","),
`Issuance by Trade`,
["Trade","Slips"].map(csvCell).join(","),
...byDisc.map(d => [d.disciplineName, d.count].map(csvCell).join(",")),
``,
`Top Recipients`,
@ -501,7 +559,7 @@
<div class="rpt-company">${_esc(data.companyName ?? "")}</div>
<div class="rpt-title">Return Issuance Slip Report</div>
<div class="rpt-subtitle">
Period: ${_fmtDate(data.dateFrom)} ${_fmtDate(data.dateTo)} · All departments · All disciplines
Period: ${_fmtDate(data.dateFrom)} ${_fmtDate(data.dateTo)} · All departments · All trades
</div>
</div>
<div class="rpt-logo"><i class="fas fa-file-export"></i></div>
@ -544,7 +602,7 @@
<thead>
<tr>
<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">Net Out</th><th>Status</th>
</tr>
@ -576,7 +634,7 @@
<div class="two-col">
<div class="col-left">
<div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Issuance by discipline</div>
<div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Issuance by trade</div>
<div style="display:flex;flex-direction:column;gap:8px">
${byDisc.map((d, i) => `
<div>
@ -628,14 +686,14 @@
}
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
function exportExcel() {
async function exportExcel() {
if (!lastData) return;
const rows = lastData.rows ?? [];
const byDisc = lastData.byDiscipline ?? [];
const topRecv = lastData.topRecipients ?? [];
const summary = lastData.summary ?? {};
const headerCells = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"]
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("");
const bodyRows = rows.map(r => `
@ -677,8 +735,8 @@
<br/>
<table border="1">
<tr><td colspan="2"><b>Issuance by Discipline</b></td></tr>
<tr><th>Discipline</th><th>Slips</th></tr>
<tr><td colspan="2"><b>Issuance by Trade</b></td></tr>
<tr><th>Trade</th><th>Slips</th></tr>
${discRows}
</table>

View File

@ -7,10 +7,27 @@
<label class="rpt-date-lbl"><i class="fas fa-calendar-check"></i> To</label>
<input type="date" id="inv-rpt-to" class="rpt-date-input">
</div>
<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">
<i class="fas fa-sync"></i> Generate
</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
</button>
<button id="inv-rpt-csv" class="rpt-btn rpt-btn-outline">

View File

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