Full CRUD with RIS, MRS, Inventory including reports
This commit is contained in:
parent
44862d01b5
commit
fa03ef5a3d
@ -10,6 +10,9 @@
|
||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||
<PackageReference Include="CaptchaGen.NetCore" Version="1.1.2" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="FastReport.OpenSource" Version="2026.2.3" />
|
||||
<PackageReference Include="FastReport.OpenSource.Export.PdfSimple" Version="2026.2.3" />
|
||||
<PackageReference Include="FastReport.OpenSource.Web" Version="2026.2.3" />
|
||||
<PackageReference Include="Google.Apis.Drive.v3" Version="1.67.0.3373" />
|
||||
<PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.2.0" />
|
||||
|
||||
14
CPRNIMS.Domain/Contracts/Common/ITransactionFacade.cs
Normal file
14
CPRNIMS.Domain/Contracts/Common/ITransactionFacade.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.Contracts.Common
|
||||
{
|
||||
public interface ITransactionFacade
|
||||
{
|
||||
Task<T> ExecuteAsync<T>(Func<Task<T>> operation,CancellationToken ct);
|
||||
Task ExecuteAsync(Func<Task> operation,CancellationToken ct);
|
||||
}
|
||||
}
|
||||
16
CPRNIMS.Domain/Contracts/Inventory/IInventoryReports.cs
Normal file
16
CPRNIMS.Domain/Contracts/Inventory/IInventoryReports.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.Contracts.Inventory
|
||||
{
|
||||
public interface IInventoryReports
|
||||
{
|
||||
Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "");
|
||||
Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "");
|
||||
Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "");
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,10 @@ namespace CPRNIMS.Domain.Contracts.Inventory
|
||||
{
|
||||
public interface IMRS
|
||||
{
|
||||
Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter);
|
||||
Task<MRS?> GetByIdAsync(long mrsId);
|
||||
Task<MRS> CreateAsync(CreateMRSRequest dto, string createdBy);
|
||||
Task ApproveAsync(long mrsId, string approvedBy);
|
||||
Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter, CancellationToken ct,int? departmentId = null, string? userName = "");
|
||||
Task<MRS?> GetByIdAsync(long mrsId, CancellationToken ct);
|
||||
Task<MRS> CreateAsync(CreateMRSRequest dto, string createdBy, CancellationToken ct);
|
||||
Task ApproveAsync(long mrsId, string approvedBy, CancellationToken ct);
|
||||
Task CancelAsync(CancelMRSRequest request, string canceledBy, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,10 +13,10 @@ namespace CPRNIMS.Domain.Contracts.Inventory
|
||||
{
|
||||
public interface IRIS
|
||||
{
|
||||
Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct);
|
||||
Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct, int? departmentId = null, string? userName = "");
|
||||
Task<RISResponse?> GetByIdAsync(long risId, CancellationToken ct);
|
||||
Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct);
|
||||
Task ApproveAsync(ApproveRISRequest request, string approvedBy, CancellationToken ct);
|
||||
Task CancelAsync(CancelRISRequest request, CancellationToken ct);
|
||||
Task CancelAsync(CancelRISRequest request,string canceledBy, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
16
CPRNIMS.Domain/Contracts/Reports/IReportBuilder.cs
Normal file
16
CPRNIMS.Domain/Contracts/Reports/IReportBuilder.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using FastReport;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.Contracts.Reports
|
||||
{
|
||||
public interface IReportBuilder
|
||||
{
|
||||
Task<Report> RISBuildAsync(DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct);
|
||||
Task<Report> MRSBuildAsync(DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
16
CPRNIMS.Domain/Contracts/Reports/IReportDataService.cs
Normal file
16
CPRNIMS.Domain/Contracts/Reports/IReportDataService.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.Contracts.Reports
|
||||
{
|
||||
public interface IReportDataService
|
||||
{
|
||||
List<RISRowDto> GetMain(DateTime dateFrom, DateTime dateTo);
|
||||
List<DisciplineAggDto> GetDisciplines(DateTime dateFrom, DateTime dateTo);
|
||||
List<TopRecipientDto> GetRecipients(DateTime dateFrom, DateTime dateTo);
|
||||
}
|
||||
}
|
||||
@ -57,6 +57,7 @@ namespace CPRNIMS.Domain.Services.Account
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||
new Claim("FullName", user.FullName ?? ""),
|
||||
new Claim("Company", user.Company ?? ""),
|
||||
new Claim("DepartmentId", Convert.ToString(user.DepartmentId)),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
|
||||
|
||||
49
CPRNIMS.Domain/Services/Common/TransactionFacade.cs
Normal file
49
CPRNIMS.Domain/Services/Common/TransactionFacade.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using CPRNIMS.Domain.Contracts.Common;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CPRNIMS.Domain.Services.Common
|
||||
{
|
||||
public class TransactionFacade : ITransactionFacade
|
||||
{
|
||||
private readonly NonInventoryDbContext _db;
|
||||
|
||||
public TransactionFacade(NonInventoryDbContext db)
|
||||
=> _db = db ?? throw new ArgumentNullException(nameof(db));
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(operation);
|
||||
|
||||
var strategy = _db.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var tx = await _db.Database.BeginTransactionAsync(ct);
|
||||
try
|
||||
{
|
||||
var result = await operation();
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await tx.CommitAsync(ct);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Func<Task> operation, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(operation);
|
||||
|
||||
await ExecuteAsync<bool>(async () =>
|
||||
{
|
||||
await operation();
|
||||
return true;
|
||||
}, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
294
CPRNIMS.Domain/Services/Inventory/InventoryReports.cs
Normal file
294
CPRNIMS.Domain/Services/Inventory/InventoryReports.cs
Normal file
@ -0,0 +1,294 @@
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CPRNIMS.Domain.Services.Inventory
|
||||
{
|
||||
public class InventoryReports : IInventoryReports
|
||||
{
|
||||
private readonly NonInventoryDbContext _db;
|
||||
public InventoryReports(NonInventoryDbContext db) => _db = db;
|
||||
|
||||
public async Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
|
||||
int? departmentId = null, string? userName = "")
|
||||
{
|
||||
var endDate = dateTo.Date.AddDays(1);
|
||||
|
||||
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
||||
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
||||
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var dateToInclusive = dateTo.AddDays(1);
|
||||
|
||||
var query = _db.RIS
|
||||
.Include(r => r.Discipline)
|
||||
.Include(r => r.Inventory)
|
||||
.Include(r => r.MaterialReturns)
|
||||
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < dateToInclusive);
|
||||
|
||||
if (departmentId.HasValue && !seeAllDepartments)
|
||||
{
|
||||
query = query.Where(m =>
|
||||
m.Inventory.User != null &&
|
||||
m.Inventory.User.DepartmentId == departmentId.Value);
|
||||
}
|
||||
|
||||
var rows = await query
|
||||
.OrderByDescending(r => r.CreatedDate)
|
||||
.Select(r => new RISReportRow
|
||||
{
|
||||
RISNo = r.RISNo,
|
||||
CreatedDate = r.CreatedDate,
|
||||
ItemName = r.PRDetail != null ? r.PRDetail.ItemName : "—",
|
||||
ItemNo = r.Inventory.ItemNo,
|
||||
DisciplineName = r.Discipline.DisciplineName,
|
||||
IssuedTo = r.IssuedTo,
|
||||
QtyIssued = r.QtyIssued,
|
||||
TotalReturned = r.MaterialReturns
|
||||
.Where(m => m.Status != 2)
|
||||
.Sum(m => (int?)m.QtyReturned) ?? 0,
|
||||
Status = r.Status,
|
||||
StatusLabel = r.Status == 0 ? "Draft"
|
||||
: r.Status == 1 ? "Approved"
|
||||
: "Cancelled"
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var row in rows)
|
||||
row.NetIssued = row.QtyIssued - row.TotalReturned;
|
||||
|
||||
var summary = new RISReportSummary
|
||||
{
|
||||
TotalRIS = rows.Count,
|
||||
TotalApproved = rows.Count(r => r.Status == 1),
|
||||
TotalPending = rows.Count(r => r.Status == 0),
|
||||
TotalCancelled = rows.Count(r => r.Status == 2),
|
||||
TotalQtyIssued = rows.Sum(r => r.QtyIssued),
|
||||
TotalQtyReturned = rows.Sum(r => r.TotalReturned),
|
||||
TotalNetIssued = rows.Sum(r => r.NetIssued),
|
||||
ApprovalRatePct = rows.Count > 0
|
||||
? Math.Round(rows.Count(r => r.Status == 1) * 100m / rows.Count, 1)
|
||||
: 0
|
||||
};
|
||||
|
||||
var byDiscipline = rows
|
||||
.GroupBy(r => r.DisciplineName)
|
||||
.Select(g => new DisciplineCount { DisciplineName = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(d => d.Count)
|
||||
.ToList();
|
||||
|
||||
var topRecipients = rows
|
||||
.GroupBy(r => r.IssuedTo)
|
||||
.Select(g => new TopRecipient
|
||||
{
|
||||
IssuedTo = g.Key,
|
||||
SlipCount = g.Count(),
|
||||
TotalQty = g.Sum(r => r.QtyIssued)
|
||||
})
|
||||
.OrderByDescending(t => t.TotalQty)
|
||||
.Take(5)
|
||||
.ToList();
|
||||
|
||||
return new RISReportDto
|
||||
{
|
||||
ReportNo = $"RPT-RIS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
|
||||
DateFrom = dateFrom,
|
||||
DateTo = dateTo,
|
||||
Summary = summary,
|
||||
Rows = rows,
|
||||
ByDiscipline = byDiscipline,
|
||||
TopRecipients = topRecipients
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
|
||||
int? departmentId = null, string? userName = "")
|
||||
{
|
||||
var endDate = dateTo.Date.AddDays(1);
|
||||
|
||||
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
||||
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
||||
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var query = _db.MRS
|
||||
.Include(m => m.RIS)
|
||||
.Include(m => m.Inventory)
|
||||
.ThenInclude(i => i.User)
|
||||
.Where(m => m.CreatedDate >= dateFrom &&
|
||||
m.CreatedDate < endDate);
|
||||
|
||||
if (departmentId.HasValue && !seeAllDepartments)
|
||||
{
|
||||
query = query.Where(m =>
|
||||
m.Inventory.User != null &&
|
||||
m.Inventory.User.DepartmentId == departmentId.Value);
|
||||
}
|
||||
|
||||
var rows = await query
|
||||
.OrderByDescending(m => m.CreatedDate)
|
||||
.Select(m => new MRSReportRow
|
||||
{
|
||||
MRSNo = m.MRSNo,
|
||||
CreatedDate = m.CreatedDate,
|
||||
RISNo = m.RIS.RISNo,
|
||||
ItemName = m.RIS.PRDetail != null ? m.RIS.PRDetail.ItemName : "—",
|
||||
ReturnedBy = m.ReturnedBy,
|
||||
QtyReturned = m.QtyReturned,
|
||||
Condition = m.Condition ?? "Good",
|
||||
Status = m.Status,
|
||||
StatusLabel = m.Status == 0 ? "Draft"
|
||||
: m.Status == 1 ? "Approved"
|
||||
: "Cancelled"
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Total RIS qty issued in the same period (for the comparison panel)
|
||||
var totalRISQty = await _db.RIS
|
||||
.Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < endDate
|
||||
&& r.Status != 2)
|
||||
.SumAsync(r => (int?)r.QtyIssued, ct) ?? 0;
|
||||
|
||||
var totalReturned = rows.Where(r => r.Status != 2).Sum(r => r.QtyReturned);
|
||||
var goodCount = rows.Count(r => r.Condition == "Good");
|
||||
|
||||
var summary = new MRSReportSummary
|
||||
{
|
||||
TotalMRS = rows.Count,
|
||||
TotalQtyReturned = totalReturned,
|
||||
TotalQtyIssuedRIS = totalRISQty,
|
||||
NetQtyConsumed = totalRISQty - totalReturned,
|
||||
ReturnRatePct = totalRISQty > 0
|
||||
? Math.Round(totalReturned * 100m / totalRISQty, 1)
|
||||
: 0,
|
||||
GoodConditionPct = rows.Count > 0
|
||||
? Math.Round(goodCount * 100m / rows.Count, 1)
|
||||
: 0
|
||||
};
|
||||
|
||||
var byCondition = rows
|
||||
.Where(r => r.Status != 2)
|
||||
.GroupBy(r => r.Condition)
|
||||
.Select(g => new ConditionTotal { Condition = g.Key, TotalQty = g.Sum(r => r.QtyReturned) })
|
||||
.OrderByDescending(c => c.TotalQty)
|
||||
.ToList();
|
||||
|
||||
return new MRSReportDto
|
||||
{
|
||||
ReportNo = $"RPT-MRS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
|
||||
DateFrom = dateFrom,
|
||||
DateTo = dateTo,
|
||||
Summary = summary,
|
||||
Rows = rows,
|
||||
ByCondition = byCondition
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct,
|
||||
int? departmentId = null, string? userName = "")
|
||||
{
|
||||
var endDate = dateTo.Date.AddDays(1);
|
||||
|
||||
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25" , "LHRIOCAS24" };
|
||||
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
||||
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var query = _db.InventTransDetails
|
||||
.Where(itd =>
|
||||
itd.IsActive &&
|
||||
itd.InventTrans.IsActive &&
|
||||
itd.InventTrans.Inventory.IsActive &&
|
||||
itd.CreatedDate >= dateFrom &&
|
||||
itd.CreatedDate < endDate);
|
||||
|
||||
if (departmentId.HasValue && !seeAllDepartments)
|
||||
{
|
||||
query = query.Where(itd =>
|
||||
itd.InventTrans.Inventory.User.DepartmentId == departmentId.Value);
|
||||
}
|
||||
|
||||
var rawRows = await query
|
||||
.Select(itd => new
|
||||
{
|
||||
itd.InventTrans.Inventory.InventoryId,
|
||||
itd.InventTrans.Inventory.ItemNo,
|
||||
itd.InventTrans.Inventory.QtyIn,
|
||||
itd.InventTrans.Inventory.QtyOut,
|
||||
itd.InventTrans.Inventory.QtyOnHand,
|
||||
LotNo = itd.InventTrans.Inventory.Lot != null
|
||||
? itd.InventTrans.Inventory.Lot.LotName
|
||||
: null,
|
||||
ItemName = itd.InventTrans.Inventory.Item.ItemCode.ItemName ?? "None",
|
||||
ItemCategoryName =
|
||||
itd.InventTrans.Inventory.Item.ItemCode.ItemCategory.ItemCategoryName ?? "None",
|
||||
itd.CreatedDate
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
// De-duplicate: one row per Inventory (latest trans detail wins for the date shown)
|
||||
var rows = rawRows
|
||||
.GroupBy(r => r.InventoryId)
|
||||
.Select(g =>
|
||||
{
|
||||
var inv = g.OrderByDescending(x => x.CreatedDate).First();
|
||||
return new InventoryReportRow
|
||||
{
|
||||
ItemName = inv.ItemName,
|
||||
ItemNo = inv.ItemNo,
|
||||
ItemCategoryName = inv.ItemCategoryName,
|
||||
LotNo = inv.LotNo,
|
||||
QtyIn = inv.QtyIn,
|
||||
QtyOut = inv.QtyOut,
|
||||
QtyOnHand = inv.QtyOnHand,
|
||||
StockPct = inv.QtyIn > 0
|
||||
? (int)Math.Round(Math.Max(0, Math.Min(100, (inv.QtyOnHand / inv.QtyIn) * 100)))
|
||||
: 0
|
||||
};
|
||||
})
|
||||
.OrderBy(r => r.ItemName)
|
||||
.ToList();
|
||||
|
||||
var summary = new InventoryReportSummary
|
||||
{
|
||||
TotalSKUs = rows.Count,
|
||||
TotalOnHand = rows.Sum(r => r.QtyOnHand),
|
||||
TotalQtyIn = rows.Sum(r => r.QtyIn),
|
||||
TotalQtyOut = rows.Sum(r => r.QtyOut),
|
||||
LowStockCount = rows.Count(r => r.StockPct < 20 && r.QtyOnHand > 0),
|
||||
OutOfStockCount = rows.Count(r => r.QtyOnHand <= 0)
|
||||
};
|
||||
|
||||
var byCategory = rows
|
||||
.GroupBy(r => r.ItemCategoryName)
|
||||
.Select(g => new CategoryStockLevel
|
||||
{
|
||||
CategoryName = g.Key,
|
||||
AvgStockPct = (int)Math.Round(g.Average(r => r.StockPct))
|
||||
})
|
||||
.OrderByDescending(c => c.AvgStockPct)
|
||||
.ToList();
|
||||
|
||||
var alerts = rows
|
||||
.Where(r => r.StockPct < 20)
|
||||
.OrderBy(r => r.StockPct)
|
||||
.Take(10)
|
||||
.Select(r => new InventoryAlert
|
||||
{
|
||||
ItemName = r.ItemName,
|
||||
QtyOnHand = r.QtyOnHand,
|
||||
Severity = r.QtyOnHand <= 0 ? "Critical" : "Low"
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new InventoryReportDto
|
||||
{
|
||||
ReportNo = $"RPT-INV-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}",
|
||||
AsOf = dateTo.Date, // reflect the requested period end, not always today
|
||||
Summary = summary,
|
||||
Rows = rows,
|
||||
ByCategory = byCategory,
|
||||
Alerts = alerts
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,23 @@
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Domain.Contracts.Common;
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
|
||||
using CPRNIMS.Infrastructure.Entities.Inventory;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.Services.Inventory
|
||||
{
|
||||
public class MRS : IMRS
|
||||
{
|
||||
private readonly NonInventoryDbContext _db;
|
||||
public MRS(NonInventoryDbContext db) => _db = db;
|
||||
|
||||
public async Task ApproveAsync(long mrsId, string approvedBy)
|
||||
private readonly ITransactionFacade _transactionFacade;
|
||||
public MRS(NonInventoryDbContext db, ITransactionFacade transactionFacade)
|
||||
{
|
||||
_db = db;
|
||||
_transactionFacade = transactionFacade;
|
||||
}
|
||||
public async Task ApproveAsync(long mrsId, string approvedBy, CancellationToken ct)
|
||||
{
|
||||
var rms = await _db.MRS.FindAsync(mrsId)
|
||||
?? throw new InvalidOperationException("MRS not found.");
|
||||
@ -30,14 +29,38 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
rms.ApprovedBy = approvedBy;
|
||||
rms.ApprovedDate = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Infrastructure.Entities.Inventory.MRS> CreateAsync(CreateMRSRequest dto, string createdBy)
|
||||
public async Task CancelAsync(CancelMRSRequest request,string canceledBy, CancellationToken ct)
|
||||
{
|
||||
await _transactionFacade.ExecuteAsync(async () =>
|
||||
{
|
||||
var mrs = await _db.MRS
|
||||
.Include(m => m.Inventory)
|
||||
.FirstOrDefaultAsync(m => m.MRSId == request.MRSId)
|
||||
?? throw new InvalidOperationException("MRS not found.");
|
||||
|
||||
if (mrs.Status == 2)
|
||||
throw new InvalidOperationException("MRS is already cancelled.");
|
||||
|
||||
// Reverse the return: deduct qty back out
|
||||
mrs.Inventory.QtyOut = Math.Max(0m, mrs.Inventory.QtyOut) + mrs.QtyReturned;
|
||||
mrs.Inventory.QtyOnHand = mrs.Inventory.QtyIn - mrs.Inventory.QtyOut;
|
||||
mrs.Reason = request.Reason;
|
||||
mrs.Status = 2;
|
||||
mrs.CanceledDate = DateTime.Now;
|
||||
mrs.CanceledBy = canceledBy;
|
||||
}, ct);
|
||||
}
|
||||
|
||||
public async Task<Infrastructure.Entities.Inventory.MRS> CreateAsync(CreateMRSRequest dto, string createdBy, CancellationToken ct)
|
||||
{
|
||||
return await _transactionFacade.ExecuteAsync(async () =>
|
||||
{
|
||||
var ris = await _db.RIS
|
||||
.Include(r => r.Inventory)
|
||||
.FirstOrDefaultAsync(r => r.RISId == dto.RISId)
|
||||
.FirstOrDefaultAsync(r => r.RISId == dto.RISId, ct)
|
||||
?? throw new InvalidOperationException("Referenced RIS not found.");
|
||||
|
||||
if (dto.QtyReturned > ris.QtyIssued)
|
||||
@ -67,7 +90,7 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
inventory.QtyOnHand = inventory.QtyIn - inventory.QtyOut;
|
||||
|
||||
var trans = await _db.InventTrans
|
||||
.FirstOrDefaultAsync(t => t.InventoryId == ris.InventoryId && t.IsActive == true)!;
|
||||
.FirstOrDefaultAsync(t => t.InventoryId == ris.InventoryId && t.IsActive == true, ct)!;
|
||||
|
||||
_db.InventTransDetails.Add(new InventTransDetail
|
||||
{
|
||||
@ -79,24 +102,35 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return mrs;
|
||||
}, ct);
|
||||
}
|
||||
|
||||
public async Task<Infrastructure.Entities.Inventory.MRS?> GetByIdAsync(long mrsId)
|
||||
public async Task<Infrastructure.Entities.Inventory.MRS?> GetByIdAsync(long mrsId, CancellationToken ct)
|
||||
|
||||
=> await _db.MRS
|
||||
.Include(r => r.Inventory)
|
||||
.Include(r => r.RIS)
|
||||
.FirstOrDefaultAsync(r => r.RISId == mrsId);
|
||||
.FirstOrDefaultAsync(r => r.RISId == mrsId, ct);
|
||||
|
||||
public async Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter)
|
||||
public async Task<MRSPagedResult> GetPagedAsync(MRSFilterDto filter, CancellationToken ct,
|
||||
int? departmentId = null, string? userName = "")
|
||||
{
|
||||
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
||||
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
||||
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var q = _db.MRS
|
||||
.Include(m => m.RIS)
|
||||
.Include(m => m.Inventory)
|
||||
.AsQueryable();
|
||||
|
||||
if (departmentId.HasValue && !seeAllDepartments)
|
||||
{
|
||||
q = q.Where(itd =>
|
||||
itd.Inventory.User.DepartmentId == departmentId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.SearchMRSNo))
|
||||
q = q.Where(m => m.MRSNo.Contains(filter.SearchMRSNo));
|
||||
|
||||
@ -112,13 +146,13 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
if (filter.DateTo.HasValue)
|
||||
q = q.Where(m => m.CreatedDate <= filter.DateTo.Value.AddDays(1));
|
||||
|
||||
var total = await q.CountAsync();
|
||||
var total = await q.CountAsync(ct);
|
||||
|
||||
var data = await q
|
||||
.OrderByDescending(m => m.CreatedDate)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Skip((filter.PageNumber - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.Select(m => new MRSResponse
|
||||
.Select(m => new MRSPagedDto
|
||||
{
|
||||
MRSId = m.MRSId,
|
||||
MRSNo = m.MRSNo,
|
||||
@ -139,7 +173,7 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
ApprovedBy = m.ApprovedBy,
|
||||
ApprovedDate = m.ApprovedDate
|
||||
})
|
||||
.ToListAsync();
|
||||
.ToListAsync(ct);
|
||||
|
||||
return new MRSPagedResult { Data = data, RecordsTotal = total };
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Domain.UIServices.Inventory;
|
||||
using CPRNIMS.Domain.Contracts.Common;
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
@ -17,17 +17,15 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
public class RIS : IRIS
|
||||
{
|
||||
private readonly NonInventoryDbContext _db;
|
||||
|
||||
public RIS(NonInventoryDbContext db) => _db = db;
|
||||
|
||||
private readonly ITransactionFacade _transactionFacade;
|
||||
public RIS(NonInventoryDbContext db, ITransactionFacade transactionFacade)
|
||||
{
|
||||
_db = db;
|
||||
_transactionFacade = transactionFacade;
|
||||
}
|
||||
public async Task<Infrastructure.Entities.Inventory.RIS> CreateAsync(CreateRISRequest dto, string createdBy, CancellationToken ct)
|
||||
{
|
||||
var strategy = _db.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var tx = await _db.Database.BeginTransactionAsync(ct);
|
||||
try
|
||||
return await _transactionFacade.ExecuteAsync(async () =>
|
||||
{
|
||||
var inventory = await _db.Inventories
|
||||
.FirstOrDefaultAsync(i => i.InventoryId == dto.InventoryId, ct)
|
||||
@ -53,7 +51,6 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
CreatedDate = DateTime.Now
|
||||
};
|
||||
_db.RIS.Add(ris);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
var trans = await _db.InventTrans
|
||||
.Where(t => t.InventoryId == dto.InventoryId && t.IsActive == true)
|
||||
@ -75,17 +72,8 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
inventory.QtyOut = Math.Max(0m, inventory.QtyOut) + dto.QtyIssued;
|
||||
inventory.QtyOnHand = Math.Max(0m, inventory.QtyIn) - (decimal)inventory.QtyOut;
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await tx.CommitAsync(ct);
|
||||
|
||||
return ris;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}, ct);
|
||||
}
|
||||
|
||||
public async Task ApproveAsync(ApproveRISRequest request, string approvedBy, CancellationToken ct)
|
||||
@ -103,14 +91,9 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task CancelAsync(CancelRISRequest request, CancellationToken ct)
|
||||
public async Task CancelAsync(CancelRISRequest request,string canceledBy, CancellationToken ct)
|
||||
{
|
||||
var strategy = _db.Database.CreateExecutionStrategy();
|
||||
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var tx = await _db.Database.BeginTransactionAsync(ct);
|
||||
try
|
||||
await _transactionFacade.ExecuteAsync(async () =>
|
||||
{
|
||||
var ris = await _db.RIS
|
||||
.Include(r => r.Inventory)
|
||||
@ -120,6 +103,23 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
if (ris.Status == 2)
|
||||
throw new InvalidOperationException("RIS is already cancelled.");
|
||||
|
||||
//Check if already approved the related RIS No to MRS must cannot be cancel
|
||||
var mrs = await _db.MRS.FirstOrDefaultAsync(m => m.RISId == request.RISId, ct);
|
||||
|
||||
if (mrs != null)
|
||||
{
|
||||
if (mrs.Status == 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"MRS #{mrs.MRSNo} has already been approved in relation to RIS #{ris.RISNo}.");
|
||||
}
|
||||
|
||||
mrs.Status = 2;
|
||||
mrs.CanceledDate = DateTime.Now;
|
||||
mrs.CanceledBy = canceledBy;
|
||||
mrs.Reason = "Canceled in RIS already";
|
||||
}
|
||||
|
||||
ris.Inventory.QtyOut = Math.Max(0m, ris.Inventory.QtyOut - ris.QtyIssued);
|
||||
ris.Inventory.QtyOnHand = ris.Inventory.QtyIn - ris.Inventory.QtyOut;
|
||||
ris.Reason = request.Reason;
|
||||
@ -141,31 +141,7 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
Remarks = request.Reason,
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
//var inventory = await _db.Inventories
|
||||
// .FirstOrDefaultAsync(i => i.InventoryId == ris.InventoryId, ct)
|
||||
// ?? throw new InvalidOperationException("Inventory record not found.");
|
||||
|
||||
//if (inventory.QtyOnHand < ris.QtyIssued)
|
||||
// throw new InvalidOperationException(
|
||||
// $"Insufficient stock. On hand: {inventory.QtyOnHand}, requested: {ris.QtyIssued}.");
|
||||
|
||||
////restore the QtyOnHand using ris.QtyIssued
|
||||
//inventory.QtyOnHand = Math.Max(0m, inventory.QtyOnHand) + ris.QtyIssued;
|
||||
////reduce the QtyOut using ris.QtyIssued as we cancel the return isuance slip
|
||||
//inventory.QtyOut = Math.Max(0m, inventory.QtyOut) - ris.QtyIssued;
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await tx.CommitAsync(ct);
|
||||
|
||||
return ris;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}, ct);
|
||||
}
|
||||
|
||||
private async Task<string> GenerateRISNoAsync(CancellationToken ct)
|
||||
@ -177,19 +153,30 @@ namespace CPRNIMS.Domain.Services.Inventory
|
||||
return $"RIS-{year}{month}-{count:D4}"; // e.g. RIS-202606-0001
|
||||
}
|
||||
|
||||
public async Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct)
|
||||
public async Task<RISPagedResult> GetPagedAsync(RISFilterDto filter, CancellationToken ct,
|
||||
int? departmentId = null, string? userName = "")
|
||||
{
|
||||
var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" };
|
||||
bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName)
|
||||
&& allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var q = _db.RIS
|
||||
.Include(r => r.Discipline)
|
||||
.Include(r => r.Inventory)
|
||||
.Include(r => r.MaterialReturns)
|
||||
.AsQueryable();
|
||||
|
||||
if (departmentId.HasValue && !seeAllDepartments)
|
||||
{
|
||||
q = q.Where(itd =>
|
||||
itd.Inventory.User.DepartmentId == departmentId.Value);
|
||||
}
|
||||
|
||||
// Status filter (default to Draft=0 if null)
|
||||
if (filter.Status.HasValue)
|
||||
q = q.Where(r => r.Status == filter.Status.Value);
|
||||
else
|
||||
q = q.Where(r => r.Status == 0);
|
||||
//else
|
||||
// q = q.Where(r => r.Status == 0);
|
||||
|
||||
// RIS No
|
||||
if (!string.IsNullOrWhiteSpace(filter.SearchRISNo))
|
||||
|
||||
80
CPRNIMS.Domain/Services/Reports/ReportBuilder.cs
Normal file
80
CPRNIMS.Domain/Services/Reports/ReportBuilder.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using CPRNIMS.Domain.Contracts.Reports;
|
||||
using CPRNIMS.Domain.UIContracts.Common;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using FastReport;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Data;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Reports
|
||||
{
|
||||
public class ReportBuilder : IReportBuilder
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly TokenHelper _tokenHelper;
|
||||
private readonly IApiConfigurationService _apiConfigurationService;
|
||||
public ReportBuilder(IConfiguration configuration, TokenHelper tokenHelper,
|
||||
IApiConfigurationService apiConfigurationService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_tokenHelper = tokenHelper;
|
||||
_apiConfigurationService = apiConfigurationService;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
public Task<Report> MRSBuildAsync(DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<Report> RISBuildAsync(
|
||||
DateTime dateFrom, DateTime dateTo, string templatePath, CancellationToken ct = default)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:RISReportData"]
|
||||
?? throw new InvalidOperationException("RISReportData endpoint is not configured.");
|
||||
|
||||
// Append the date range as query string
|
||||
var url = $"{endpoint}?dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}";
|
||||
|
||||
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
var response = await httpClient.GetAsync(url, ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to fetch RIS report data. HTTP {(int)response.StatusCode}: {json}");
|
||||
|
||||
var dto = JsonSerializer.Deserialize<RISReportDataDto>(json, _jsonOptions)
|
||||
?? new RISReportDataDto();
|
||||
|
||||
var report = new Report();
|
||||
report.Load(templatePath);
|
||||
|
||||
// RegisterData accepts IEnumerable<object> — names must match the .frx datasources.
|
||||
report.RegisterData(dto.Rows ?? new(), "TRIS");
|
||||
report.RegisterData(dto.Disciplines ?? new(), "TDisciplineAgg");
|
||||
report.RegisterData(dto.Recipients ?? new(), "TTopRecipients");
|
||||
|
||||
/* report.GetDataSource("Table").Enabled = true;
|
||||
report.GetDataSource("Table1").Enabled = true;
|
||||
report.GetDataSource("Table3").Enabled = true;*/
|
||||
report.GetDataSource("TRIS").Enabled = true;
|
||||
report.GetDataSource("TDisciplineAgg").Enabled = true;
|
||||
report.GetDataSource("TTopRecipients").Enabled = true;
|
||||
|
||||
report.SetParameterValue("DateFrom", dateFrom);
|
||||
report.SetParameterValue("DateTo", dateTo);
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
CPRNIMS.Domain/Services/Reports/ReportDataService.cs
Normal file
109
CPRNIMS.Domain/Services/Reports/ReportDataService.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using CPRNIMS.Domain.Contracts.Reports;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Reports
|
||||
{
|
||||
public class ReportDataService : IReportDataService
|
||||
{
|
||||
private readonly NonInventoryDbContext _context;
|
||||
|
||||
public ReportDataService(NonInventoryDbContext context) => _context = context;
|
||||
|
||||
private const string MainSql = """
|
||||
SELECT
|
||||
R.RISNo,
|
||||
PR.PRNo,
|
||||
R.QtyIssued,
|
||||
R.IssuedTo,
|
||||
CASE R.Status
|
||||
WHEN 0 THEN 'Draft'
|
||||
WHEN 1 THEN 'Approved'
|
||||
WHEN 2 THEN 'Cancelled'
|
||||
ELSE 'Unknown'
|
||||
END AS StatusLabel,
|
||||
CreatedBy.CreatedBy,
|
||||
ApprovedBy.ApprovedBy,
|
||||
R.ApprovedDate,
|
||||
R.CreatedDate,
|
||||
D.DisciplineName,
|
||||
PRD.ItemName,
|
||||
PRD.ItemNo,
|
||||
IV.QtyIn,
|
||||
IV.QtyOut,
|
||||
IV.QtyOnHand,
|
||||
DEP.Department AS DepartmentName,
|
||||
ISNULL(MRS_AGG.TotalReturned, 0) AS TotalReturned,
|
||||
ISNULL(MRS_AGG.MRSCount, 0) AS MRSCount,
|
||||
R.QtyIssued - ISNULL(MRS_AGG.TotalReturned, 0) AS NetIssued
|
||||
FROM dbo.RIS R
|
||||
INNER JOIN dbo.Disciplines D ON R.DisciplineId = D.DisciplineId
|
||||
INNER JOIN dbo.Inventory IV ON R.InventoryId = IV.InventoryId AND IV.IsActive = 1
|
||||
LEFT JOIN dbo.Lot L ON IV.LotId = L.LotId
|
||||
OUTER APPLY (SELECT U.FullName ApprovedBy FROM dbo.Users U WHERE R.ApprovedBy = U.UserName) ApprovedBy
|
||||
OUTER APPLY (SELECT U2.FullName CreatedBy FROM dbo.Users U2 WHERE R.CreatedBy = U2.UserName) CreatedBy
|
||||
INNER JOIN Users U ON IV.UserId = U.Id
|
||||
LEFT JOIN dbo.Departments DEP ON U.DepartmentId = DEP.DepartmentId
|
||||
LEFT JOIN dbo.PRDetails PRD ON R.PRDetailId = PRD.PRDetailsId AND PRD.IsActive = 1
|
||||
LEFT JOIN dbo.PR PR ON PRD.PRId = PR.PRId AND PR.IsActive = 1
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
RISId,
|
||||
COUNT(*) AS MRSCount,
|
||||
SUM(QtyReturned) AS TotalReturned
|
||||
FROM dbo.MRS
|
||||
WHERE Status != 2
|
||||
GROUP BY RISId
|
||||
) MRS_AGG ON R.RISId = MRS_AGG.RISId
|
||||
WHERE R.Status != 2
|
||||
AND R.CreatedDate >= @DateFrom
|
||||
AND R.CreatedDate < DATEADD(DAY, 1, @DateTo)
|
||||
ORDER BY R.CreatedDate DESC, R.RISNo ASC;
|
||||
""";
|
||||
|
||||
private const string DisciplineSql = """
|
||||
SELECT
|
||||
D.DisciplineName,
|
||||
COUNT(*) AS SlipCount
|
||||
FROM dbo.RIS R
|
||||
INNER JOIN dbo.Disciplines D ON R.DisciplineId = D.DisciplineId
|
||||
WHERE R.Status != 2
|
||||
AND R.CreatedDate >= @DateFrom
|
||||
AND R.CreatedDate < DATEADD(DAY, 1, @DateTo)
|
||||
GROUP BY D.DisciplineName
|
||||
ORDER BY SlipCount DESC;
|
||||
""";
|
||||
|
||||
private const string RecipientsSql = """
|
||||
SELECT TOP 5
|
||||
R.IssuedTo AS Name,
|
||||
COUNT(*) AS SlipCount,
|
||||
SUM(IV.QtyOut) AS QtyOut
|
||||
FROM dbo.RIS R
|
||||
INNER JOIN dbo.Inventory IV ON R.InventoryId = IV.InventoryId AND IV.IsActive = 1
|
||||
WHERE R.Status != 2
|
||||
AND R.CreatedDate >= @DateFrom
|
||||
AND R.CreatedDate < DATEADD(DAY, 1, @DateTo)
|
||||
GROUP BY R.IssuedTo
|
||||
ORDER BY COUNT(*) DESC;
|
||||
""";
|
||||
|
||||
public List<RISRowDto> GetMain(DateTime dateFrom, DateTime dateTo) =>
|
||||
Query<RISRowDto>(MainSql, dateFrom, dateTo);
|
||||
|
||||
public List<DisciplineAggDto> GetDisciplines(DateTime dateFrom, DateTime dateTo) =>
|
||||
Query<DisciplineAggDto>(DisciplineSql, dateFrom, dateTo);
|
||||
|
||||
public List<TopRecipientDto> GetRecipients(DateTime dateFrom, DateTime dateTo) =>
|
||||
Query<TopRecipientDto>(RecipientsSql, dateFrom, dateTo);
|
||||
|
||||
private List<T> Query<T>(string sql, DateTime from, DateTime to)
|
||||
{
|
||||
using var conn = new SqlConnection(_context.Database.GetConnectionString());
|
||||
return conn.Query<T>(sql, new { DateFrom = from, DateTo = to }).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
CPRNIMS.Domain/UIContracts/Inventory/IInventoryReports.cs
Normal file
18
CPRNIMS.Domain/UIContracts/Inventory/IInventoryReports.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.UIContracts.Inventory
|
||||
{
|
||||
public interface IInventoryReports
|
||||
{
|
||||
|
||||
Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
|
||||
Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
|
||||
Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
20
CPRNIMS.Domain/UIContracts/Inventory/IMRS.cs
Normal file
20
CPRNIMS.Domain/UIContracts/Inventory/IMRS.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.UIContracts.Inventory
|
||||
{
|
||||
public interface IMRS
|
||||
{
|
||||
Task<MRSPagedResponse> GetMRSPaged(MRSPagedRequest request, CancellationToken ct);
|
||||
Task<ApiResponse<object>> CreateMRS(CreateMRSRequest request, CancellationToken ct);
|
||||
Task<ApiResponse<object>> ApproveMRS(ApproveMRSRequest request, CancellationToken ct);
|
||||
Task<ApiResponse<object>> CancelMRS(CancelMRSRequest request, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
|
||||
using CPRNIMS.Infrastructure.ViewModel.Inventory;
|
||||
using System;
|
||||
@ -11,10 +12,9 @@ namespace CPRNIMS.Domain.UIContracts.Inventory
|
||||
{
|
||||
public interface IRIS
|
||||
{
|
||||
Task<bool> ApproveRIS(ApproveRISRequest request, CancellationToken ct);
|
||||
Task<bool> CancelRIS(CancelRISRequest request, CancellationToken ct);
|
||||
Task<RISResponse> CreateRIS(CreateRISRequest request, CancellationToken ct);
|
||||
Task<RISPagedResponse> GetRISById(int risId, CancellationToken ct);
|
||||
Task<ApiResponse<object>> ApproveRIS(ApproveRISRequest request, CancellationToken ct);
|
||||
Task<ApiResponse<object>> CancelRIS(CancelRISRequest request, CancellationToken ct);
|
||||
Task<ApiResponse<object>> CreateRIS(CreateRISRequest request, CancellationToken ct);
|
||||
Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
96
CPRNIMS.Domain/UIServices/Inventory/InventoryReports.cs
Normal file
96
CPRNIMS.Domain/UIServices/Inventory/InventoryReports.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using CPRNIMS.Domain.UIContracts.Common;
|
||||
using CPRNIMS.Domain.UIContracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CPRNIMS.Domain.UIServices.Inventory
|
||||
{
|
||||
public class InventoryReports : IInventoryReports
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly TokenHelper _tokenHelper;
|
||||
private readonly IApiConfigurationService _apiConfigurationService;
|
||||
public InventoryReports(IConfiguration configuration, TokenHelper tokenHelper, IApiConfigurationService apiConfigurationService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_tokenHelper = tokenHelper;
|
||||
_apiConfigurationService = apiConfigurationService;
|
||||
}
|
||||
private static readonly JsonSerializerOptions _jsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
public async Task<InventoryReportDto> GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetInventoryReport"]
|
||||
?? throw new InvalidOperationException("GetInventoryReport endpoint is not configured.");
|
||||
|
||||
var qs = new StringBuilder(baseEndpoint).Append('?');
|
||||
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
|
||||
|
||||
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
var response = await http.GetAsync(qs.ToString(), ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return new InventoryReportDto();
|
||||
|
||||
var result = JsonSerializer.Deserialize<InventoryReportDto>(json, _jsonOpts);
|
||||
return result ?? new InventoryReportDto();
|
||||
}
|
||||
|
||||
public async Task<MRSReportDto> GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetMRSReport"]
|
||||
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
|
||||
|
||||
var qs = new StringBuilder(baseEndpoint).Append('?');
|
||||
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
|
||||
|
||||
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
var response = await http.GetAsync(qs.ToString(), ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return new MRSReportDto { };
|
||||
|
||||
var result = JsonSerializer.Deserialize<MRSReportDto>(json, _jsonOpts);
|
||||
return result ?? new MRSReportDto();
|
||||
}
|
||||
|
||||
public async Task<RISReportDto> GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetRISReport"]
|
||||
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
|
||||
|
||||
var qs = new StringBuilder(baseEndpoint).Append('?');
|
||||
qs.Append($"dateFrom={dateFrom:yyyy-MM-dd}&dateTo={dateTo:yyyy-MM-dd}");
|
||||
|
||||
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
var response = await http.GetAsync(qs.ToString(), ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return new RISReportDto {};
|
||||
|
||||
var result = JsonSerializer.Deserialize<RISReportDto>(json, _jsonOpts);
|
||||
return result ?? new RISReportDto();
|
||||
}
|
||||
}
|
||||
}
|
||||
145
CPRNIMS.Domain/UIServices/Inventory/MRS.cs
Normal file
145
CPRNIMS.Domain/UIServices/Inventory/MRS.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using CPRNIMS.Domain.Services;
|
||||
using CPRNIMS.Domain.UIContracts.Common;
|
||||
using CPRNIMS.Domain.UIContracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Domain.UIServices.Inventory
|
||||
{
|
||||
public class MRS : IMRS
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly TokenHelper _tokenHelper;
|
||||
private readonly IApiConfigurationService _apiConfigurationService;
|
||||
public MRS(IConfiguration configuration, TokenHelper tokenHelper,IApiConfigurationService apiConfigurationService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_tokenHelper = tokenHelper;
|
||||
_apiConfigurationService = apiConfigurationService;
|
||||
}
|
||||
private static readonly JsonSerializerOptions _mrsJsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public async Task<MRSPagedResponse> GetMRSPaged(MRSPagedRequest request, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var baseEndpoint = _configuration["LLI:NonInvent:InventoryMgmt:GetMRS"]
|
||||
?? throw new InvalidOperationException("GetMRS endpoint is not configured.");
|
||||
|
||||
var qs = new StringBuilder(baseEndpoint).Append('?');
|
||||
qs.Append($"pageNumber={request.PageNumber}&pageSize={request.PageSize}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SearchMRSNo))
|
||||
qs.Append($"&searchMRSNo={Uri.EscapeDataString(request.SearchMRSNo)}");
|
||||
if (!string.IsNullOrWhiteSpace(request.SearchRISNo))
|
||||
qs.Append($"&searchRISNo={Uri.EscapeDataString(request.SearchRISNo)}");
|
||||
if (!string.IsNullOrWhiteSpace(request.SearchItemName))
|
||||
qs.Append($"&searchItemName={Uri.EscapeDataString(request.SearchItemName)}");
|
||||
if (!string.IsNullOrWhiteSpace(request.SearchReturnedBy))
|
||||
qs.Append($"&searchReturnedBy={Uri.EscapeDataString(request.SearchReturnedBy)}");
|
||||
if (request.Status.HasValue)
|
||||
qs.Append($"&status={request.Status.Value}");
|
||||
if (!string.IsNullOrWhiteSpace(request.Condition))
|
||||
qs.Append($"&condition={Uri.EscapeDataString(request.Condition)}");
|
||||
|
||||
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
var response = await http.GetAsync(qs.ToString(), ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return new MRSPagedResponse { Data = [], RecordsTotal = 0 };
|
||||
|
||||
var result = JsonSerializer.Deserialize<MRSPagedResponse>(json, _mrsJsonOpts);
|
||||
return result ?? new MRSPagedResponse();
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<object>> CreateMRS(CreateMRSRequest request, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:CreateMRS"]
|
||||
?? throw new InvalidOperationException("CreateMRS endpoint is not configured.");
|
||||
|
||||
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
using var content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await http.PostAsync(endpoint, content, ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _mrsJsonOpts)
|
||||
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
result.success = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<object>> ApproveMRS(ApproveMRSRequest request, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:ApproveMRS"]
|
||||
?? throw new InvalidOperationException("ApproveMRS endpoint is not configured.");
|
||||
|
||||
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(request),
|
||||
Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await http.PostAsync(endpoint, content, ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _mrsJsonOpts)
|
||||
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
result.success = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<object>> CancelMRS(CancelMRSRequest request, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("Token has been expired.");
|
||||
|
||||
var endpoint = _configuration["LLI:NonInvent:InventoryMgmt:CancelMRS"]
|
||||
?? throw new InvalidOperationException("CancelMRS endpoint is not configured.");
|
||||
|
||||
using var http = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(request),
|
||||
Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await http.PostAsync(endpoint, content, ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _mrsJsonOpts)
|
||||
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
result.success = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ using CPRNIMS.Domain.UIContracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
|
||||
using CPRNIMS.Infrastructure.Dto.PR.Response;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
@ -29,8 +30,6 @@ namespace CPRNIMS.Domain.UIServices.Inventory
|
||||
}
|
||||
#region Get
|
||||
public async Task<RISPagedResponse> GetRISPaged(RISPagedRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
@ -74,16 +73,9 @@ namespace CPRNIMS.Domain.UIServices.Inventory
|
||||
var result = JsonSerializer.Deserialize<RISPagedResponse>(json, _jsonOptions);
|
||||
return result ?? new RISPagedResponse();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.ToString();
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
#endregion
|
||||
#region Post Put
|
||||
public async Task<bool> ApproveRIS(ApproveRISRequest request, CancellationToken ct)
|
||||
public async Task<ApiResponse<object>> ApproveRIS(ApproveRISRequest request, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
@ -97,15 +89,16 @@ namespace CPRNIMS.Domain.UIServices.Inventory
|
||||
|
||||
var response = await httpClient.PutAsync(endpoint, content, ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _jsonOptions)
|
||||
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
result.success = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> CancelRIS(CancelRISRequest request, CancellationToken ct)
|
||||
public async Task<ApiResponse<object>> CancelRIS(CancelRISRequest request, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
@ -115,19 +108,21 @@ namespace CPRNIMS.Domain.UIServices.Inventory
|
||||
?? throw new InvalidOperationException("CancelRIS endpoint is not configured.");
|
||||
|
||||
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(request),Encoding.UTF8,"application/json");
|
||||
using var content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await httpClient.PutAsync(endpoint, content, ct);
|
||||
var json = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _jsonOptions)
|
||||
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return false;
|
||||
result.success = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public async Task<RISResponse> CreateRIS(CreateRISRequest request, CancellationToken ct)
|
||||
|
||||
public async Task<ApiResponse<object>> CreateRIS(CreateRISRequest request, CancellationToken ct)
|
||||
{
|
||||
var token = await _tokenHelper.GetValidTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
@ -138,27 +133,19 @@ namespace CPRNIMS.Domain.UIServices.Inventory
|
||||
throw new InvalidOperationException("CreateRIS endpoint is not configured.");
|
||||
|
||||
using var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token);
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(request),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
using var content = new StringContent(JsonSerializer.Serialize(request),Encoding.UTF8,"application/json");
|
||||
|
||||
var response = await httpClient.PostAsync(endpoint, content,ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new RISResponse();
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var result = JsonSerializer.Deserialize<ApiResponse<RISResponse>>(json, _jsonOptions);
|
||||
return result?.data ?? new RISResponse();
|
||||
}
|
||||
var result = JsonSerializer.Deserialize<ApiResponse<object>>(json, _jsonOptions)
|
||||
?? new ApiResponse<object> { success = false, message = $"HTTP {(int)response.StatusCode}" };
|
||||
|
||||
public Task<RISPagedResponse> GetRISById(int risId, CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
result.success = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
|
||||
@ -198,7 +198,7 @@ namespace CPRNIMS.Infrastructure.Database
|
||||
b.HasOne(u => u.Attachment)
|
||||
.WithOne(a => a.ApplicationUser)
|
||||
.HasForeignKey<Attachment>(a => a.AttachmentId)
|
||||
.IsRequired(false); // Allow null if there is no attachment
|
||||
.IsRequired(false);
|
||||
});
|
||||
modelBuilder.Entity<Attachment>(b =>
|
||||
{
|
||||
@ -206,7 +206,7 @@ namespace CPRNIMS.Infrastructure.Database
|
||||
b.HasOne(a => a.AttachmentExtention)
|
||||
.WithOne()
|
||||
.HasForeignKey<Attachment>(a => a.ExtensionId)
|
||||
.IsRequired(false); // Allow null if there is no extension
|
||||
.IsRequired(false);
|
||||
});
|
||||
modelBuilder.Entity<ApplicationUser>(b =>
|
||||
{
|
||||
@ -270,13 +270,23 @@ namespace CPRNIMS.Infrastructure.Database
|
||||
.HasForeignKey(i => i.LotId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<InventTrans>(e =>
|
||||
{
|
||||
e.HasMany(r => r.InventTransDetails)
|
||||
.WithOne()
|
||||
.HasForeignKey(t => t.InventTransId)
|
||||
modelBuilder.Entity<Inventory>()
|
||||
.HasOne(i => i.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.ItemNo)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<Item>()
|
||||
.HasOne(i => i.ItemCode)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.ItemCodeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<ItemCode>()
|
||||
.HasOne(i => i.ItemCategory)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.ItemCategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<InventTransDetail>(e =>
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -20,6 +21,7 @@ namespace CPRNIMS.Infrastructure.Dto.Account
|
||||
public string URLAttachment { get; set; } = string.Empty;
|
||||
public string? token { get; set; }
|
||||
public string? company { get; set; }
|
||||
public int? departmentId { get; set; }
|
||||
public string? refreshToken { get; set; }
|
||||
public DateTime expiresAt { get; set; }
|
||||
public int expiresInSeconds { get; set; }
|
||||
|
||||
@ -13,5 +13,6 @@ namespace CPRNIMS.Infrastructure.Dto.Account
|
||||
public string FullName { get; init; } = default!;
|
||||
public string Company { get; init; } = default!;
|
||||
public IReadOnlyList<string> Roles { get; init; } = [];
|
||||
public int? DepartmentId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ namespace CPRNIMS.Infrastructure.Dto.Inventory
|
||||
{
|
||||
public class MRSFilterDto
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 12;
|
||||
public string? SearchMRSNo { get; set; }
|
||||
public long? RISId { get; set; }
|
||||
|
||||
30
CPRNIMS.Infrastructure/Dto/Inventory/MRSPagedDto.cs
Normal file
30
CPRNIMS.Infrastructure/Dto/Inventory/MRSPagedDto.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory
|
||||
{
|
||||
public class MRSPagedDto
|
||||
{
|
||||
public long MRSId { get; set; }
|
||||
public string MRSNo { get; set; } = string.Empty;
|
||||
public long RISId { get; set; }
|
||||
public string? RISNo { get; set; }
|
||||
public string? ItemName { get; set; }
|
||||
public string? ReturnedBy { get; set; }
|
||||
public decimal QtyReturned { get; set; }
|
||||
public string? Condition { get; set; }
|
||||
public string? Remarks { get; set; }
|
||||
public short Status { get; set; }
|
||||
public string? StatusLabel { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string? ApprovedBy { get; set; }
|
||||
public DateTime? ApprovedDate { get; set; }
|
||||
public int InventoryId { get; set; }
|
||||
public List<MRSPagedDto> Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
20
CPRNIMS.Infrastructure/Dto/Inventory/MRSPagedRequest.cs
Normal file
20
CPRNIMS.Infrastructure/Dto/Inventory/MRSPagedRequest.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory
|
||||
{
|
||||
public class MRSPagedRequest
|
||||
{
|
||||
public string? SearchMRSNo { get; set; }
|
||||
public string? SearchRISNo { get; set; }
|
||||
public string? SearchItemName { get; set; }
|
||||
public string? SearchReturnedBy { get; set; }
|
||||
public short? Status { get; set; }
|
||||
public string? Condition { get; set; }
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 12;
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,7 @@ namespace CPRNIMS.Infrastructure.Dto.Inventory
|
||||
{
|
||||
public class MRSPagedResult
|
||||
{
|
||||
public IEnumerable<MRSResponse> Data { get; set; } = [];
|
||||
public IEnumerable<MRSPagedDto> Data { get; set; } = [];
|
||||
public int RecordsTotal { get; set; }
|
||||
public List<string> DepartmentList { get; set; } = new List<string>();
|
||||
public List<string> DisciplineList { get; set; } = new List<string>();
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports
|
||||
{
|
||||
public class RISReportDataDto
|
||||
{
|
||||
public List<RISRowDto> Rows { get; set; } = new();
|
||||
public List<DisciplineAggDto> Disciplines { get; set; } = new();
|
||||
public List<TopRecipientDto> Recipients { get; set; } = new();
|
||||
}
|
||||
public class RISRowDto
|
||||
{
|
||||
public string? RISNo { get; set; }
|
||||
public long? PRNo { get; set; }
|
||||
public decimal QtyIssued { get; set; }
|
||||
public string? IssuedTo { get; set; }
|
||||
public string? StatusLabel { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public string? ApprovedBy { get; set; }
|
||||
public DateTime? ApprovedDate { get; set; }
|
||||
public DateTime? CreatedDate { get; set; }
|
||||
public string? DisciplineName { get; set; }
|
||||
public string? ItemName { get; set; }
|
||||
public long? ItemNo { get; set; }
|
||||
public decimal QtyIn { get; set; }
|
||||
public decimal QtyOut { get; set; }
|
||||
public decimal QtyOnHand { get; set; }
|
||||
public string? DepartmentName { get; set; }
|
||||
public decimal TotalReturned { get; set; }
|
||||
public int MRSCount { get; set; }
|
||||
public decimal NetIssued { get; set; }
|
||||
}
|
||||
|
||||
public class DisciplineAggDto
|
||||
{
|
||||
public string? DisciplineName { get; set; }
|
||||
public int SlipCount { get; set; }
|
||||
}
|
||||
|
||||
public class TopRecipientDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public int SlipCount { get; set; }
|
||||
public decimal QtyOut { get; set; }
|
||||
}
|
||||
}
|
||||
152
CPRNIMS.Infrastructure/Dto/Inventory/Reports/ReportDtos.cs
Normal file
152
CPRNIMS.Infrastructure/Dto/Inventory/Reports/ReportDtos.cs
Normal file
@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports
|
||||
{
|
||||
public class RISReportDto
|
||||
{
|
||||
public string CompanyName { get; set; } = "Lloyd Laboratories Incorporated";
|
||||
public string PreparedBy { get; set; } = "Finance Department";
|
||||
public string ReportNo { get; set; } = string.Empty;
|
||||
public DateTime DateFrom { get; set; }
|
||||
public DateTime DateTo { get; set; }
|
||||
|
||||
public RISReportSummary Summary { get; set; } = new();
|
||||
public List<RISReportRow> Rows { get; set; } = [];
|
||||
public List<DisciplineCount> ByDiscipline { get; set; } = [];
|
||||
public List<TopRecipient> TopRecipients { get; set; } = [];
|
||||
}
|
||||
|
||||
public class RISReportSummary
|
||||
{
|
||||
public int TotalRIS { get; set; }
|
||||
public int TotalApproved { get; set; }
|
||||
public int TotalPending { get; set; }
|
||||
public int TotalCancelled { get; set; }
|
||||
public decimal TotalQtyIssued { get; set; }
|
||||
public decimal TotalQtyReturned { get; set; }
|
||||
public decimal TotalNetIssued { get; set; }
|
||||
public decimal ApprovalRatePct { get; set; }
|
||||
}
|
||||
|
||||
public class RISReportRow
|
||||
{
|
||||
public string RISNo { get; set; } = string.Empty;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
public long ItemNo { get; set; }
|
||||
public string DisciplineName { get; set; } = string.Empty;
|
||||
public string IssuedTo { get; set; } = string.Empty;
|
||||
public decimal QtyIssued { get; set; }
|
||||
public decimal TotalReturned { get; set; }
|
||||
public decimal NetIssued { get; set; }
|
||||
public short Status { get; set; }
|
||||
public string StatusLabel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DisciplineCount
|
||||
{
|
||||
public string DisciplineName { get; set; } = string.Empty;
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class TopRecipient
|
||||
{
|
||||
public string IssuedTo { get; set; } = string.Empty;
|
||||
public int SlipCount { get; set; }
|
||||
public decimal TotalQty { get; set; }
|
||||
}
|
||||
|
||||
public class MRSReportDto
|
||||
{
|
||||
public string CompanyName { get; set; } = "Lloyd Laboratories Incorporated";
|
||||
public string PreparedBy { get; set; } = "Finance Department";
|
||||
public string ReportNo { get; set; } = string.Empty;
|
||||
public DateTime DateFrom { get; set; }
|
||||
public DateTime DateTo { get; set; }
|
||||
|
||||
public MRSReportSummary Summary { get; set; } = new();
|
||||
public List<MRSReportRow> Rows { get; set; } = [];
|
||||
public List<ConditionTotal> ByCondition { get; set; } = [];
|
||||
}
|
||||
|
||||
public class MRSReportSummary
|
||||
{
|
||||
public int TotalMRS { get; set; }
|
||||
public decimal TotalQtyReturned { get; set; }
|
||||
public decimal TotalQtyIssuedRIS { get; set; }
|
||||
public decimal NetQtyConsumed { get; set; }
|
||||
public decimal ReturnRatePct { get; set; }
|
||||
public decimal GoodConditionPct { get; set; }
|
||||
}
|
||||
|
||||
public class MRSReportRow
|
||||
{
|
||||
public string MRSNo { get; set; } = string.Empty;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string RISNo { get; set; } = string.Empty;
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
public string ReturnedBy { get; set; } = string.Empty;
|
||||
public decimal QtyReturned { get; set; }
|
||||
public string Condition { get; set; } = string.Empty;
|
||||
public short Status { get; set; }
|
||||
public string StatusLabel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ConditionTotal
|
||||
{
|
||||
public string Condition { get; set; } = string.Empty;
|
||||
public decimal TotalQty { get; set; }
|
||||
}
|
||||
|
||||
public class InventoryReportDto
|
||||
{
|
||||
public string CompanyName { get; set; } = "Lloyd Laboratories Incorporated";
|
||||
public string PreparedBy { get; set; } = "Finance Department";
|
||||
public string ReportNo { get; set; } = string.Empty;
|
||||
public DateTime AsOf { get; set; }
|
||||
|
||||
public InventoryReportSummary Summary { get; set; } = new();
|
||||
public List<InventoryReportRow> Rows { get; set; } = [];
|
||||
public List<CategoryStockLevel> ByCategory { get; set; } = [];
|
||||
public List<InventoryAlert> Alerts { get; set; } = [];
|
||||
}
|
||||
|
||||
public class InventoryReportSummary
|
||||
{
|
||||
public int TotalSKUs { get; set; }
|
||||
public decimal TotalOnHand { get; set; }
|
||||
public decimal TotalQtyIn { get; set; }
|
||||
public decimal TotalQtyOut { get; set; }
|
||||
public int LowStockCount { get; set; }
|
||||
public int OutOfStockCount { get; set; }
|
||||
}
|
||||
|
||||
public class InventoryReportRow
|
||||
{
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
public long ItemNo { get; set; }
|
||||
public string ItemCategoryName { get; set; } = string.Empty;
|
||||
public string? LotNo { get; set; }
|
||||
public decimal QtyIn { get; set; }
|
||||
public decimal QtyOut { get; set; }
|
||||
public decimal QtyOnHand { get; set; }
|
||||
public int StockPct { get; set; }
|
||||
}
|
||||
|
||||
public class CategoryStockLevel
|
||||
{
|
||||
public string CategoryName { get; set; } = string.Empty;
|
||||
public int AvgStockPct { get; set; }
|
||||
}
|
||||
|
||||
public class InventoryAlert
|
||||
{
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
public decimal QtyOnHand { get; set; }
|
||||
public string Severity { get; set; } = string.Empty; // "Critical" | "Low"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Response;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response
|
||||
{
|
||||
public class InventoryData
|
||||
{
|
||||
public List<InventoryReportDto> Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response
|
||||
{
|
||||
public class MRSData
|
||||
{
|
||||
public List<MRSReportDto> Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Reports.Response
|
||||
{
|
||||
public class RISData
|
||||
{
|
||||
public List<RISReportDto> Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
|
||||
{
|
||||
public class ApproveMRSRequest { public long MRSId { get; set; } }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Request
|
||||
{
|
||||
public class CancelMRSRequest
|
||||
{
|
||||
public long MRSId { get; set; }
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
|
||||
{
|
||||
public class MRSPagedResponse
|
||||
{
|
||||
public List<MRSListItem> Data { get; set; } = [];
|
||||
public int RecordsTotal { get; set; }
|
||||
}
|
||||
public class MRSListItem
|
||||
{
|
||||
public long MRSId { get; set; }
|
||||
public string MRSNo { get; set; } = string.Empty;
|
||||
public long RISId { get; set; }
|
||||
public string? RISNo { get; set; }
|
||||
public string? ItemName { get; set; }
|
||||
public string? ReturnedBy { get; set; }
|
||||
public decimal QtyReturned { get; set; }
|
||||
public string? Condition { get; set; }
|
||||
public string? Remarks { get; set; }
|
||||
public short Status { get; set; }
|
||||
public string? StatusLabel { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string? ApprovedBy { get; set; }
|
||||
public DateTime? ApprovedDate { get; set; }
|
||||
public int InventoryId { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,17 @@
|
||||
using System;
|
||||
using CPRNIMS.Infrastructure.Models.Inventory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Inventory.Response
|
||||
{
|
||||
public class MRSResponse
|
||||
{
|
||||
public long MRSId { get; set; }
|
||||
public string MRSNo { get; set; } = string.Empty;
|
||||
public long RISId { get; set; }
|
||||
public string RISNo { get; set; } = string.Empty;
|
||||
public int InventoryId { get; set; }
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
public string ReturnedBy { get; set; } = string.Empty;
|
||||
public decimal QtyReturned { get; set; }
|
||||
public string? Condition { get; set; }
|
||||
public string? Remarks { get; set; }
|
||||
public short Status { get; set; }
|
||||
public string StatusLabel => Status switch { 0 => "Draft", 1 => "Approved", 2 => "Cancelled", _ => "Unknown" };
|
||||
public string CreatedBy { get; set; } = string.Empty;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string? ApprovedBy { get; set; }
|
||||
public DateTime? ApprovedDate { get; set; }
|
||||
[JsonPropertyName("success")] public bool Success { get; set; }
|
||||
[JsonPropertyName("message")] public string? Message { get; set; }
|
||||
[JsonPropertyName("data")] public MRSData? Data { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
|
||||
public long RRNo { get; set; }
|
||||
public string CreatedBy { get; set; }=string.Empty;
|
||||
public bool IsActive { get; set; }
|
||||
public Inventory? Inventory { get; set; }
|
||||
// public InventTransDetail? InventTransDetail { get; set; }
|
||||
public ICollection<InventTransDetail> InventTransDetails { get; set; } = [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CPRNIMS.Infrastructure.Entities.Purchasing;
|
||||
using CPRNIMS.Infrastructure.Entities.Items;
|
||||
using CPRNIMS.Infrastructure.Entities.Purchasing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -24,5 +25,7 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
|
||||
public bool IsActive { get; set; }
|
||||
public long PRDetailId { get; set; }
|
||||
public PRDetails? PRDetails { get; set; }
|
||||
public ItemCategory? ItemCategory { get; set; }
|
||||
public InventTrans? InventTrans { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CPRNIMS.Infrastructure.Entities.Account;
|
||||
using CPRNIMS.Infrastructure.Entities.Items;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -24,5 +25,6 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
|
||||
public Lot? Lot { get; set; }
|
||||
public ICollection<InventTrans> InventTrans { get; set; } = [];
|
||||
public ApplicationUser? User { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,5 +42,4 @@ namespace CPRNIMS.Infrastructure.Entities.Inventory
|
||||
public RIS RIS { get; set; } = null!;
|
||||
public Inventory Inventory { get; set; } = null!;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
using CPRNIMS.Infrastructure.Entities.Account;
|
||||
using CPRNIMS.Infrastructure.Entities.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CPRNIMS.Infrastructure.Entities.Common;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Entities.Items
|
||||
{
|
||||
@ -42,5 +36,6 @@ namespace CPRNIMS.Infrastructure.Entities.Items
|
||||
public string? ItemAttachPath { get; set; }
|
||||
public bool IsMDLD { get; set; }
|
||||
public bool CheckBox { get; set; }
|
||||
public ItemCode? ItemCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,5 +23,7 @@ namespace CPRNIMS.Infrastructure.Entities.Items
|
||||
public byte RequestTypeId { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public int CartItemCount { get; set; }
|
||||
public short ItemCategoryId { get; set; }
|
||||
public ItemCategory? ItemCategory { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Entities.Purchasing
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CPRNIMS.Infrastructure.Entities.Common;
|
||||
using CPRNIMS.Infrastructure.Entities.Items;
|
||||
using CPRNIMS.Infrastructure.ViewModel.Items;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -24,5 +25,6 @@ namespace CPRNIMS.Infrastructure.Entities.Purchasing
|
||||
public decimal Qty { get; set; }
|
||||
public bool IsSearched { get; set; }
|
||||
public PR? PRs { get; set; }
|
||||
public ItemCategory? ItemCategory { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
26
CPRNIMS.Infrastructure/Models/Inventory/MRSData.cs
Normal file
26
CPRNIMS.Infrastructure/Models/Inventory/MRSData.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Models.Inventory
|
||||
{
|
||||
public class MRSData
|
||||
{
|
||||
[JsonPropertyName("mrsId")] public long MRSId { get; set; }
|
||||
[JsonPropertyName("mrsNo")] public string? MRSNo { get; set; }
|
||||
[JsonPropertyName("risId")] public long RISId { get; set; }
|
||||
[JsonPropertyName("inventoryId")] public int InventoryId { get; set; }
|
||||
[JsonPropertyName("returnedBy")] public string? ReturnedBy { get; set; }
|
||||
[JsonPropertyName("qtyReturned")] public decimal QtyReturned { get; set; }
|
||||
[JsonPropertyName("condition")] public string? Condition { get; set; }
|
||||
[JsonPropertyName("remarks")] public string? Remarks { get; set; }
|
||||
[JsonPropertyName("status")] public short Status { get; set; }
|
||||
[JsonPropertyName("createdBy")] public string? CreatedBy { get; set; }
|
||||
[JsonPropertyName("createdDate")] public DateTime CreatedDate { get; set; }
|
||||
[JsonPropertyName("approvedBy")] public string? ApprovedBy { get; set; }
|
||||
[JsonPropertyName("approvedDate")] public DateTime? ApprovedDate { get; set; }
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastReport.OpenSource" Version="2026.2.3" />
|
||||
<PackageReference Include="FastReport.OpenSource.Export.PdfSimple" Version="2026.2.3" />
|
||||
<PackageReference Include="FastReport.OpenSource.Web" Version="2026.2.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ActiveDebugProfile>https</ActiveDebugProfile>
|
||||
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
|
||||
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
|
||||
<NameOfLastUsedPublishProfile>D:\sourcecode\NonInventPurchasing\CPRNIMS.WebApi\Properties\PublishProfiles\FolderProfile1.pubxml</NameOfLastUsedPublishProfile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
using CPRNIMS.Domain.Contracts.Account;
|
||||
using CPRNIMS.Domain.Contracts.Canvass;
|
||||
using CPRNIMS.Domain.Contracts.Common;
|
||||
using CPRNIMS.Domain.Contracts.Finance;
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Domain.Contracts.Items;
|
||||
using CPRNIMS.Domain.Contracts.PO;
|
||||
using CPRNIMS.Domain.Contracts.PR;
|
||||
using CPRNIMS.Domain.Contracts.Receiving;
|
||||
using CPRNIMS.Domain.Contracts.Reports;
|
||||
using CPRNIMS.Domain.Contracts.SMTP;
|
||||
using CPRNIMS.Domain.Services;
|
||||
using CPRNIMS.Domain.Services.Account;
|
||||
using CPRNIMS.Domain.Services.Canvass;
|
||||
using CPRNIMS.Domain.Services.Common;
|
||||
using CPRNIMS.Domain.Services.Finance;
|
||||
using CPRNIMS.Domain.Services.Inventory;
|
||||
using CPRNIMS.Domain.Services.PO;
|
||||
@ -18,6 +21,7 @@ using CPRNIMS.Domain.Services.SMTP;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using CPRNIMS.Infrastructure.Entities.Account;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using CPRNIMS.Infrastructure.Reports;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -89,7 +93,6 @@ namespace CPRNIMS.WebApi.Common
|
||||
sql.EnableRetryOnFailure(5, TimeSpan.FromHours(2), null);
|
||||
sql.CommandTimeout(20);
|
||||
}));
|
||||
|
||||
services.AddDbContext<PurchLocalDbContext>(options =>
|
||||
options.UseSqlServer(localConn, sql =>
|
||||
{
|
||||
@ -147,6 +150,7 @@ namespace CPRNIMS.WebApi.Common
|
||||
{
|
||||
services.AddMemoryCache();
|
||||
services.AddScoped<IRoleAuthorizationCache, RoleAuthorizationCache>();
|
||||
services.AddScoped<ITransactionFacade, TransactionFacade>();
|
||||
services.AddScoped<IDepartment, Department>();
|
||||
services.AddScoped<IAttachment, Domain.Services.Account.Attachment>();
|
||||
services.AddScoped<IItem, Domain.Services.Items.Item>();
|
||||
@ -154,7 +158,8 @@ namespace CPRNIMS.WebApi.Common
|
||||
services.AddScoped<ICanvass, Canvass>();
|
||||
services.AddScoped<IRIS, RIS>();
|
||||
services.AddScoped<IMRS, MRS>();
|
||||
|
||||
services.AddScoped<IReportDataService, ReportDataService>();
|
||||
services.AddScoped<IInventoryReports, InventoryReports>();
|
||||
#region Automation using LLM
|
||||
services.AddHttpClient<SupplierSearchService>();
|
||||
#endregion
|
||||
|
||||
@ -67,6 +67,7 @@ namespace CPRNIMS.WebApi.Controllers.Account
|
||||
userId = user.Id,
|
||||
userName = user.UserName,
|
||||
fullName = user.FullName,
|
||||
departmentId = user.DepartmentId,
|
||||
email = user.Email,
|
||||
phoneNumber = user.PhoneNumber,
|
||||
company = user.Company,
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
using CPRNIMS.Domain.Services;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.PR;
|
||||
using CPRNIMS.WebApi.Controllers.Base;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Domain.Contracts.Reports;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Reports;
|
||||
using CPRNIMS.WebApi.Security;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class InventoryReportsController : ControllerBase
|
||||
{
|
||||
private readonly IReportDataService _data;
|
||||
private readonly IInventoryReports _reports;
|
||||
public InventoryReportsController(IReportDataService data, IInventoryReports reports)
|
||||
{
|
||||
_data = data;
|
||||
_reports = reports;
|
||||
}
|
||||
|
||||
[HttpGet("GetRISReport")]
|
||||
public async Task<IActionResult> GetRISReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
var data = await _reports.GetRISReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName);
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
[HttpGet("GetMRSReport")]
|
||||
public async Task<IActionResult> GetMRSReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
var data = await _reports.GetMRSReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName);
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
[HttpGet("GetInventoryReport")]
|
||||
public async Task<IActionResult> GetInventoryReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
var data = await _reports.GetInventoryReportAsync(dateFrom, dateTo, ct, currentUser.DepartmentId, currentUser.UserName);
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
[HttpGet("GetRISData")]
|
||||
public IActionResult GetData([FromQuery] DateTime dateFrom, [FromQuery] DateTime dateTo,CancellationToken ct)
|
||||
{
|
||||
var result = new RISReportDataDto
|
||||
{
|
||||
Rows = _data.GetMain(dateFrom, dateTo),
|
||||
Disciplines = _data.GetDisciplines(dateFrom, dateTo),
|
||||
Recipients = _data.GetRecipients(dateFrom, dateTo)
|
||||
};
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,63 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CPRNIMS.Domain.Contracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.WebApi.Security;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
{
|
||||
public class MRSMgmtController : Controller
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public partial class MRSMgmtController : ControllerBase
|
||||
{
|
||||
public IActionResult Index()
|
||||
private readonly IMRS _mrs;
|
||||
public MRSMgmtController(IMRS mrs) => _mrs = mrs;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMRS([FromQuery] MRSFilterDto filter, CancellationToken ct)
|
||||
{
|
||||
return View();
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
var result = await _mrs.GetPagedAsync(filter, ct, currentUser.DepartmentId, currentUser.UserName);
|
||||
return Ok(result);
|
||||
}
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> CreateMRS([FromBody] CreateMRSRequest request, CancellationToken ct)
|
||||
{
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
var result = await _mrs.CreateAsync(request, currentUser.UserName, ct);
|
||||
return Ok(new { success = true, message = $"MRS# {result.MRSNo} created successfully." });
|
||||
}
|
||||
|
||||
[HttpPost("Approve")]
|
||||
public async Task<IActionResult> ApproveMRS([FromBody] ApproveMRSRequest request, CancellationToken ct)
|
||||
{
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
await _mrs.ApproveAsync(request.MRSId, currentUser.UserName, ct);
|
||||
return Ok(new { success = true, message = "MRS approved successfully." });
|
||||
}
|
||||
|
||||
[HttpPost("Cancel")]
|
||||
public async Task<IActionResult> CancelMRS([FromBody] CancelMRSRequest request, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Reason))
|
||||
return BadRequest(new { success = false, message = "A reason for cancellation is required." });
|
||||
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
await _mrs.CancelAsync(request, currentUser.UserName, ct);
|
||||
return Ok(new {success = true,message = "MRS cancelled and inventory adjusted."});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.WebApi.Security;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
{
|
||||
@ -20,7 +19,11 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
[HttpGet("GetRIS")]
|
||||
public async Task<IActionResult> GetRIS([FromQuery] RISFilterDto filter, CancellationToken ct)
|
||||
{
|
||||
var result = await _risRepo.GetPagedAsync(filter,ct);
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
var result = await _risRepo.GetPagedAsync(filter,ct, currentUser.DepartmentId, currentUser.UserName);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
@ -48,12 +51,7 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
});
|
||||
|
||||
var ris = await _risRepo.CreateAsync(request, currentUser.UserName,ct);
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = $"RIS {ris.RISNo} created successfully.",
|
||||
data= ris
|
||||
});
|
||||
return Ok(new { success = true, message = $"RIS# {ris.RISNo} created successfully." });
|
||||
}
|
||||
|
||||
[HttpPut("ApproveRIS")]
|
||||
@ -69,7 +67,11 @@ namespace CPRNIMS.WebApi.Controllers.Inventory
|
||||
[HttpPut("CancelRIS")]
|
||||
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request, CancellationToken ct)
|
||||
{
|
||||
await _risRepo.CancelAsync(request, ct);
|
||||
var currentUser = User.ToUserClaims();
|
||||
if (currentUser == null)
|
||||
return BadRequest();
|
||||
|
||||
await _risRepo.CancelAsync(request, currentUser.UserName, ct);
|
||||
return Ok(new { success = true, message = "RIS cancelled and inventory restored." });
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ using CPRNIMS.WebApi.Common;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddApplicationServices(builder);
|
||||
builder.Services.AddFastReport();
|
||||
|
||||
builder.Services.AddAutoMapper(cfg => { }, typeof(SupplierRequestProfile));
|
||||
|
||||
@ -30,6 +31,8 @@ app.UseCors("AllowAnyOrigin");
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseFastReport();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
@ -16,6 +16,7 @@ namespace CPRNIMS.WebApi.Security
|
||||
UserName = user.FindFirstValue(ClaimTypes.Name) ?? "",
|
||||
FullName = user.FindFirstValue("FullName") ?? "",
|
||||
Company = user.FindFirstValue("Company") ?? "",
|
||||
DepartmentId = Convert.ToInt32(user.FindFirstValue("DepartmentId")),
|
||||
Roles = user.FindAll(ClaimTypes.Role)
|
||||
.Select(r => r.Value)
|
||||
.ToList()
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
USE [CPRNIMS]
|
||||
GO
|
||||
/****** Object: StoredProcedure [dbo].[GetInventory] Script Date: 6/11/2026 9:29:29 AM ******/
|
||||
/****** Object: StoredProcedure [dbo].[GetInventory] Script Date: 6/18/2026 3:09:49 PM ******/
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
ALTER PROCEDURE [dbo].[GetInventory]
|
||||
(
|
||||
@UserId VARCHAR(450)='89da2977-c70f-4df9-94d4-9a610aa999ea',
|
||||
@UserId VARCHAR(450)='16b37c00-131f-4205-b8f6-ad4d0f9f3a32',
|
||||
@SearchPRNo VARCHAR(50) = '',
|
||||
@SearchItemNo VARCHAR(50) = '',
|
||||
@SearchItemName VARCHAR(100) = '',
|
||||
@ -20,6 +20,18 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
DECLARE @Offset INT = (@PageNumber - 1) * @PageSize;
|
||||
DECLARE @HasFullAccess BIT = 0;
|
||||
DECLARE @UserDepartmentId INT,@UserName VARCHAR(50);
|
||||
|
||||
SELECT @UserDepartmentId = DepartmentId,@UserName=UserName
|
||||
FROM dbo.Users
|
||||
WHERE Id = @UserId;
|
||||
|
||||
IF @UserName IN ('LSKRISUR24','LSCYNDIZ25','LSJONTAN25','LHRIOCAS24')
|
||||
SET @HasFullAccess = 1;
|
||||
|
||||
IF @UserDepartmentId = 6
|
||||
SET @HasFullAccess = 1;
|
||||
|
||||
-- ── 1. Full department list — unaffected by any filter ──────────────
|
||||
SELECT DISTINCT D.Department
|
||||
@ -49,38 +61,26 @@ BEGIN
|
||||
, ICAT.ItemCategoryName
|
||||
, RRD.RemainingQty
|
||||
, ITD.CreatedDate
|
||||
,COALESCE(PCD.ProjectCode,'N/A') ProjectCode
|
||||
,COALESCE(PC.ProjectCode,'N/A') ProjectCode
|
||||
INTO #Inventory
|
||||
FROM dbo.Inventory IV
|
||||
INNER JOIN dbo.InventTrans IT
|
||||
ON IV.InventoryId = IT.InventoryId
|
||||
INNER JOIN dbo.InventTransDetail ITD
|
||||
ON IT.InventTransId = ITD.InventTransId
|
||||
INNER JOIN dbo.PRDetails PRD
|
||||
ON ITD.PRDetailId = PRD.PRDetailsId
|
||||
AND PRD.IsActive = 1
|
||||
INNER JOIN dbo.RRDetails RRD
|
||||
ON PRD.PRDetailsId = RRD.PRDetailId
|
||||
AND RRD.IsActive = 1
|
||||
INNER JOIN dbo.ItemCategories ICAT
|
||||
ON PRD.ItemCategoryId = ICAT.ItemCategoryId
|
||||
INNER JOIN dbo.Lot L
|
||||
ON IV.LotId = L.LotId
|
||||
INNER JOIN dbo.LotType LT
|
||||
ON L.LotTypeId = LT.LotTypeId
|
||||
INNER JOIN dbo.PR PR
|
||||
ON PR.UserId = IV.UserId
|
||||
AND PR.IsActive = 1
|
||||
INNER JOIN dbo.ProjectCodes PC
|
||||
ON PR.ProjectCodeId = PC.ProjectCodeId
|
||||
INNER JOIN dbo.Users U
|
||||
ON IV.UserId = U.Id
|
||||
INNER JOIN dbo.Departments D
|
||||
ON U.DepartmentId = D.DepartmentId
|
||||
INNER JOIN dbo.ProjectCodes PCD
|
||||
ON PR.ProjectCodeId = PCD.ProjectCodeId
|
||||
WHERE ITD.TransTypeId=2 AND IV.IsActive=1 AND ITD.IsActive=1 AND
|
||||
(@SearchPRNo = '' OR PR.PRNo = @SearchPRNo)
|
||||
INNER JOIN dbo.InventTrans IT ON IV.InventoryId = IT.InventoryId
|
||||
INNER JOIN dbo.InventTransDetail ITD ON IT.InventTransId = ITD.InventTransId
|
||||
INNER JOIN dbo.PRDetails PRD ON ITD.PRDetailId = PRD.PRDetailsId AND PRD.IsActive = 1
|
||||
INNER JOIN dbo.PR ON PR.PRId=PRD.PRId AND PR.IsActive = 1
|
||||
INNER JOIN dbo.RRDetails RRD ON PRD.PRDetailsId = RRD.PRDetailId AND RRD.IsActive = 1
|
||||
INNER JOIN dbo.ItemCategories ICAT ON PRD.ItemCategoryId = ICAT.ItemCategoryId
|
||||
LEFT JOIN dbo.Lot L ON IV.LotId = L.LotId
|
||||
LEFT JOIN dbo.LotType LT ON L.LotTypeId = LT.LotTypeId
|
||||
LEFT JOIN dbo.ProjectCodes PC ON PR.ProjectCodeId = PC.ProjectCodeId
|
||||
INNER JOIN dbo.Users U ON IV.UserId = U.Id
|
||||
INNER JOIN dbo.Departments D ON U.DepartmentId = D.DepartmentId
|
||||
WHERE ITD.TransTypeId = 2 AND IV.IsActive = 1 AND ITD.IsActive = 1
|
||||
AND (
|
||||
@HasFullAccess = 1
|
||||
OR D.DepartmentId = @UserDepartmentId
|
||||
)
|
||||
AND (@SearchPRNo = '' OR PR.PRNo = @SearchPRNo)
|
||||
AND (@SearchItemNo = '' OR PRD.ItemNo = @SearchItemNo)
|
||||
AND (@SearchItemName = '' OR PRD.ItemName LIKE '%' + @SearchItemName + '%')
|
||||
AND (@SearchDept = '' OR D.Department LIKE '%' + @SearchDept + '%')
|
||||
@ -100,7 +100,7 @@ BEGIN
|
||||
, ICAT.ItemCategoryName
|
||||
, RRD.RemainingQty
|
||||
, ITD.CreatedDate
|
||||
,PCD.ProjectCode;
|
||||
,PC.ProjectCode;
|
||||
|
||||
-- ── 3. Total count of filtered results ──────────────────────────────
|
||||
SELECT COUNT(*) AS TotalCount
|
||||
|
||||
214
CPRNIMS.WebApi/wwwroot/Reports/RIS.frx
Normal file
214
CPRNIMS.WebApi/wwwroot/Reports/RIS.frx
Normal file
@ -0,0 +1,214 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Report ScriptLanguage="CSharp" ReportInfo.Created="06/16/2026 15:54:16" ReportInfo.Modified="06/17/2026 12:14:22" ReportInfo.CreatorVersion="2024.2.0.0">
|
||||
<Dictionary>
|
||||
<TableDataSource Name="TRIS" Alias="TRIS" DataType="System.Int32" Enabled="true">
|
||||
<Column Name="RISNo" DataType="System.String"/>
|
||||
<Column Name="PRNo" DataType="System.Int64"/>
|
||||
<Column Name="QtyIssued" DataType="System.Decimal"/>
|
||||
<Column Name="IssuedTo" DataType="System.String"/>
|
||||
<Column Name="StatusLabel" Alias="Status" DataType="System.String"/>
|
||||
<Column Name="CreatedBy" DataType="System.String"/>
|
||||
<Column Name="ApprovedBy" DataType="System.String"/>
|
||||
<Column Name="ApprovedDate" DataType="System.DateTime"/>
|
||||
<Column Name="CreatedDate" DataType="System.DateTime"/>
|
||||
<Column Name="DisciplineName" Alias="Discipline" DataType="System.String"/>
|
||||
<Column Name="ItemName" DataType="System.String"/>
|
||||
<Column Name="ItemNo" DataType="System.Int64"/>
|
||||
<Column Name="QtyIn" DataType="System.Decimal"/>
|
||||
<Column Name="QtyOut" DataType="System.Decimal"/>
|
||||
<Column Name="QtyOnHand" DataType="System.Decimal"/>
|
||||
<Column Name="DepartmentName" Alias="Department" DataType="System.String"/>
|
||||
<Column Name="TotalReturned" DataType="System.Decimal"/>
|
||||
<Column Name="MRSCount" DataType="System.Int32"/>
|
||||
<Column Name="NetIssued" DataType="System.Decimal"/>
|
||||
</TableDataSource>
|
||||
<TableDataSource Name="TDisciplineAgg" Alias="TDisciplineAgg" DataType="System.Int32" Enabled="true">
|
||||
<Column Name="DisciplineName" DataType="System.String"/>
|
||||
<Column Name="SlipCount" DataType="System.Int32"/>
|
||||
</TableDataSource>
|
||||
<TableDataSource Name="TTopRecipients" Alias="TTopRecipients" DataType="System.Int32" Enabled="true">
|
||||
<Column Name="Name" DataType="System.String"/>
|
||||
<Column Name="SlipCount" DataType="System.Int32"/>
|
||||
<Column Name="QtyOut" DataType="System.Decimal"/>
|
||||
</TableDataSource>
|
||||
|
||||
<Parameter Name="PreparedBy" DataType="System.String" AsString=""/>
|
||||
<Parameter Name="PrintDate" DataType="System.String" AsString=""/>
|
||||
<Parameter Name="ReportNo" DataType="System.String" AsString=""/>
|
||||
<Parameter Name="TotalRISIssued" DataType="System.String" AsString=""/>
|
||||
<Parameter Name="ApprovedRIS" DataType="System.String" AsString=""/>
|
||||
<Parameter Name="DateFrom" DataType="System.DateTime"/>
|
||||
<Parameter Name="DateTo" DataType="System.DateTime"/>
|
||||
</Dictionary>
|
||||
<ReportPage Name="Page1" PaperWidth="216" PaperHeight="279" RawPaperSize="1" Watermark.Font="Arial, 60pt">
|
||||
<ReportTitleBand Name="ReportTitle1" Width="740.88" Height="56.7">
|
||||
<TextObject Name="Text1" Left="179.55" Top="18.9" Width="311.85" Height="28.35" Text="Return Issuance Slip Report" HorzAlign="Center" Font="Segoe UI, 18pt"/>
|
||||
<TextObject Name="Text3" Width="283.5" Height="18.9" Text="LLOYD LABORATORIES INCORPORATED" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text7" Left="-9450" Top="-9450" Width="94.5" Height="18.9" Text="PreparedBy" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text11" Left="-9450" Top="-9450" Width="94.5" Height="18.9" Text="Table_RIS_Query" Font="Arial, 10pt"/>
|
||||
</ReportTitleBand>
|
||||
<PageHeaderBand Name="PageHeader1" Top="59.9" Width="740.88" Height="42.05">
|
||||
<TextObject Name="Text2" Width="113.4" Height="18.9" Text="PREPARED BY" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text4" Left="151.2" Width="94.5" Height="18.9" Text="PRINT DATE" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text5" Left="302.4" Width="94.5" Height="18.9" Text="REPORT NO." Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text6" Top="18.9" Width="94.5" Height="18.9" Text="[PreparedBy]" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text9" Left="302.4" Top="18.9" Width="94.5" Height="18.9" Text="[ReportNo]" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text10" Left="154.5" Top="23.15" Width="94.5" Height="18.9" Text="[PrintDate]" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text8" Left="538.65" Width="94.5" Height="18.9" Text="QtyIssued" Font="Arial, 10pt"/>
|
||||
<TextObject Name="Text15" Left="670.95" Width="94.5" Height="18.9" Text="NetIssued" Font="Arial, 10pt"/>
|
||||
</PageHeaderBand>
|
||||
<DataBand Name="Data1" Top="105.15" Width="740.88" Height="529.2" DataSource="TRIS">
|
||||
<TableObject Name="Table_RIS" Width="803.2" Height="311.85">
|
||||
<TableColumn Name="Column6" Width="42.85"/>
|
||||
<TableColumn Name="Column7" Width="42.85"/>
|
||||
<TableColumn Name="Column8" Width="146.77"/>
|
||||
<TableColumn Name="Column11" Width="59.84"/>
|
||||
<TableColumn Name="Column9" Width="90.07"/>
|
||||
<TableColumn Name="Column10" Width="90.07"/>
|
||||
<TableColumn Name="Column20"/>
|
||||
<TableColumn Name="Column21"/>
|
||||
<TableColumn Name="Column22"/>
|
||||
<TableColumn Name="Column23"/>
|
||||
<TableColumn Name="Column24"/>
|
||||
<TableRow Name="Row6" Height="20.79">
|
||||
<TableCell Name="Cell26" Text="[TRIS.RISNo]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell27" Text="[TRIS.PRNo]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell28" Text="[TRIS.ItemName]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell51" Text="[TRIS.ItemNo]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell29" Text="[TRIS.Discipline]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell30" Text="[TRIS.IssuedTo]" Font="Arial, 10pt">
|
||||
<TextObject Name="Text12" Left="75.6" Width="66.15" Height="18.9" Text="[TRIS.QtyIssued]" Format="Currency" Format.UseLocale="true" Format.DecimalDigits="2" HorzAlign="Right" WordWrap="false" Font="Arial, 10pt" Trimming="EllipsisCharacter"/>
|
||||
</TableCell>
|
||||
<TableCell Name="Cell112" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell117" Text="[TRIS.TotalReturned]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell122" Text="[TRIS.NetIssued]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell127" Text="[TRIS.Status]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell132" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row7" Height="20.79">
|
||||
<TableCell Name="Cell31" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell32" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell33" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell52" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell34" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell35" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell113" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell118" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell123" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell128" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell133" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row8" Height="20.79">
|
||||
<TableCell Name="Cell36" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell37" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell38" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell53" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell39" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell40" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell114" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell119" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell124" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell129" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell134" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row9" Height="20.79">
|
||||
<TableCell Name="Cell41" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell42" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell43" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell54" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell44" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell45" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell115" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell120" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell125" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell130" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell135" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row10" Height="228.69">
|
||||
<TableCell Name="Cell46" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell47" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell48" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell55" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell49" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell50" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell116" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell121" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell126" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell131" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell136" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
</TableObject>
|
||||
<TextObject Name="Text18" Left="378" Top="340.2" Width="141.75" Height="18.9" Text="TOP RECIPIENTS" Font="Arial, 10pt"/>
|
||||
<TableObject Name="Table5" Left="378" Top="359.1" Width="321.3" Height="113.37" PrintOnParent="true">
|
||||
<TableColumn Name="Column17" Width="189"/>
|
||||
<TableColumn Name="Column18"/>
|
||||
<TableColumn Name="Column19"/>
|
||||
<TableRow Name="Row18" Height="25.98">
|
||||
<TableCell Name="Cell91" Text="[TTopRecipients.Name]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell92" Text="[TTopRecipients.SlipCount]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell108" Text="[TTopRecipients.QtyOut]" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row19" Height="25.98">
|
||||
<TableCell Name="Cell96" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell97" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell109" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row20" Height="25.98">
|
||||
<TableCell Name="Cell101" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell102" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell110" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row21" Height="35.43">
|
||||
<TableCell Name="Cell106" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell107" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell111" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
</TableObject>
|
||||
<TextObject Name="Text17" Top="340.2" Width="189" Height="18.9" Text="ISSUANCE BY DISCIPLINE" Font="Arial, 10pt"/>
|
||||
<TableObject Name="Table4" Top="359.1" Width="311.42" Height="113.4" PrintOnParent="true">
|
||||
<TableColumn Name="Column12" Width="200.79"/>
|
||||
<TableColumn Name="Column13" Width="87.39"/>
|
||||
<TableColumn Name="Column15" Width="22.24"/>
|
||||
<TableColumn Name="Column16" Width="1"/>
|
||||
<TableRow Name="Row11">
|
||||
<TableCell Name="Cell56" Text="[TDisciplineAgg.DisciplineName]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell57" Text="[TDisciplineAgg.SlipCount]" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell59" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell60" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row16">
|
||||
<TableCell Name="Cell81" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell82" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell84" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell85" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row12">
|
||||
<TableCell Name="Cell61" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell62" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell64" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell65" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row13">
|
||||
<TableCell Name="Cell66" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell67" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell69" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell70" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row14">
|
||||
<TableCell Name="Cell71" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell72" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell74" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell75" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
<TableRow Name="Row15">
|
||||
<TableCell Name="Cell76" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell77" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell79" Font="Arial, 10pt"/>
|
||||
<TableCell Name="Cell80" Font="Arial, 10pt"/>
|
||||
</TableRow>
|
||||
</TableObject>
|
||||
</DataBand>
|
||||
<ReportSummaryBand Name="ReportSummary1" Top="637.55" Width="740.88" Height="37.8"/>
|
||||
<PageFooterBand Name="PageFooter1" Top="678.55" Width="740.88" Height="604.8"/>
|
||||
</ReportPage>
|
||||
</Report>
|
||||
@ -1,11 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<StaticWebAssetsEnabled>true</StaticWebAssetsEnabled>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Remove="Views\Components\Dashboard\DeptApprover.cshtml" />
|
||||
</ItemGroup>
|
||||
@ -58,8 +60,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Common\Helper\" />
|
||||
<Folder Include="Properties\NewFolder\" />
|
||||
<Folder Include="wwwroot\Content\Uploads\PRAttachment\" />
|
||||
<Folder Include="wwwroot\_content\FastReport.Web\js\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -68,6 +70,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CaptchaGen.NetCore" Version="1.1.2" />
|
||||
<PackageReference Include="FastReport.OpenSource" Version="2026.2.3" />
|
||||
<PackageReference Include="FastReport.OpenSource.Export.PdfSimple" Version="2026.2.3" />
|
||||
<PackageReference Include="FastReport.OpenSource.Web" Version="2026.2.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CPRNIMS.Domain.UIContracts.Account;
|
||||
using CPRNIMS.Domain.Contracts.Reports;
|
||||
using CPRNIMS.Domain.UIContracts.Account;
|
||||
using CPRNIMS.Domain.UIContracts.Canvass;
|
||||
using CPRNIMS.Domain.UIContracts.CaptCha;
|
||||
using CPRNIMS.Domain.UIContracts.Common;
|
||||
@ -22,6 +23,7 @@ using CPRNIMS.Domain.UIServices.Receiving;
|
||||
using CPRNIMS.Domain.UIServices.SMTP;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using CPRNIMS.Infrastructure.Reports;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
@ -88,6 +90,9 @@ namespace CPRNIMS.WebApps.Common
|
||||
builder.Services.AddTransient<ICaptchaService, CaptchaService>();
|
||||
builder.Services.AddScoped<ErrorLogHelper>();
|
||||
builder.Services.AddScoped<IRIS, RIS>();
|
||||
builder.Services.AddScoped<IMRS, MRS>();
|
||||
builder.Services.AddScoped<IReportBuilder, ReportBuilder>();
|
||||
builder.Services.AddScoped<IInventoryReports, InventoryReports>();
|
||||
}
|
||||
|
||||
private static void AddSessionAndAuthentication(WebApplicationBuilder builder)
|
||||
@ -170,7 +175,6 @@ namespace CPRNIMS.WebApps.Common
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddDbContext(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddDbContext<NonInventoryDbContext>(options =>
|
||||
|
||||
@ -164,6 +164,7 @@ namespace CPRNIMS.WebApps.Controllers
|
||||
new Claim(ClaimTypes.NameIdentifier, login.userId),
|
||||
new Claim(ClaimTypes.Name, login.userName),
|
||||
new Claim("FullName", login.fullName),
|
||||
new Claim("DepartmentId", Convert.ToString(login.departmentId)),
|
||||
new Claim("Company", login.company),
|
||||
new Claim("Token", login.token),
|
||||
new Claim("TokenExpiry", expirationTime.ToString("O"))
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using Azure.Core;
|
||||
using CPRNIMS.Domain.UIContracts.Account;
|
||||
using CPRNIMS.Domain.UIContracts.Account;
|
||||
using CPRNIMS.Domain.UIContracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
using CPRNIMS.Domain.UIContracts.Inventory;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
{
|
||||
public class InventoryReportsController : Controller
|
||||
{
|
||||
private readonly IInventoryReports _reports;
|
||||
public InventoryReportsController(IInventoryReports reports)
|
||||
{
|
||||
_reports=reports;
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetInventoryReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var response = await _reports.GetInventoryReportAsync(dateFrom, dateTo, ct);
|
||||
return GetResponse(response);
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRISReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var response = await _reports.GetRISReportAsync(dateFrom, dateTo, ct);
|
||||
return GetResponse(response);
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMRSReport(DateTime dateFrom, DateTime dateTo, CancellationToken ct)
|
||||
{
|
||||
var response = await _reports.GetMRSReportAsync(dateFrom, dateTo, ct);
|
||||
return GetResponse(response);
|
||||
}
|
||||
protected IActionResult GetResponse<T>(T response)
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
success = response != null,
|
||||
data = response ?? Activator.CreateInstance<T>()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
{
|
||||
public class MRSController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
77
CPRNIMS.WebApps/Controllers/Inventory/MRSMgmtController.cs
Normal file
77
CPRNIMS.WebApps/Controllers/Inventory/MRSMgmtController.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using CPRNIMS.Domain.UIContracts.Inventory;
|
||||
using CPRNIMS.Domain.UIContracts.Account;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using CPRNIMS.WebApps.Controllers.Base;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
{
|
||||
public partial class MRSMgmtController : BaseMethod
|
||||
{
|
||||
private readonly IMRS _mrs;
|
||||
public MRSMgmtController(ErrorLogHelper errorMessageService,IWebHostEnvironment webHostEnvironment,TokenHelper tokenHelper,
|
||||
IMRS mrs, IAccount account) : base(errorMessageService, webHostEnvironment, tokenHelper, account) => _mrs = mrs;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMRS([FromQuery] string? searchMRSNo,string? searchRISNo,string? searchItemName,string?
|
||||
searchReturnedBy,string? status,string? condition,int pageNumber = 1,int pageSize = 12,CancellationToken ct = default)
|
||||
{
|
||||
short? statusCode = status switch
|
||||
{
|
||||
"0" => 0,
|
||||
"1" => 1,
|
||||
"2" => 2,
|
||||
_ => null
|
||||
};
|
||||
|
||||
var result = await _mrs.GetMRSPaged(new MRSPagedRequest
|
||||
{
|
||||
SearchMRSNo = searchMRSNo,
|
||||
SearchRISNo = searchRISNo,
|
||||
SearchItemName = searchItemName,
|
||||
SearchReturnedBy = searchReturnedBy,
|
||||
Status = statusCode,
|
||||
Condition = condition,
|
||||
PageNumber = pageNumber,
|
||||
PageSize = pageSize
|
||||
}, ct);
|
||||
|
||||
return Json(new { data = result.Data, recordsTotal = result.RecordsTotal});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateMRS([FromBody] CreateMRSRequest request,CancellationToken ct)
|
||||
{
|
||||
var result = await _mrs.CreateMRS(request, ct);
|
||||
|
||||
if (!result.success)
|
||||
return BadRequest(new { success = false, message = result.message });
|
||||
|
||||
return Ok(new { success = true, message = result.message });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ApproveMRS([FromBody] ApproveMRSRequest request,CancellationToken ct)
|
||||
{
|
||||
var result = await _mrs.ApproveMRS(request, ct);
|
||||
|
||||
if (!result.success)
|
||||
return BadRequest(new { success = false, message = result.message });
|
||||
|
||||
return Ok(new { success = true, message = result.message });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CancelMRS([FromBody] CancelMRSRequest request,CancellationToken ct)
|
||||
{
|
||||
var result = await _mrs.CancelMRS(request, ct);
|
||||
|
||||
if (!result.success)
|
||||
return BadRequest(new { success = false, message = result.message });
|
||||
|
||||
return Ok(new { success = true, message = result.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,63 +1,85 @@
|
||||
using CPRNIMS.Domain.UIContracts.Account;
|
||||
using CPRNIMS.Domain.Contracts.Reports;
|
||||
using CPRNIMS.Domain.UIContracts.Account;
|
||||
using CPRNIMS.Domain.UIContracts.Inventory;
|
||||
using CPRNIMS.Infrastructure.Dto.Inventory.Request;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using CPRNIMS.WebApps.Controllers.Base;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FastReport;
|
||||
using FastReport.Web;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
{
|
||||
public class RISMgmtController : BaseMethod
|
||||
{
|
||||
private readonly IRIS _ris;
|
||||
private readonly IReportBuilder _builder;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
public RISMgmtController(ErrorLogHelper errorMessageService,
|
||||
IWebHostEnvironment webHostEnvironment, TokenHelper tokenHelper
|
||||
, IRIS ris, IAccount account)
|
||||
, IRIS ris, IAccount account, IReportBuilder builder)
|
||||
: base(errorMessageService, webHostEnvironment, tokenHelper, account)
|
||||
{
|
||||
_ris = ris;
|
||||
_builder = builder;
|
||||
_env = webHostEnvironment;
|
||||
|
||||
}
|
||||
|
||||
public IActionResult RISReport(DateTime? dateFrom, DateTime? dateTo, CancellationToken ct)
|
||||
{
|
||||
return View();
|
||||
}
|
||||
public async Task<IActionResult> RISReportPdf(DateTime? dateFrom, DateTime? dateTo, CancellationToken ct)
|
||||
{
|
||||
var from = dateFrom ?? DateTime.Today.AddDays(-15);
|
||||
var to = dateTo ?? DateTime.Today;
|
||||
|
||||
var templatePath = Path.Combine(_env.WebRootPath, "Reports", "RIS_v2.frx");
|
||||
var report = await _builder.RISBuildAsync(from, to, templatePath, ct);
|
||||
report.Prepare();
|
||||
|
||||
using var pdf = new FastReport.Export.PdfSimple.PDFSimpleExport();
|
||||
using var ms = new MemoryStream();
|
||||
pdf.Export(report, ms);
|
||||
|
||||
return File(ms.ToArray(), "application/pdf",
|
||||
$"RIS-Report-{from:yyyyMMdd}-{to:yyyyMMdd}.pdf");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateRIS([FromBody] CreateRISRequest request,CancellationToken ct)
|
||||
{
|
||||
var result = await _ris.CreateRIS(request,ct);
|
||||
if (!result.success)
|
||||
return BadRequest(new { success = false, message = result.message });
|
||||
|
||||
return Json(new { success = true, message= $"RIS {result.RISNo} created successfully.", data = result });
|
||||
return Ok(new { success = true, message = result.message });
|
||||
}
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ApproveRIS([FromBody] ApproveRISRequest request,CancellationToken ct)
|
||||
{
|
||||
bool isSuccess = await _ris.ApproveRIS(request, ct);
|
||||
var result = await _ris.ApproveRIS(request, ct);
|
||||
|
||||
if (!isSuccess)
|
||||
return BadRequest(new { success = false, message = "RIS cancelled failed" });
|
||||
if (!result.success)
|
||||
return BadRequest(new { success = false, message = result.message });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = $"RIS approved successfully."
|
||||
});
|
||||
return Ok(new { success = true, message = result.message });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CancelRIS([FromBody] CancelRISRequest request,CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Reason))
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "A reason for cancellation is required."
|
||||
});
|
||||
bool isSuccess = await _ris.CancelRIS(request,ct);
|
||||
return BadRequest(new{success = false,message = "A reason for cancellation is required."});
|
||||
|
||||
if (!isSuccess)
|
||||
return BadRequest(new { success = false, message = "RIS cancelled failed" });
|
||||
var result = await _ris.CancelRIS(request, ct);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "RIS cancelled and inventory restored."
|
||||
});
|
||||
if (!result.success)
|
||||
return BadRequest(new { success = false, message = result.message });
|
||||
|
||||
return Ok(new { success = true, message = result.message });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -92,10 +114,5 @@ namespace CPRNIMS.WebApps.Controllers.Inventory
|
||||
disciplineList = result.DisciplineList
|
||||
});
|
||||
}
|
||||
public async Task<IActionResult> GetRISById(int risId, CancellationToken ct)
|
||||
{
|
||||
var response = await _ris.GetRISById(risId,ct);
|
||||
return GetResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +1,35 @@
|
||||
using CPRNIMS.Domain.UIServices.Updater;
|
||||
using CPRNIMS.Domain.UIServices.Updater;
|
||||
using CPRNIMS.WebApps.Common;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.WebHost.UseStaticWebAssets();
|
||||
|
||||
builder.Services.AddApplicationServices(builder);
|
||||
builder.Services.AddFastReport();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
app.UseHsts();
|
||||
}
|
||||
//app.UseRewriter(options);
|
||||
|
||||
//var provider = new FileExtensionContentTypeProvider();
|
||||
//provider.Mappings[".js"] = "text/javascript";
|
||||
//provider.Mappings[".css"] = "text/css";
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseSession();
|
||||
|
||||
app.MapHub<CartHub>("/cartHub");
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseFastReport();
|
||||
app.MapHub<CartHub>("/cartHub");
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
//pattern: "{controller=ItemMgmt}/{action=Index}/{id?}");
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
app.Run();
|
||||
@ -9,9 +9,11 @@ namespace CPRNIMS.WebApps.ViewComponents.Inventory
|
||||
string viewName = InventoryTabPageId switch
|
||||
{
|
||||
1 => "~/Views/Components/Inventory/TabPage/Inventory.cshtml",
|
||||
2 => "~/Views/Components/Inventory/TabPage/RISForApproval.cshtml",
|
||||
2 => "~/Views/Components/Inventory/TabPage/RIS.cshtml",
|
||||
3 => "~/Views/Components/Inventory/TabPage/MRS.cshtml",
|
||||
_ => "~/Views/Components/Inventory/TabPage/InventoryTransaction.cshtml"
|
||||
4 => "~/Views/Components/Inventory/TabPage/Reports/InventorySummaryReport.cshtml",
|
||||
5 => "~/Views/Components/Inventory/TabPage/Reports/RISReport.cshtml",
|
||||
_ => "~/Views/Components/Inventory/TabPage/Reports/MRSReport.cshtml"
|
||||
};
|
||||
return View(viewName);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1272
CPRNIMS.WebApps/Views/Components/Inventory/TabPage/MRS.cshtml
Normal file
1272
CPRNIMS.WebApps/Views/Components/Inventory/TabPage/MRS.cshtml
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,3 @@
|
||||
@* ── Tab: Return Issuance Slip — For Approval ───────────────────────────────
|
||||
Returned by GetInventoryTabPage?id=2 (or whichever tab id you assign)
|
||||
Depends on: _InventoryStyles, _InventoryHelpers, window.InventoryHelpers
|
||||
────────────────────────────────────────────────────────────────────────────── *@
|
||||
|
||||
@* ── FILTER BAR ── *@
|
||||
<div class="inv-filters">
|
||||
<div class="inv-search-box">
|
||||
@ -196,7 +191,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ── RIS card grid ── */
|
||||
.ris-grid {
|
||||
@ -418,6 +412,58 @@
|
||||
font-weight: 600;
|
||||
color: var(--teal-dark, #0d5c63);
|
||||
}
|
||||
/* ── Report trigger button ── */
|
||||
.ris-report-trigger {
|
||||
padding: 7px 14px;
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
border: 1.5px solid var(--teal-mid, #0e7c86);
|
||||
background: var(--teal-pale, #e6f7f8);
|
||||
color: var(--teal-dark, #0d5c63);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .82rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all .2s;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.ris-report-trigger:hover {
|
||||
background: var(--teal-mid, #0e7c86);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Preset chips ── */
|
||||
.ris-rep-presets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ris-preset {
|
||||
padding: 6px 12px;
|
||||
border-radius: 50px;
|
||||
border: 1.5px solid var(--border, #d6eaec);
|
||||
background: var(--card-bg, #fff);
|
||||
color: var(--text-muted, #6b8890);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.ris-preset:hover {
|
||||
border-color: var(--teal-mid, #0e7c86);
|
||||
}
|
||||
|
||||
.ris-preset.active {
|
||||
background: var(--teal-mid, #0e7c86);
|
||||
color: #fff;
|
||||
border-color: var(--teal-mid, #0e7c86);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@ -929,7 +975,95 @@
|
||||
month: "short", day: "numeric", year: "numeric"
|
||||
});
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// REPORT DATE-RANGE MODAL
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
(function initReportModal() {
|
||||
const trigger = document.getElementById("ris-report-btn");
|
||||
const overlay = document.getElementById("ris-report-modal-overlay");
|
||||
if (!trigger || !overlay) return;
|
||||
|
||||
const fromEl = document.getElementById("ris-rep-from");
|
||||
const toEl = document.getElementById("ris-rep-to");
|
||||
const errEl = document.getElementById("ris-rep-err");
|
||||
const presets = document.getElementById("ris-rep-presets");
|
||||
const dismiss = document.getElementById("ris-rep-dismiss");
|
||||
const viewBtn = document.getElementById("ris-rep-view");
|
||||
const pdfBtn = document.getElementById("ris-rep-pdf");
|
||||
|
||||
const fmt = d => d.toISOString().slice(0, 10); // yyyy-MM-dd
|
||||
const today = () => new Date();
|
||||
|
||||
function applyPreset(btn) {
|
||||
presets.querySelectorAll(".ris-preset")
|
||||
.forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
|
||||
const to = today();
|
||||
let from = new Date();
|
||||
|
||||
if (btn.dataset.month) {
|
||||
from = new Date(to.getFullYear(), to.getMonth(), 1); // 1st of month
|
||||
} else {
|
||||
from.setDate(to.getDate() - parseInt(btn.dataset.days, 10));
|
||||
}
|
||||
fromEl.value = fmt(from);
|
||||
toEl.value = fmt(to);
|
||||
errEl.style.display = "none";
|
||||
}
|
||||
|
||||
// Default 15-day range on open
|
||||
function openModal() {
|
||||
const def = presets.querySelector('[data-days="15"]');
|
||||
applyPreset(def);
|
||||
overlay.style.display = "flex";
|
||||
}
|
||||
function closeModal() { overlay.style.display = "none"; }
|
||||
|
||||
trigger.addEventListener("click", openModal);
|
||||
dismiss.addEventListener("click", closeModal);
|
||||
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
|
||||
|
||||
presets.querySelectorAll(".ris-preset").forEach(b =>
|
||||
b.addEventListener("click", () => applyPreset(b)));
|
||||
|
||||
// Manual edits clear the active preset
|
||||
[fromEl, toEl].forEach(el =>
|
||||
el.addEventListener("input", () => {
|
||||
presets.querySelectorAll(".ris-preset")
|
||||
.forEach(b => b.classList.remove("active"));
|
||||
errEl.style.display = "none";
|
||||
}));
|
||||
|
||||
function validRange() {
|
||||
if (!fromEl.value || !toEl.value) return false;
|
||||
if (new Date(fromEl.value) > new Date(toEl.value)) {
|
||||
errEl.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildUrl(action) {
|
||||
const p = new URLSearchParams({
|
||||
dateFrom: fromEl.value,
|
||||
dateTo: toEl.value
|
||||
});
|
||||
return `/RISMgmt/${action}?${p}`;
|
||||
}
|
||||
|
||||
viewBtn.addEventListener("click", () => {
|
||||
if (!validRange()) return;
|
||||
window.open(buildUrl("RISReport"), "_blank"); // viewer in new tab
|
||||
closeModal();
|
||||
});
|
||||
|
||||
pdfBtn.addEventListener("click", () => {
|
||||
if (!validRange()) return;
|
||||
window.open(buildUrl("RISReportPdf"), "_blank"); // PDF download
|
||||
closeModal();
|
||||
});
|
||||
})();
|
||||
// ── Initial load ───────────────────────────────────────────────────────
|
||||
fetchData();
|
||||
})();
|
||||
@ -0,0 +1,531 @@
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0)
|
||||
}
|
||||
|
||||
.page {
|
||||
background: var(--color-background-primary);
|
||||
border: 0.5px solid var(--color-border-tertiary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.page-tab {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 0.5px solid var(--color-border-tertiary);
|
||||
background: var(--color-background-secondary)
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 10px 18px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all .15s
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--color-text-primary);
|
||||
border-bottom-color: var(--color-text-primary);
|
||||
background: var(--color-background-primary)
|
||||
}
|
||||
|
||||
.report-page {
|
||||
display: none;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.report-page.active {
|
||||
display: block
|
||||
}
|
||||
|
||||
/* Report header band */
|
||||
.rpt-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 0.5px solid var(--color-border-tertiary)
|
||||
}
|
||||
|
||||
.rpt-header-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.rpt-company {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.rpt-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 2px
|
||||
}
|
||||
|
||||
.rpt-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary)
|
||||
}
|
||||
|
||||
.rpt-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--border-radius-md);
|
||||
background: var(--color-background-info);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.rpt-logo i {
|
||||
font-size: 20px;
|
||||
color: var(--color-text-info)
|
||||
}
|
||||
|
||||
.rpt-meta {
|
||||
display: flex;
|
||||
gap: 20px
|
||||
}
|
||||
|
||||
.rpt-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px
|
||||
}
|
||||
|
||||
.rpt-meta-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em
|
||||
}
|
||||
|
||||
.rpt-meta-val {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary)
|
||||
}
|
||||
|
||||
/* KPI strip */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3,1fr);
|
||||
gap: 0;
|
||||
border-bottom: 0.5px solid var(--color-border-tertiary)
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
padding: 14px 18px;
|
||||
border-right: 0.5px solid var(--color-border-tertiary)
|
||||
}
|
||||
|
||||
.kpi-cell:last-child {
|
||||
border-right: none
|
||||
}
|
||||
|
||||
.kpi-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px
|
||||
}
|
||||
|
||||
.kpi-lbl i {
|
||||
font-size: 13px
|
||||
}
|
||||
|
||||
.kpi-val {
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.1
|
||||
}
|
||||
|
||||
.kpi-sub {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 3px
|
||||
}
|
||||
|
||||
.kpi-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
padding: 2px 7px;
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-top: 4px
|
||||
}
|
||||
|
||||
.badge-up {
|
||||
background: #EAF3DE;
|
||||
color: #3B6D11
|
||||
}
|
||||
|
||||
.badge-dn {
|
||||
background: #FCEBEB;
|
||||
color: #A32D2D
|
||||
}
|
||||
|
||||
.badge-neu {
|
||||
background: var(--color-background-secondary);
|
||||
color: var(--color-text-secondary)
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.rpt-section {
|
||||
padding: 16px 24px
|
||||
}
|
||||
|
||||
.rpt-section + .rpt-section {
|
||||
border-top: 0.5px solid var(--color-border-tertiary)
|
||||
}
|
||||
|
||||
.rpt-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px
|
||||
}
|
||||
|
||||
.rpt-section-title i {
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.rpt-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
.rpt-table th {
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 0.5px solid var(--color-border-tertiary);
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.rpt-table td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 0.5px solid var(--color-border-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
.rpt-table tr:last-child td {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
.rpt-table tr:hover td {
|
||||
background: var(--color-background-secondary)
|
||||
}
|
||||
|
||||
.rpt-table .num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums
|
||||
}
|
||||
|
||||
.rpt-table .total-row td {
|
||||
font-weight: 500;
|
||||
background: var(--color-background-secondary)
|
||||
}
|
||||
|
||||
/* Status pill */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-md);
|
||||
font-size: 10px;
|
||||
font-weight: 500
|
||||
}
|
||||
|
||||
.pill-draft {
|
||||
background: #F1EFE8;
|
||||
color: #5F5E5A
|
||||
}
|
||||
|
||||
.pill-approved {
|
||||
background: #EAF3DE;
|
||||
color: #3B6D11
|
||||
}
|
||||
|
||||
.pill-cancelled {
|
||||
background: #FCEBEB;
|
||||
color: #A32D2D
|
||||
}
|
||||
|
||||
/* Mini bar */
|
||||
.mini-bar-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.mini-bar-track {
|
||||
flex: 1;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-border-tertiary);
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.mini-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px
|
||||
}
|
||||
|
||||
.fill-teal {
|
||||
background: #1D9E75
|
||||
}
|
||||
|
||||
.fill-coral {
|
||||
background: #D85A30
|
||||
}
|
||||
|
||||
.fill-amber {
|
||||
background: #EF9F27
|
||||
}
|
||||
|
||||
.fill-blue {
|
||||
background: #378ADD
|
||||
}
|
||||
|
||||
/* Signature block */
|
||||
.sig-block {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3,1fr);
|
||||
gap: 16px;
|
||||
margin-top: 4px
|
||||
}
|
||||
|
||||
.sig-item {
|
||||
border-top: 0.5px solid var(--color-border-tertiary);
|
||||
padding-top: 8px
|
||||
}
|
||||
|
||||
.sig-role {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary)
|
||||
}
|
||||
|
||||
/* 2-col layout */
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
border-top: 0.5px solid var(--color-border-tertiary)
|
||||
}
|
||||
|
||||
.col-left {
|
||||
padding: 16px 24px;
|
||||
border-right: 0.5px solid var(--color-border-tertiary)
|
||||
}
|
||||
|
||||
.col-right {
|
||||
padding: 16px 24px
|
||||
}
|
||||
|
||||
/* Timeline strip for MRS condition */
|
||||
.cond-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 0.5px solid var(--color-border-tertiary)
|
||||
}
|
||||
|
||||
.cond-row:last-child {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
.cond-name {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-primary);
|
||||
min-width: 70px
|
||||
}
|
||||
|
||||
.cond-bar-track {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--color-border-tertiary);
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.cond-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px
|
||||
}
|
||||
|
||||
.cond-count {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
min-width: 40px;
|
||||
text-align: right
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.rpt-footer {
|
||||
padding: 10px 24px;
|
||||
border-top: 0.5px solid var(--color-border-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--color-background-secondary)
|
||||
}
|
||||
|
||||
.rpt-footer-lbl {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-secondary)
|
||||
}
|
||||
|
||||
/* Inventory specific */
|
||||
.inv-row-hd {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 80px;
|
||||
gap: 0
|
||||
}
|
||||
|
||||
.stock-level {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px
|
||||
}
|
||||
|
||||
.stock-pct {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: right
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2 class="sr-only">Finance department inventory report designs — three report templates: RIS report, MRS report, and inventory summary</h2>
|
||||
|
||||
<div class="page-tab">
|
||||
<button class="tab-btn active" onclick="showTab('ris')">Return Issuance Slip report</button>
|
||||
<button class="tab-btn" onclick="showTab('mrs')">Material Return Slip report</button>
|
||||
<button class="tab-btn" onclick="showTab('inv')">Inventory summary report</button>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
// ── Prevent re-initialization on cache restore ──
|
||||
if (window.__invTab1Initialized) return;
|
||||
window.__invTab1Initialized = true;
|
||||
|
||||
const tabContent = document.getElementById("inv-tab-content");
|
||||
const tabBtns = document.querySelectorAll(".inv-tab-btn");
|
||||
|
||||
// Cache stores the raw HTML string per tab id
|
||||
const cache = {};
|
||||
let activeTabId = null;
|
||||
|
||||
async function loadTab(tabId) {
|
||||
if (activeTabId === tabId) return;
|
||||
activeTabId = tabId;
|
||||
|
||||
// Mark active button
|
||||
tabBtns.forEach(b => b.classList.toggle("active", b.dataset.tabId === String(tabId)));
|
||||
|
||||
if (cache[tabId]) {
|
||||
// ── FIX: inject cached HTML then re-run its <script> blocks ──────
|
||||
injectHtml(tabContent, cache[tabId]);
|
||||
return;
|
||||
}
|
||||
|
||||
tabContent.innerHTML = `<div class="inv-tab-loading">
|
||||
<div class="inv-spinner"></div><span>Loading…</span></div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/InventoryMgmt/GetInventoryTabPage?id=${tabId}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const html = await res.text();
|
||||
cache[tabId] = html;
|
||||
injectHtml(tabContent, html);
|
||||
} catch (err) {
|
||||
console.error("Tab load error:", err);
|
||||
tabContent.innerHTML = `
|
||||
<div class="inv-placeholder">
|
||||
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
|
||||
<h3>Failed to load</h3>
|
||||
<p>Please try again or refresh the page.</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set innerHTML then re-execute every <script> block so IIFEs inside
|
||||
* ViewComponent views fire correctly — both on first load AND cache restore.
|
||||
*/
|
||||
function injectHtml(container, html) {
|
||||
container.innerHTML = html;
|
||||
container.querySelectorAll("script").forEach(oldScript => {
|
||||
const newScript = document.createElement("script");
|
||||
Array.from(oldScript.attributes).forEach(attr =>
|
||||
newScript.setAttribute(attr.name, attr.value));
|
||||
newScript.textContent = oldScript.textContent;
|
||||
oldScript.replaceWith(newScript);
|
||||
});
|
||||
}
|
||||
|
||||
// Wire tab clicks
|
||||
tabBtns.forEach(btn =>
|
||||
btn.addEventListener("click", () => loadTab(parseInt(btn.dataset.tabId, 10)))
|
||||
);
|
||||
|
||||
// Auto-load the first tab on page ready
|
||||
loadTab(1);
|
||||
})();
|
||||
</script>
|
||||
@ -0,0 +1,710 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: 'DM Sans', Arial, sans-serif;
|
||||
background: #fff;
|
||||
color: #1a2e35;
|
||||
padding: 24px;
|
||||
}
|
||||
@@media print {
|
||||
body { padding: 0; margin: 0; background: #fff;}
|
||||
body * { visibility: hidden;}
|
||||
|
||||
#inv-rpt-page, #inv-rpt-page * {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#inv-rpt-page {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.rpt-table tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.rpt-page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border, #d6eaec);
|
||||
border-radius: var(--radius-lg, 14px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.rpt-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
}
|
||||
/* ── FILTER BUTTONS (blended with shared theme) ── */
|
||||
.rpt-date-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1.5px solid var(--border, #d6eaec);
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
padding: 0 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.rpt-date-lbl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #6b8890);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-date-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
padding: 9px 0;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .875rem;
|
||||
color: var(--text-dark, #1a2e35);
|
||||
}
|
||||
|
||||
.rpt-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 9px 18px;
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
border: none;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-btn-primary {
|
||||
background: var(--teal-mid, #0e7c86);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(14,124,134,.3);
|
||||
}
|
||||
|
||||
.rpt-btn-primary:hover {
|
||||
background: var(--teal-dark, #0d5c63);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(14,124,134,.35);
|
||||
}
|
||||
|
||||
.rpt-btn-outline {
|
||||
background: #fff;
|
||||
color: var(--teal-dark, #0d5c63);
|
||||
border: 1.5px solid var(--teal-mid, #0e7c86);
|
||||
}
|
||||
|
||||
.rpt-btn-outline:hover {
|
||||
background: var(--teal-pale, #e6f7f8);
|
||||
border-color: var(--teal-dark, #0d5c63);
|
||||
}
|
||||
.rpt-header-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.rpt-company {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rpt-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1a2e35;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rpt-subtitle {
|
||||
font-size: 12px;
|
||||
color: #6b8890;
|
||||
}
|
||||
|
||||
.rpt-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #EAF3DE;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rpt-logo i {
|
||||
font-size: 20px;
|
||||
color: #3B6D11;
|
||||
}
|
||||
|
||||
.rpt-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rpt-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.rpt-meta-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
}
|
||||
|
||||
.rpt-meta-val {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
padding: 14px 18px;
|
||||
border-right: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.kpi-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.kpi-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kpi-val {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.rpt-section {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.rpt-section + .rpt-section {
|
||||
border-top: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.rpt-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rpt-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rpt-table thead tr {
|
||||
background: linear-gradient(135deg, #1a3a4a, #1e5468);
|
||||
}
|
||||
|
||||
.rpt-table th {
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: rgba(255,255,255,.85);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
padding: 9px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-table td {
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rpt-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rpt-table .num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rpt-table .total-row td {
|
||||
font-weight: 700;
|
||||
background: #f0f6f7;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 50px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-bar-track {
|
||||
flex: 1;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: #d6eaec;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.fill-teal {
|
||||
background: #1D9E75;
|
||||
}
|
||||
|
||||
.fill-blue {
|
||||
background: #378ADD;
|
||||
}
|
||||
|
||||
.fill-amber {
|
||||
background: #EF9F27;
|
||||
}
|
||||
|
||||
.fill-coral {
|
||||
background: #D85A30;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
border-top: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.col-left {
|
||||
padding: 16px 24px;
|
||||
border-right: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.col-right {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.sig-block {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sig-item {
|
||||
border-top: 1px solid #d6eaec;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.sig-role {
|
||||
font-size: 10px;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.rpt-footer {
|
||||
padding: 10px 24px;
|
||||
border-top: 1px solid #d6eaec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f0f6f7;
|
||||
}
|
||||
|
||||
.rpt-footer-lbl {
|
||||
font-size: 10px;
|
||||
color: #6b8890;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
color: #6b8890;
|
||||
}
|
||||
</style>
|
||||
@await Html.PartialAsync("PagesView/Inventory/_InventoryReportHelper")
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const fromEl = document.getElementById("inv-rpt-from");
|
||||
const toEl = document.getElementById("inv-rpt-to");
|
||||
const genBtn = document.getElementById("inv-rpt-generate");
|
||||
const csvBtn = document.getElementById("inv-rpt-csv");
|
||||
const xlsxBtn = document.getElementById("inv-rpt-excel");
|
||||
const container = document.getElementById("inv-rpt-container");
|
||||
|
||||
if (!fromEl || !toEl || !container) {
|
||||
console.error("Inventory report subtab init failed — missing elements.");
|
||||
return;
|
||||
}
|
||||
|
||||
// default: first day of current month -> today
|
||||
const today = new Date();
|
||||
const first = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
toEl.value = today.toISOString().slice(0, 10);
|
||||
fromEl.value = first.toISOString().slice(0, 10);
|
||||
|
||||
let lastData = null; // cache for export
|
||||
|
||||
function buildParams() {
|
||||
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
|
||||
}
|
||||
|
||||
csvBtn.addEventListener("click", exportCsv);
|
||||
genBtn.addEventListener("click", fetchAndRender);
|
||||
|
||||
async function fetchAndRender() {
|
||||
container.innerHTML = `<div class="inv-tab-loading">
|
||||
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/InventoryReports/GetInventoryReport?${buildParams()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
lastData = json.data ?? json;
|
||||
renderReport(lastData);
|
||||
} catch (err) {
|
||||
console.error("Inventory report fetch error:", err);
|
||||
container.innerHTML = `
|
||||
<div class="inv-placeholder">
|
||||
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
|
||||
<h3>Failed to load report</h3>
|
||||
<p>Please try again.</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── CSV export (client-side, no backend needed) ──
|
||||
function exportCsv() {
|
||||
if (!lastData) return;
|
||||
const rows = lastData.rows ?? [];
|
||||
const byCat = lastData.byCategory ?? [];
|
||||
const alerts = lastData.alerts ?? [];
|
||||
const summary = lastData.summary ?? {};
|
||||
const headers = ["Item Name","Item No.","Category","Lot No.","Qty In","Qty Out","On Hand","Stock %"];
|
||||
|
||||
const csvLines = [
|
||||
`Inventory Summary Report`,
|
||||
`Company,${csvCell(lastData.companyName)}`,
|
||||
`Report No,${csvCell(lastData.reportNo)}`,
|
||||
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
|
||||
``,
|
||||
// ── Inventory detail ──
|
||||
headers.map(csvCell).join(","),
|
||||
...rows.map(r => [
|
||||
r.itemName, r.itemNo, r.itemCategoryName, r.lotNo,
|
||||
r.qtyIn, r.qtyOut, r.qtyOnHand, r.stockPct
|
||||
].map(csvCell).join(",")),
|
||||
["Total","","","", summary.totalQtyIn ?? 0, summary.totalQtyOut ?? 0, summary.totalOnHand ?? 0, ""].map(csvCell).join(","),
|
||||
``,
|
||||
// ── Stock level by category ──
|
||||
`Stock Level by Category`,
|
||||
["Category","Avg Stock %"].map(csvCell).join(","),
|
||||
...byCat.map(c => [c.categoryName, c.avgStockPct].map(csvCell).join(",")),
|
||||
``,
|
||||
// ── Items requiring attention ──
|
||||
`Items Requiring Attention`,
|
||||
["Item","On Hand","Alert"].map(csvCell).join(","),
|
||||
...alerts.map(a => [a.itemName, a.qtyOnHand, a.severity].map(csvCell).join(","))
|
||||
];
|
||||
|
||||
downloadBlob(
|
||||
"\uFEFF" + csvLines.join("\r\n"),
|
||||
`Inventory_Report_${fromEl.value}_to_${toEl.value}.csv`,
|
||||
"text/csv;charset=utf-8;"
|
||||
);
|
||||
}
|
||||
function csvCell(v) {
|
||||
const s = String(v ?? "");
|
||||
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}
|
||||
function downloadBlob(content, filename, mime) {
|
||||
const blob = new Blob([content], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = filename;
|
||||
document.body.appendChild(a); a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function renderReport(data) {
|
||||
const summary = data.summary ?? {};
|
||||
const rows = data.rows ?? [];
|
||||
const byCat = data.byCategory ?? [];
|
||||
const alerts = data.alerts ?? [];
|
||||
|
||||
const catFillColors = ["fill-teal", "fill-blue", "fill-amber", "fill-coral"];
|
||||
const maxCatPct = Math.max(1, ...byCat.map(c => c.avgStockPct));
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="rpt-page" id="inv-rpt-page">
|
||||
|
||||
<div class="rpt-header">
|
||||
<div class="rpt-header-top">
|
||||
<div>
|
||||
<div class="rpt-company">${_esc(data.companyName ?? "")}</div>
|
||||
<div class="rpt-title">Inventory summary Report</div>
|
||||
<div class="rpt-subtitle">As of ${_fmtDate(data.asOf)} · All departments · All categories</div>
|
||||
</div>
|
||||
<div class="rpt-logo" style="background:#EAF3DE">
|
||||
<i class="fas fa-boxes" style="color:#3B6D11"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpt-meta">
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Prepared by</span>
|
||||
<span class="rpt-meta-val">${_esc(data.preparedBy ?? "Finance Department")}</span>
|
||||
</div>
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Print date</span>
|
||||
<span class="rpt-meta-val">${_fmtDate(new Date().toISOString())}</span>
|
||||
</div>
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Report no.</span>
|
||||
<span class="rpt-meta-val">${_esc(data.reportNo ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-strip" style="grid-template-columns:repeat(4,1fr)">
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-box"></i> Total SKUs</div>
|
||||
<div class="kpi-val">${summary.totalSKUs ?? 0}</div>
|
||||
</div>
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-layer-group"></i> Total on hand</div>
|
||||
<div class="kpi-val">${(summary.totalOnHand ?? 0).toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-exclamation-triangle"></i> Low stock</div>
|
||||
<div class="kpi-val" style="color:#A32D2D">${summary.lowStockCount ?? 0}</div>
|
||||
</div>
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-ban"></i> Out of stock</div>
|
||||
<div class="kpi-val" style="color:#A32D2D">${summary.outOfStockCount ?? 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-section">
|
||||
<div class="rpt-section-title"><i class="fas fa-list"></i> Inventory detail by item</div>
|
||||
<table class="rpt-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item Name</th><th>Item No.</th><th>Category</th><th>Lot No.</th>
|
||||
<th class="num">Qty In</th><th class="num">Qty Out</th>
|
||||
<th class="num">On Hand</th><th>Stock Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(r => `
|
||||
<tr>
|
||||
<td style="font-weight:600">${_esc(r.itemName)}</td>
|
||||
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
|
||||
<td>${_esc(r.itemCategoryName)}</td>
|
||||
<td style="color:var(--text-muted,#6b8890)">${_esc(r.lotNo)}</td>
|
||||
<td class="num">${r.qtyIn}</td>
|
||||
<td class="num">${r.qtyOut}</td>
|
||||
<td class="num" style="font-weight:600">${r.qtyOnHand}</td>
|
||||
<td>${_stockBar(r.stockPct)}</td>
|
||||
</tr>`).join("")}
|
||||
<tr class="total-row">
|
||||
<td colspan="4">Total</td>
|
||||
<td class="num">${summary.totalQtyIn ?? 0}</td>
|
||||
<td class="num">${summary.totalQtyOut ?? 0}</td>
|
||||
<td class="num">${summary.totalOnHand ?? 0}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="two-col">
|
||||
<div class="col-left">
|
||||
<div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Stock level by category</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
${byCat.map((c, i) => `
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
|
||||
<span>${_esc(c.categoryName)}</span>
|
||||
<span style="font-weight:600">${c.avgStockPct}% avg</span>
|
||||
</div>
|
||||
<div class="mini-bar-track">
|
||||
<div class="mini-bar-fill ${catFillColors[i % catFillColors.length]}"
|
||||
style="width:${c.avgStockPct}%"></div>
|
||||
</div>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-right">
|
||||
<div class="rpt-section-title"><i class="fas fa-exclamation-triangle"></i> Items requiring attention</div>
|
||||
<table class="rpt-table">
|
||||
<thead><tr><th>Item</th><th class="num">On Hand</th><th>Alert</th></tr></thead>
|
||||
<tbody>
|
||||
${alerts.map(a => `
|
||||
<tr>
|
||||
<td>${_esc(a.itemName)}</td>
|
||||
<td class="num" style="color:#A32D2D">${a.qtyOnHand}</td>
|
||||
<td>${_alertPill(a.severity)}</td>
|
||||
</tr>`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-section">
|
||||
<div class="sig-block">
|
||||
<div class="sig-item"><div class="sig-role">Prepared by</div><div class="sig-name">Finance Officer</div></div>
|
||||
<div class="sig-item"><div class="sig-role">Reviewed by</div><div class="sig-name">Finance Manager</div></div>
|
||||
<div class="sig-item"><div class="sig-role">Approved by</div><div class="sig-name">Finance Director</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-footer">
|
||||
<span class="rpt-footer-lbl">${_esc(data.companyName ?? "")} — Confidential — For internal use only</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _stockBar(pct) {
|
||||
const cls = pct < 20 ? "fill-coral" : pct < 50 ? "fill-amber" : "fill-teal";
|
||||
const color = pct < 20 ? "#A32D2D" : pct < 50 ? "#854F0B" : "inherit";
|
||||
return `<div style="display:flex;flex-direction:column;gap:3px">
|
||||
<div class="mini-bar-track"><div class="mini-bar-fill ${cls}" style="width:${pct}%"></div></div>
|
||||
<span style="font-size:10px;color:${color};text-align:right">${pct}%${pct < 20 ? " ⚠" : ""}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _alertPill(severity) {
|
||||
const map = {
|
||||
"Critical": { bg: "#FCEBEB", fg: "#A32D2D" },
|
||||
"Low": { bg: "#FAEEDA", fg: "#854F0B" }
|
||||
};
|
||||
const c = map[severity] ?? map["Low"];
|
||||
return `<span class="pill" style="background:${c.bg};color:${c.fg}">${_esc(severity)}</span>`;
|
||||
}
|
||||
|
||||
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
|
||||
function exportExcel() {
|
||||
if (!lastData) return;
|
||||
const rows = lastData.rows ?? [];
|
||||
const byCat = lastData.byCategory ?? [];
|
||||
const alerts = lastData.alerts ?? [];
|
||||
const summary = lastData.summary ?? {};
|
||||
|
||||
const headerCells = ["Item Name","Item No.","Category","Lot No.","Qty In","Qty Out","On Hand","Stock %"]
|
||||
.map(h => `<th>${_esc(h)}</th>`).join("");
|
||||
|
||||
const bodyRows = rows.map(r => `
|
||||
<tr>
|
||||
<td>${_esc(r.itemName)}</td>
|
||||
<td>${_esc(r.itemNo)}</td>
|
||||
<td>${_esc(r.itemCategoryName)}</td>
|
||||
<td>${_esc(r.lotNo)}</td>
|
||||
<td>${r.qtyIn}</td>
|
||||
<td>${r.qtyOut}</td>
|
||||
<td>${r.qtyOnHand}</td>
|
||||
<td>${r.stockPct}</td>
|
||||
</tr>`).join("");
|
||||
|
||||
const catRows = byCat.map(c => `
|
||||
<tr><td>${_esc(c.categoryName)}</td><td>${c.avgStockPct}</td></tr>`).join("");
|
||||
|
||||
const alertRows = alerts.map(a => `
|
||||
<tr><td>${_esc(a.itemName)}</td><td>${a.qtyOnHand}</td><td>${_esc(a.severity)}</td></tr>`).join("");
|
||||
|
||||
const html = `
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
||||
xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body>
|
||||
<table border="1">
|
||||
<tr><td colspan="8"><b>Inventory Summary Report</b></td></tr>
|
||||
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr>
|
||||
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr>
|
||||
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
|
||||
<tr></tr>
|
||||
<tr>${headerCells}</tr>
|
||||
${bodyRows}
|
||||
<tr><td colspan="4"><b>Total</b></td><td>${summary.totalQtyIn ?? 0}</td><td>${summary.totalQtyOut ?? 0}</td><td>${summary.totalOnHand ?? 0}</td><td></td></tr>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<table border="1">
|
||||
<tr><td colspan="2"><b>Stock Level by Category</b></td></tr>
|
||||
<tr><th>Category</th><th>Avg Stock %</th></tr>
|
||||
${catRows}
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<table border="1">
|
||||
<tr><td colspan="3"><b>Items Requiring Attention</b></td></tr>
|
||||
<tr><th>Item</th><th>On Hand</th><th>Alert</th></tr>
|
||||
${alertRows}
|
||||
</table>
|
||||
</body></html>`;
|
||||
|
||||
downloadBlob(html, `Inventory_Report_${fromEl.value}_to_${toEl.value}.xls`,
|
||||
"application/vnd.ms-excel");
|
||||
}
|
||||
|
||||
xlsxBtn.addEventListener("click", exportExcel);
|
||||
|
||||
fetchAndRender();
|
||||
})();
|
||||
</script>
|
||||
@ -0,0 +1,694 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: 'DM Sans', Arial, sans-serif;
|
||||
background: #fff;
|
||||
color: #1a2e35;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#mrs-rpt-page, #mrs-rpt-page * {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#mrs-rpt-page {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.rpt-table tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
}
|
||||
|
||||
.rpt-page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border, #d6eaec);
|
||||
border-radius: var(--radius-lg, 14px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rpt-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
}
|
||||
/* ── FILTER BUTTONS (blended with shared theme) ── */
|
||||
.rpt-date-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1.5px solid var(--border, #d6eaec);
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
padding: 0 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.rpt-date-lbl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #6b8890);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-date-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
padding: 9px 0;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .875rem;
|
||||
color: var(--text-dark, #1a2e35);
|
||||
}
|
||||
|
||||
.rpt-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 9px 18px;
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
border: none;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-btn-primary {
|
||||
background: var(--teal-mid, #0e7c86);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(14,124,134,.3);
|
||||
}
|
||||
|
||||
.rpt-btn-primary:hover {
|
||||
background: var(--teal-dark, #0d5c63);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(14,124,134,.35);
|
||||
}
|
||||
|
||||
.rpt-btn-outline {
|
||||
background: #fff;
|
||||
color: var(--teal-dark, #0d5c63);
|
||||
border: 1.5px solid var(--teal-mid, #0e7c86);
|
||||
}
|
||||
|
||||
.rpt-btn-outline:hover {
|
||||
background: var(--teal-pale, #e6f7f8);
|
||||
border-color: var(--teal-dark, #0d5c63);
|
||||
}
|
||||
|
||||
.rpt-header-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.rpt-company {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rpt-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1a2e35;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rpt-subtitle {
|
||||
font-size: 12px;
|
||||
color: #6b8890;
|
||||
}
|
||||
|
||||
.rpt-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #EAF3DE;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rpt-logo i {
|
||||
font-size: 20px;
|
||||
color: #3B6D11;
|
||||
}
|
||||
|
||||
.rpt-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rpt-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.rpt-meta-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
}
|
||||
|
||||
.rpt-meta-val {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
padding: 14px 18px;
|
||||
border-right: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.kpi-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.kpi-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kpi-val {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.rpt-section {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.rpt-section + .rpt-section {
|
||||
border-top: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.rpt-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rpt-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rpt-table thead tr {
|
||||
background: linear-gradient(135deg, #1a3a4a, #1e5468);
|
||||
}
|
||||
|
||||
.rpt-table th {
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: rgba(255,255,255,.85);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
padding: 9px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-table td {
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rpt-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rpt-table .num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rpt-table .total-row td {
|
||||
font-weight: 700;
|
||||
background: #f0f6f7;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 50px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-bar-track {
|
||||
flex: 1;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: #d6eaec;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.fill-teal {
|
||||
background: #1D9E75;
|
||||
}
|
||||
|
||||
.fill-blue {
|
||||
background: #378ADD;
|
||||
}
|
||||
|
||||
.fill-amber {
|
||||
background: #EF9F27;
|
||||
}
|
||||
|
||||
.fill-coral {
|
||||
background: #D85A30;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
border-top: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.col-left {
|
||||
padding: 16px 24px;
|
||||
border-right: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.col-right {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.sig-block {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sig-item {
|
||||
border-top: 1px solid #d6eaec;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.sig-role {
|
||||
font-size: 10px;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.rpt-footer {
|
||||
padding: 10px 24px;
|
||||
border-top: 1px solid #d6eaec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f0f6f7;
|
||||
}
|
||||
|
||||
.rpt-footer-lbl {
|
||||
font-size: 10px;
|
||||
color: #6b8890;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
color: #6b8890;
|
||||
}
|
||||
</style>
|
||||
@await Html.PartialAsync("PagesView/Inventory/_InventoryReportHelper")
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const fromEl = document.getElementById("inv-rpt-from");
|
||||
const toEl = document.getElementById("inv-rpt-to");
|
||||
const genBtn = document.getElementById("inv-rpt-generate");
|
||||
const csvBtn = document.getElementById("inv-rpt-csv");
|
||||
const xlsxBtn = document.getElementById("inv-rpt-excel");
|
||||
const container = document.getElementById("inv-rpt-container");
|
||||
|
||||
if (!fromEl || !toEl || !container) {
|
||||
console.error("Inventory report subtab init failed — missing elements.");
|
||||
return;
|
||||
}
|
||||
|
||||
// default: first day of current month -> today
|
||||
const today = new Date();
|
||||
const first = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
toEl.value = today.toISOString().slice(0, 10);
|
||||
fromEl.value = first.toISOString().slice(0, 10);
|
||||
|
||||
let lastData = null; // cache for export
|
||||
|
||||
function buildParams() {
|
||||
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
container.innerHTML = `<div class="inv-tab-loading">
|
||||
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/InventoryReports/GetMRSReport?${buildParams()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
lastData = json.data ?? json;
|
||||
renderReport(lastData);
|
||||
} catch (err) {
|
||||
console.error("Inventory report fetch error:", err);
|
||||
container.innerHTML = `
|
||||
<div class="inv-placeholder">
|
||||
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
|
||||
<h3>Failed to load report</h3>
|
||||
<p>Please try again.</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderReport(data) {
|
||||
const summary = data.summary ?? {};
|
||||
const rows = data.rows ?? [];
|
||||
const byCond = data.byCondition ?? [];
|
||||
|
||||
const condFillMap = { "Good": "fill-teal", "Partial": "fill-amber", "Damaged": "fill-coral" };
|
||||
const maxCondQty = Math.max(1, ...byCond.map(c => c.totalQty));
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="rpt-page" id="mrs-rpt-page">
|
||||
|
||||
<div class="rpt-header">
|
||||
<div class="rpt-header-top">
|
||||
<div>
|
||||
<div class="rpt-company">${_esc(data.companyName ?? "")}</div>
|
||||
<div class="rpt-title">Material Return Slip Report</div>
|
||||
<div class="rpt-subtitle">
|
||||
Period: ${_fmtDate(data.dateFrom)} – ${_fmtDate(data.dateTo)} · All departments
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpt-logo" style="background:var(--bg-page,#f0f6f7)">
|
||||
<i class="fas fa-file-import" style="color:#185FA5"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpt-meta">
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Prepared by</span>
|
||||
<span class="rpt-meta-val">${_esc(data.preparedBy ?? "Finance Department")}</span>
|
||||
</div>
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Print date</span>
|
||||
<span class="rpt-meta-val">${_fmtDate(new Date().toISOString())}</span>
|
||||
</div>
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Report no.</span>
|
||||
<span class="rpt-meta-val">${_esc(data.reportNo ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-file-import"></i> Total MRS</div>
|
||||
<div class="kpi-val">${summary.totalMRS ?? 0}</div>
|
||||
</div>
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-undo"></i> Total qty returned</div>
|
||||
<div class="kpi-val">${summary.totalQtyReturned ?? 0}</div>
|
||||
</div>
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-tag"></i> Good condition</div>
|
||||
<div class="kpi-val">${summary.goodConditionPct ?? 0}%</div>
|
||||
<div class="kpi-sub">of returned items</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-section">
|
||||
<div class="rpt-section-title"><i class="fas fa-list"></i> Return detail</div>
|
||||
<table class="rpt-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MRS No.</th><th>Date</th><th>Against RIS</th><th>Item</th>
|
||||
<th>Returned By</th><th class="num">Qty Returned</th>
|
||||
<th>Condition</th><th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(r => `
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#185FA5">${_esc(r.mrsNo)}</td>
|
||||
<td>${_fmtDate(r.createdDate)}</td>
|
||||
<td style="color:var(--text-muted,#6b8890)">${_esc(r.risNo)}</td>
|
||||
<td>${_esc(r.itemName)}</td>
|
||||
<td>${_esc(r.returnedBy)}</td>
|
||||
<td class="num">${r.qtyReturned}</td>
|
||||
<td>${_condPill(r.condition)}</td>
|
||||
<td>${_statusPill(r.status, r.statusLabel)}</td>
|
||||
</tr>`).join("")}
|
||||
<tr class="total-row">
|
||||
<td colspan="5">Total</td>
|
||||
<td class="num">${summary.totalQtyReturned ?? 0}</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="two-col">
|
||||
<div class="col-left">
|
||||
<div class="rpt-section-title"><i class="fas fa-tag"></i> Returns by condition</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
${byCond.map(c => `
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
|
||||
<span>${_esc(c.condition)}</span>
|
||||
<span style="font-weight:600">${c.totalQty} pcs</span>
|
||||
</div>
|
||||
<div class="mini-bar-track">
|
||||
<div class="mini-bar-fill ${condFillMap[c.condition] ?? 'fill-teal'}"
|
||||
style="width:${Math.round((c.totalQty / maxCondQty) * 100)}%"></div>
|
||||
</div>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-right">
|
||||
<div class="rpt-section-title"><i class="fas fa-exchange-alt"></i> RIS vs MRS comparison</div>
|
||||
<table class="rpt-table">
|
||||
<thead><tr><th>Metric</th><th class="num">Value</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Total qty issued (RIS)</td><td class="num">${summary.totalQtyIssuedRIS ?? 0}</td></tr>
|
||||
<tr><td>Total qty returned (MRS)</td><td class="num">${summary.totalQtyReturned ?? 0}</td></tr>
|
||||
<tr><td style="font-weight:600">Net qty consumed</td><td class="num" style="font-weight:600">${summary.netQtyConsumed ?? 0}</td></tr>
|
||||
<tr><td>Return rate</td><td class="num">${summary.returnRatePct ?? 0}%</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-section">
|
||||
<div class="sig-block">
|
||||
<div class="sig-item"><div class="sig-role">Prepared by</div><div class="sig-name">Finance Officer</div></div>
|
||||
<div class="sig-item"><div class="sig-role">Reviewed by</div><div class="sig-name">Finance Manager</div></div>
|
||||
<div class="sig-item"><div class="sig-role">Approved by</div><div class="sig-name">Finance Director</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-footer">
|
||||
<span class="rpt-footer-lbl">${_esc(data.companyName ?? "")} — Confidential — For internal use only</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
csvBtn.addEventListener("click", exportCsv);
|
||||
genBtn.addEventListener("click", fetchAndRender);
|
||||
|
||||
function _condPill(cond) {
|
||||
const map = {
|
||||
"Good": { bg: "#EAF3DE", fg: "#3B6D11", icon: "fa-check" },
|
||||
"Partial": { bg: "#FAEEDA", fg: "#854F0B", icon: "fa-adjust" },
|
||||
"Damaged": { bg: "#FCEBEB", fg: "#A32D2D", icon: "fa-exclamation-circle" }
|
||||
};
|
||||
const c = map[cond] ?? map["Good"];
|
||||
return `<span class="pill" style="background:${c.bg};color:${c.fg}">
|
||||
<i class="fas ${c.icon}"></i> ${_esc(cond)}</span>`;
|
||||
}
|
||||
|
||||
function _statusPill(status, label) {
|
||||
const cls = status === 1 ? "pill-approved" : status === 2 ? "pill-cancelled" : "pill-draft";
|
||||
const icon = status === 1 ? "fa-check" : status === 2 ? "fa-ban" : "fa-clock";
|
||||
return `<span class="pill ${cls}"><i class="fas ${icon}"></i> ${_esc(label)}</span>`;
|
||||
}
|
||||
|
||||
|
||||
// ── CSV export (MRS schema) ──
|
||||
function exportCsv() {
|
||||
if (!lastData) return;
|
||||
const rows = lastData.rows ?? [];
|
||||
const byCond = lastData.byCondition ?? [];
|
||||
const summary = lastData.summary ?? {};
|
||||
const headers = ["MRS No.","Date","Against RIS","Item","Returned By","Qty Returned","Condition","Status"];
|
||||
|
||||
const csvLines = [
|
||||
`Material Return Slip Report`,
|
||||
`Company,${csvCell(lastData.companyName)}`,
|
||||
`Report No,${csvCell(lastData.reportNo)}`,
|
||||
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
|
||||
``,
|
||||
headers.map(csvCell).join(","),
|
||||
...rows.map(r => [
|
||||
r.mrsNo, r.createdDate, r.risNo, r.itemName,
|
||||
r.returnedBy, r.qtyReturned, r.condition, r.statusLabel
|
||||
].map(csvCell).join(",")),
|
||||
["Total","","","","", summary.totalQtyReturned ?? 0, "", ""].map(csvCell).join(","),
|
||||
``,
|
||||
`Returns by Condition`,
|
||||
["Condition","Total Qty"].map(csvCell).join(","),
|
||||
...byCond.map(c => [c.condition, c.totalQty].map(csvCell).join(",")),
|
||||
``,
|
||||
`RIS vs MRS Comparison`,
|
||||
["Metric","Value"].map(csvCell).join(","),
|
||||
["Total qty issued (RIS)", summary.totalQtyIssuedRIS ?? 0].map(csvCell).join(","),
|
||||
["Total qty returned (MRS)", summary.totalQtyReturned ?? 0].map(csvCell).join(","),
|
||||
["Net qty consumed", summary.netQtyConsumed ?? 0].map(csvCell).join(","),
|
||||
["Return rate %", summary.returnRatePct ?? 0].map(csvCell).join(",")
|
||||
];
|
||||
|
||||
downloadBlob(
|
||||
"\uFEFF" + csvLines.join("\r\n"),
|
||||
`MRS_Report_${fromEl.value}_to_${toEl.value}.csv`,
|
||||
"text/csv;charset=utf-8;"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Excel export (MRS schema, HTML-table .xls) ──
|
||||
function exportExcel() {
|
||||
if (!lastData) return;
|
||||
const rows = lastData.rows ?? [];
|
||||
const byCond = lastData.byCondition ?? [];
|
||||
const summary = lastData.summary ?? {};
|
||||
|
||||
const headerCells = ["MRS No.","Date","Against RIS","Item","Returned By","Qty Returned","Condition","Status"]
|
||||
.map(h => `<th>${_esc(h)}</th>`).join("");
|
||||
|
||||
const bodyRows = rows.map(r => `
|
||||
<tr>
|
||||
<td>${_esc(r.mrsNo)}</td>
|
||||
<td>${_esc(_fmtDate(r.createdDate))}</td>
|
||||
<td>${_esc(r.risNo)}</td>
|
||||
<td>${_esc(r.itemName)}</td>
|
||||
<td>${_esc(r.returnedBy)}</td>
|
||||
<td>${r.qtyReturned}</td>
|
||||
<td>${_esc(r.condition)}</td>
|
||||
<td>${_esc(r.statusLabel)}</td>
|
||||
</tr>`).join("");
|
||||
|
||||
const condRows = byCond.map(c => `
|
||||
<tr><td>${_esc(c.condition)}</td><td>${c.totalQty}</td></tr>`).join("");
|
||||
|
||||
const html = `
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
||||
xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body>
|
||||
<table border="1">
|
||||
<tr><td colspan="8"><b>Material Return Slip Report</b></td></tr>
|
||||
<tr><td>Company</td><td colspan="7">${_esc(lastData.companyName)}</td></tr>
|
||||
<tr><td>Report No</td><td colspan="7">${_esc(lastData.reportNo)}</td></tr>
|
||||
<tr><td>Period</td><td colspan="7">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
|
||||
<tr></tr>
|
||||
<tr>${headerCells}</tr>
|
||||
${bodyRows}
|
||||
<tr><td colspan="5"><b>Total</b></td><td>${summary.totalQtyReturned ?? 0}</td><td colspan="2"></td></tr>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<table border="1">
|
||||
<tr><td colspan="2"><b>Returns by Condition</b></td></tr>
|
||||
<tr><th>Condition</th><th>Total Qty</th></tr>
|
||||
${condRows}
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<table border="1">
|
||||
<tr><td colspan="2"><b>RIS vs MRS Comparison</b></td></tr>
|
||||
<tr><th>Metric</th><th>Value</th></tr>
|
||||
<tr><td>Total qty issued (RIS)</td><td>${summary.totalQtyIssuedRIS ?? 0}</td></tr>
|
||||
<tr><td>Total qty returned (MRS)</td><td>${summary.totalQtyReturned ?? 0}</td></tr>
|
||||
<tr><td>Net qty consumed</td><td>${summary.netQtyConsumed ?? 0}</td></tr>
|
||||
<tr><td>Return rate %</td><td>${summary.returnRatePct ?? 0}</td></tr>
|
||||
</table>
|
||||
</body></html>`;
|
||||
|
||||
downloadBlob(html, `MRS_Report_${fromEl.value}_to_${toEl.value}.xls`,
|
||||
"application/vnd.ms-excel");
|
||||
}
|
||||
xlsxBtn.addEventListener("click", exportExcel);
|
||||
fetchAndRender();
|
||||
})();
|
||||
</script>
|
||||
@ -0,0 +1,700 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: 'DM Sans', Arial, sans-serif;
|
||||
background: #fff;
|
||||
color: #1a2e35;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#ris-rpt-page, #ris-rpt-page * {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#ris-rpt-page {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.rpt-table tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
}
|
||||
|
||||
.rpt-page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border, #d6eaec);
|
||||
border-radius: var(--radius-lg, 14px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rpt-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
}
|
||||
/* ── FILTER BUTTONS (blended with shared theme) ── */
|
||||
.rpt-date-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1.5px solid var(--border, #d6eaec);
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
padding: 0 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.rpt-date-lbl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #6b8890);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-date-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
padding: 9px 0;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .875rem;
|
||||
color: var(--text-dark, #1a2e35);
|
||||
}
|
||||
|
||||
.rpt-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 9px 18px;
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
border: none;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: .84rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-btn-primary {
|
||||
background: var(--teal-mid, #0e7c86);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(14,124,134,.3);
|
||||
}
|
||||
|
||||
.rpt-btn-primary:hover {
|
||||
background: var(--teal-dark, #0d5c63);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(14,124,134,.35);
|
||||
}
|
||||
|
||||
.rpt-btn-outline {
|
||||
background: #fff;
|
||||
color: var(--teal-dark, #0d5c63);
|
||||
border: 1.5px solid var(--teal-mid, #0e7c86);
|
||||
}
|
||||
|
||||
.rpt-btn-outline:hover {
|
||||
background: var(--teal-pale, #e6f7f8);
|
||||
border-color: var(--teal-dark, #0d5c63);
|
||||
}
|
||||
|
||||
.rpt-header-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.rpt-company {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rpt-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1a2e35;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rpt-subtitle {
|
||||
font-size: 12px;
|
||||
color: #6b8890;
|
||||
}
|
||||
|
||||
.rpt-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #EAF3DE;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rpt-logo i {
|
||||
font-size: 20px;
|
||||
color: #3B6D11;
|
||||
}
|
||||
|
||||
.rpt-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rpt-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.rpt-meta-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
}
|
||||
|
||||
.rpt-meta-val {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
padding: 14px 18px;
|
||||
border-right: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.kpi-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.kpi-lbl {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kpi-val {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.rpt-section {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.rpt-section + .rpt-section {
|
||||
border-top: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.rpt-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rpt-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rpt-table thead tr {
|
||||
background: linear-gradient(135deg, #1a3a4a, #1e5468);
|
||||
}
|
||||
|
||||
.rpt-table th {
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: rgba(255,255,255,.85);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
padding: 9px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpt-table td {
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rpt-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rpt-table .num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rpt-table .total-row td {
|
||||
font-weight: 700;
|
||||
background: #f0f6f7;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 50px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-bar-track {
|
||||
flex: 1;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: #d6eaec;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.fill-teal {
|
||||
background: #1D9E75;
|
||||
}
|
||||
|
||||
.fill-blue {
|
||||
background: #378ADD;
|
||||
}
|
||||
|
||||
.fill-amber {
|
||||
background: #EF9F27;
|
||||
}
|
||||
|
||||
.fill-coral {
|
||||
background: #D85A30;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
border-top: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.col-left {
|
||||
padding: 16px 24px;
|
||||
border-right: 1px solid #d6eaec;
|
||||
}
|
||||
|
||||
.col-right {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.sig-block {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sig-item {
|
||||
border-top: 1px solid #d6eaec;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.sig-role {
|
||||
font-size: 10px;
|
||||
color: #6b8890;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #1a2e35;
|
||||
}
|
||||
|
||||
.rpt-footer {
|
||||
padding: 10px 24px;
|
||||
border-top: 1px solid #d6eaec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f0f6f7;
|
||||
}
|
||||
|
||||
.rpt-footer-lbl {
|
||||
font-size: 10px;
|
||||
color: #6b8890;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
color: #6b8890;
|
||||
}
|
||||
</style>
|
||||
@await Html.PartialAsync("PagesView/Inventory/_InventoryReportHelper")
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const fromEl = document.getElementById("inv-rpt-from");
|
||||
const toEl = document.getElementById("inv-rpt-to");
|
||||
const genBtn = document.getElementById("inv-rpt-generate");
|
||||
const csvBtn = document.getElementById("inv-rpt-csv");
|
||||
const xlsxBtn = document.getElementById("inv-rpt-excel");
|
||||
const container = document.getElementById("inv-rpt-container");
|
||||
|
||||
if (!fromEl || !toEl || !container) {
|
||||
console.error("Inventory report subtab init failed — missing elements.");
|
||||
return;
|
||||
}
|
||||
|
||||
// default: first day of current month -> today
|
||||
const today = new Date();
|
||||
const first = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
toEl.value = today.toISOString().slice(0, 10);
|
||||
fromEl.value = first.toISOString().slice(0, 10);
|
||||
|
||||
let lastData = null; // cache for export
|
||||
|
||||
function buildParams() {
|
||||
return new URLSearchParams({ dateFrom: fromEl.value, dateTo: toEl.value });
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
container.innerHTML = `<div class="inv-tab-loading">
|
||||
<div class="inv-spinner"></div><span>Loading report…</span></div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/InventoryReports/GetRISReport?${buildParams()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
lastData = json.data ?? json;
|
||||
renderReport(lastData);
|
||||
} catch (err) {
|
||||
console.error("Inventory report fetch error:", err);
|
||||
container.innerHTML = `
|
||||
<div class="inv-placeholder">
|
||||
<i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i>
|
||||
<h3>Failed to load report</h3>
|
||||
<p>Please try again.</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
csvBtn.addEventListener("click", exportCsv);
|
||||
genBtn.addEventListener("click", fetchAndRender);
|
||||
// ── CSV export (client-side, no backend needed) ──
|
||||
function exportCsv() {
|
||||
if (!lastData) return;
|
||||
const rows = lastData.rows ?? [];
|
||||
const byDisc = lastData.byDiscipline ?? [];
|
||||
const topRecv = lastData.topRecipients ?? [];
|
||||
const summary = lastData.summary ?? {};
|
||||
const headers = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"];
|
||||
|
||||
const csvLines = [
|
||||
`Return Issuance Slip Report`,
|
||||
`Company,${csvCell(lastData.companyName)}`,
|
||||
`Report No,${csvCell(lastData.reportNo)}`,
|
||||
`Period,${csvCell(fromEl.value)} to ${csvCell(toEl.value)}`,
|
||||
``,
|
||||
headers.map(csvCell).join(","),
|
||||
...rows.map(r => [
|
||||
r.risNo, r.createdDate, r.itemName, r.itemNo, r.disciplineName,
|
||||
r.issuedTo, r.qtyIssued, r.totalReturned, r.netIssued, r.statusLabel
|
||||
].map(csvCell).join(",")),
|
||||
["Total","","","","","", summary.totalQtyIssued ?? 0, summary.totalQtyReturned ?? 0, summary.totalNetIssued ?? 0, ""].map(csvCell).join(","),
|
||||
``,
|
||||
`Issuance by Discipline`,
|
||||
["Discipline","Slips"].map(csvCell).join(","),
|
||||
...byDisc.map(d => [d.disciplineName, d.count].map(csvCell).join(",")),
|
||||
``,
|
||||
`Top Recipients`,
|
||||
["Name","Slips","Qty Out"].map(csvCell).join(","),
|
||||
...topRecv.map(t => [t.issuedTo, t.slipCount, t.totalQty].map(csvCell).join(","))
|
||||
];
|
||||
|
||||
downloadBlob(
|
||||
"\uFEFF" + csvLines.join("\r\n"),
|
||||
`RIS_Report_${fromEl.value}_to_${toEl.value}.csv`,
|
||||
"text/csv;charset=utf-8;"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function downloadBlob(content, filename, mime) {
|
||||
const blob = new Blob([content], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = filename;
|
||||
document.body.appendChild(a); a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function renderReport(data) {
|
||||
const summary = data.summary ?? {};
|
||||
const rows = data.rows ?? [];
|
||||
const byDisc = data.byDiscipline ?? [];
|
||||
const topRecv = data.topRecipients ?? [];
|
||||
|
||||
const maxDiscCount = Math.max(1, ...byDisc.map(d => d.count));
|
||||
const fillColors = ["fill-teal", "fill-blue", "fill-amber", "fill-coral"];
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="rpt-page" id="ris-rpt-page">
|
||||
|
||||
<div class="rpt-header">
|
||||
<div class="rpt-header-top">
|
||||
<div>
|
||||
<div class="rpt-company">${_esc(data.companyName ?? "")}</div>
|
||||
<div class="rpt-title">Return Issuance Slip Report</div>
|
||||
<div class="rpt-subtitle">
|
||||
Period: ${_fmtDate(data.dateFrom)} – ${_fmtDate(data.dateTo)} · All departments · All disciplines
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpt-logo"><i class="fas fa-file-export"></i></div>
|
||||
</div>
|
||||
<div class="rpt-meta">
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Prepared by</span>
|
||||
<span class="rpt-meta-val">${_esc(data.preparedBy ?? "Finance Department")}</span>
|
||||
</div>
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Print date</span>
|
||||
<span class="rpt-meta-val">${_fmtDate(new Date().toISOString())}</span>
|
||||
</div>
|
||||
<div class="rpt-meta-item">
|
||||
<span class="rpt-meta-lbl">Report no.</span>
|
||||
<span class="rpt-meta-val">${_esc(data.reportNo ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-file-export"></i> Total RIS issued</div>
|
||||
<div class="kpi-val">${summary.totalRIS ?? 0}</div>
|
||||
</div>
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-check-circle"></i> Approved</div>
|
||||
<div class="kpi-val">${summary.totalApproved ?? 0}</div>
|
||||
<div class="kpi-sub">${summary.approvalRatePct ?? 0}% approval rate</div>
|
||||
</div>
|
||||
<div class="kpi-cell">
|
||||
<div class="kpi-lbl"><i class="fas fa-clock"></i> Pending / Cancelled</div>
|
||||
<div class="kpi-val">${summary.totalPending ?? 0} / ${summary.totalCancelled ?? 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-section">
|
||||
<div class="rpt-section-title"><i class="fas fa-list"></i> Issuance detail</div>
|
||||
<table class="rpt-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RIS No.</th><th>Date</th><th>Item</th><th>Item No.</th>
|
||||
<th>Discipline</th><th>Issued To</th>
|
||||
<th class="num">Qty Issued</th><th class="num">Qty Returned</th>
|
||||
<th class="num">Net Out</th><th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(r => `
|
||||
<tr>
|
||||
<td style="font-weight:600;color:var(--teal-dark,#0d5c63)">${_esc(r.risNo)}</td>
|
||||
<td>${_fmtDate(r.createdDate)}</td>
|
||||
<td>${_esc(r.itemName)}</td>
|
||||
<td style="color:var(--text-muted,#6b8890)">#${_esc(r.itemNo)}</td>
|
||||
<td>${_esc(r.disciplineName)}</td>
|
||||
<td>${_esc(r.issuedTo)}</td>
|
||||
<td class="num">${r.qtyIssued}</td>
|
||||
<td class="num">${r.totalReturned}</td>
|
||||
<td class="num" style="font-weight:600">${r.netIssued}</td>
|
||||
<td>${_statusPill(r.status, r.statusLabel)}</td>
|
||||
</tr>`).join("")}
|
||||
<tr class="total-row">
|
||||
<td colspan="6">Total</td>
|
||||
<td class="num">${summary.totalQtyIssued ?? 0}</td>
|
||||
<td class="num">${summary.totalQtyReturned ?? 0}</td>
|
||||
<td class="num">${summary.totalNetIssued ?? 0}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="two-col">
|
||||
<div class="col-left">
|
||||
<div class="rpt-section-title"><i class="fas fa-chart-bar"></i> Issuance by discipline</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
${byDisc.map((d, i) => `
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">
|
||||
<span>${_esc(d.disciplineName)}</span>
|
||||
<span style="font-weight:600">${d.count} slips</span>
|
||||
</div>
|
||||
<div class="mini-bar-track">
|
||||
<div class="mini-bar-fill ${fillColors[i % fillColors.length]}"
|
||||
style="width:${Math.round((d.count / maxDiscCount) * 100)}%"></div>
|
||||
</div>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-right">
|
||||
<div class="rpt-section-title"><i class="fas fa-users"></i> Top recipients</div>
|
||||
<table class="rpt-table">
|
||||
<thead><tr><th>Name</th><th class="num">Slips</th><th class="num">Qty Out</th></tr></thead>
|
||||
<tbody>
|
||||
${topRecv.map(t => `
|
||||
<tr>
|
||||
<td>${_esc(t.issuedTo)}</td>
|
||||
<td class="num">${t.slipCount}</td>
|
||||
<td class="num">${t.totalQty}</td>
|
||||
</tr>`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-section">
|
||||
<div class="sig-block">
|
||||
<div class="sig-item"><div class="sig-role">Prepared by</div><div class="sig-name">Finance Officer</div></div>
|
||||
<div class="sig-item"><div class="sig-role">Reviewed by</div><div class="sig-name">Finance Manager</div></div>
|
||||
<div class="sig-item"><div class="sig-role">Approved by</div><div class="sig-name">Finance Director</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpt-footer">
|
||||
<span class="rpt-footer-lbl">${_esc(data.companyName ?? "")} — Confidential — For internal use only</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _statusPill(status, label) {
|
||||
const cls = status === 1 ? "pill-approved" : status === 2 ? "pill-cancelled" : "pill-draft";
|
||||
const icon = status === 1 ? "fa-check" : status === 2 ? "fa-ban" : "fa-clock";
|
||||
return `<span class="pill ${cls}"><i class="fas ${icon}"></i> ${_esc(label)}</span>`;
|
||||
}
|
||||
|
||||
// ── Excel export (client-side, HTML-table .xls — Excel opens natively) ──
|
||||
function exportExcel() {
|
||||
if (!lastData) return;
|
||||
const rows = lastData.rows ?? [];
|
||||
const byDisc = lastData.byDiscipline ?? [];
|
||||
const topRecv = lastData.topRecipients ?? [];
|
||||
const summary = lastData.summary ?? {};
|
||||
|
||||
const headerCells = ["RIS No.","Date","Item","Item No.","Discipline","Issued To","Qty Issued","Qty Returned","Net Out","Status"]
|
||||
.map(h => `<th>${_esc(h)}</th>`).join("");
|
||||
|
||||
const bodyRows = rows.map(r => `
|
||||
<tr>
|
||||
<td>${_esc(r.risNo)}</td>
|
||||
<td>${_esc(_fmtDate(r.createdDate))}</td>
|
||||
<td>${_esc(r.itemName)}</td>
|
||||
<td>${_esc(r.itemNo)}</td>
|
||||
<td>${_esc(r.disciplineName)}</td>
|
||||
<td>${_esc(r.issuedTo)}</td>
|
||||
<td>${r.qtyIssued}</td>
|
||||
<td>${r.totalReturned}</td>
|
||||
<td>${r.netIssued}</td>
|
||||
<td>${_esc(r.statusLabel)}</td>
|
||||
</tr>`).join("");
|
||||
|
||||
const discRows = byDisc.map(d => `
|
||||
<tr><td>${_esc(d.disciplineName)}</td><td>${d.count}</td></tr>`).join("");
|
||||
|
||||
const recvRows = topRecv.map(t => `
|
||||
<tr><td>${_esc(t.issuedTo)}</td><td>${t.slipCount}</td><td>${t.totalQty}</td></tr>`).join("");
|
||||
|
||||
const html = `
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
||||
xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body>
|
||||
<table border="1">
|
||||
<tr><td colspan="10"><b>Return Issuance Slip Report</b></td></tr>
|
||||
<tr><td>Company</td><td colspan="9">${_esc(lastData.companyName)}</td></tr>
|
||||
<tr><td>Report No</td><td colspan="9">${_esc(lastData.reportNo)}</td></tr>
|
||||
<tr><td>Period</td><td colspan="9">${_esc(fromEl.value)} to ${_esc(toEl.value)}</td></tr>
|
||||
<tr></tr>
|
||||
<tr>${headerCells}</tr>
|
||||
${bodyRows}
|
||||
<tr><td colspan="6"><b>Total</b></td><td>${summary.totalQtyIssued ?? 0}</td><td>${summary.totalQtyReturned ?? 0}</td><td>${summary.totalNetIssued ?? 0}</td><td></td></tr>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<table border="1">
|
||||
<tr><td colspan="2"><b>Issuance by Discipline</b></td></tr>
|
||||
<tr><th>Discipline</th><th>Slips</th></tr>
|
||||
${discRows}
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<table border="1">
|
||||
<tr><td colspan="3"><b>Top Recipients</b></td></tr>
|
||||
<tr><th>Name</th><th>Slips</th><th>Qty Out</th></tr>
|
||||
${recvRows}
|
||||
</table>
|
||||
</body></html>`;
|
||||
|
||||
downloadBlob(html, `RIS_Report_${fromEl.value}_to_${toEl.value}.xls`,
|
||||
"application/vnd.ms-excel");
|
||||
}
|
||||
xlsxBtn.addEventListener("click", exportExcel);
|
||||
// ── Initial load ──
|
||||
fetchAndRender();
|
||||
})();
|
||||
</script>
|
||||
@ -7,7 +7,7 @@
|
||||
<link href="~/lib/font-awesome/css/all.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link href="~/css/spinner.css" rel="stylesheet" />
|
||||
<link href="~/css/toast-notifications.css" rel="stylesheet" />
|
||||
<link href="~/css/toast-notificationsV2.css" rel="stylesheet" />
|
||||
<link href="~/css/loginpageinteractive.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -19,18 +19,20 @@
|
||||
<i class="fas fa-user-tag"></i> Inventory
|
||||
</button>
|
||||
<button class="inv-tab-btn" data-tab-id="2" role="tab">
|
||||
<i class="fas fa-clock"></i> For Approval
|
||||
<i class="fas fa-file-export"></i>RIS
|
||||
</button>
|
||||
<button class="inv-tab-btn" data-tab-id="3" role="tab">
|
||||
<i class="fas fa-id-badge"></i> Return Issuance Slip (Report)
|
||||
<i class="fas fa-store"></i> MRS
|
||||
</button>
|
||||
<button class="inv-tab-btn" data-tab-id="4" role="tab">
|
||||
<i class="fas fa-store"></i> Material Return Slip (Report)
|
||||
<i class="fa-solid fa-list-check"></i> Inventory Reports
|
||||
</button>
|
||||
<button class="inv-tab-btn" data-tab-id="5" role="tab">
|
||||
<i class="fas fa-check-circle"></i> Transaction History
|
||||
<i class="fa-solid fa-file-export"></i> RIS Reports
|
||||
</button>
|
||||
<button class="inv-tab-btn" data-tab-id="6" role="tab">
|
||||
<i class="fa-solid fa-arrow-rotate-left"></i> MRS Reports
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="inv-tab-content">
|
||||
@ -76,6 +78,7 @@
|
||||
const html = await res.text();
|
||||
cache[tabId] = html;
|
||||
injectHtml(tabContent, html);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Tab load error:", err);
|
||||
tabContent.innerHTML = `
|
||||
|
||||
84
CPRNIMS.WebApps/Views/RISMgmt/RISReport - Copy.cshtml
Normal file
84
CPRNIMS.WebApps/Views/RISMgmt/RISReport - Copy.cshtml
Normal file
@ -0,0 +1,84 @@
|
||||
@model FastReport.Web.WebReport
|
||||
@{
|
||||
Layout = null;
|
||||
var from = (DateTime)ViewBag.DateFrom;
|
||||
var to = (DateTime)ViewBag.DateTo;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>RIS Report — @from.ToString("MMM d") to @to.ToString("MMM d, yyyy")</title>
|
||||
@* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" /> *@
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
background: #f0f6f7;
|
||||
}
|
||||
|
||||
.rep-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #0d5c63, #0e7c86);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rep-bar h1 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rep-bar .period {
|
||||
font-size: .8rem;
|
||||
color: rgba(255,255,255,.7);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.rep-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,.15);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: .85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rep-back:hover {
|
||||
background: rgba(255,255,255,.28);
|
||||
}
|
||||
|
||||
.rep-body {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="rep-bar">
|
||||
<div>
|
||||
<h1><i class="fas fa-file-lines"></i> Return Issuance Slip Report</h1>
|
||||
<div class="period">
|
||||
Period: @from.ToString("MMMM d, yyyy") – @to.ToString("MMMM d, yyyy")
|
||||
</div>
|
||||
</div>
|
||||
<a href="javascript:window.close()" class="rep-back">
|
||||
<i class="fas fa-xmark"></i> Close
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="rep-body">
|
||||
@await Model.Render()
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
93
CPRNIMS.WebApps/Views/RISMgmt/RISReport.cshtml
Normal file
93
CPRNIMS.WebApps/Views/RISMgmt/RISReport.cshtml
Normal file
@ -0,0 +1,93 @@
|
||||
<!-- ReportDesigner.cshtml -->
|
||||
<link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/grapesjs"></script>
|
||||
|
||||
<div style="display:flex;gap:8px;padding:10px">
|
||||
<button id="btn-save">Save Template</button>
|
||||
<button id="btn-preview">Preview with Sample Data</button>
|
||||
<select id="report-type">
|
||||
<option value="ris">RIS Report</option>
|
||||
<option value="mrs">MRS Report</option>
|
||||
<option value="inventory">Inventory Report</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="gjs-editor"></div>
|
||||
|
||||
<script>
|
||||
const editor = grapesjs.init({
|
||||
container: '#gjs-editor',
|
||||
height: '100vh',
|
||||
fromElement: false,
|
||||
storageManager: false, // we handle save/load ourselves
|
||||
|
||||
// ── Block manager: drag-drop components your team can use ──
|
||||
blockManager: {
|
||||
blocks: [
|
||||
{
|
||||
id: 'data-field',
|
||||
label: 'Data Field',
|
||||
content: '<span class="df-token">{{FieldName}}</span>',
|
||||
category: 'Report Fields'
|
||||
},
|
||||
{
|
||||
id: 'table-block',
|
||||
label: 'Data Table',
|
||||
content: `<table class="rpt-table">
|
||||
<thead><tr><th>Column 1</th><th>Column 2</th></tr></thead>
|
||||
<tbody><tr><td>{{Field1}}</td><td>{{Field2}}</td></tr></tbody>
|
||||
</table>`,
|
||||
category: 'Report Fields'
|
||||
},
|
||||
{
|
||||
id: 'kpi-card',
|
||||
label: 'KPI Card',
|
||||
content: `<div class="kpi-cell">
|
||||
<div class="kpi-lbl">Label</div>
|
||||
<div class="kpi-val">{{KpiValue}}</div>
|
||||
</div>`,
|
||||
category: 'Report Fields'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// ── Load existing template when report type changes ──
|
||||
document.getElementById('report-type').addEventListener('change', async (e) => {
|
||||
const res = await fetch(`/ReportDesigner/GetTemplate?type=${e.target.value}`);
|
||||
const data = await res.json();
|
||||
editor.setComponents(data.html || '');
|
||||
editor.setStyle(data.css || '');
|
||||
});
|
||||
|
||||
// ── Save template ──
|
||||
document.getElementById('btn-save').addEventListener('click', async () => {
|
||||
const html = editor.getHtml();
|
||||
const css = editor.getCss();
|
||||
const type = document.getElementById('report-type').value;
|
||||
|
||||
await fetch('/ReportDesigner/SaveTemplate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reportType: type, html, css })
|
||||
});
|
||||
|
||||
alert('Template saved.');
|
||||
});
|
||||
|
||||
// ── Preview with real sample data ──
|
||||
document.getElementById('btn-preview').addEventListener('click', async () => {
|
||||
const html = editor.getHtml();
|
||||
const css = editor.getCss();
|
||||
const type = document.getElementById('report-type').value;
|
||||
|
||||
const res = await fetch('/ReportDesigner/PreviewPdf', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reportType: type, html, css })
|
||||
});
|
||||
|
||||
const blob = await res.blob();
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,120 @@
|
||||
<div class="inv-filters no-print">
|
||||
<div class="rpt-date-group">
|
||||
<label class="rpt-date-lbl"><i class="fas fa-calendar-check"></i> From</label>
|
||||
<input type="date" id="inv-rpt-from" class="rpt-date-input">
|
||||
</div>
|
||||
<div class="rpt-date-group">
|
||||
<label class="rpt-date-lbl"><i class="fas fa-calendar-check"></i> To</label>
|
||||
<input type="date" id="inv-rpt-to" class="rpt-date-input">
|
||||
</div>
|
||||
<button id="inv-rpt-generate" class="rpt-btn rpt-btn-primary">
|
||||
<i class="fas fa-sync"></i> Generate
|
||||
</button>
|
||||
<button id="inv-rpt-print" class="rpt-btn rpt-btn-outline" onclick="window.print()">
|
||||
<i class="fas fa-print"></i> Print / PDF
|
||||
</button>
|
||||
<button id="inv-rpt-csv" class="rpt-btn rpt-btn-outline">
|
||||
<i class="fas fa-file-csv"></i> CSV
|
||||
</button>
|
||||
<button id="inv-rpt-excel" class="rpt-btn rpt-btn-outline">
|
||||
<i class="fas fa-file-excel"></i> Excel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="inv-rpt-container">
|
||||
<div class="inv-tab-loading">
|
||||
<div class="inv-spinner"></div>
|
||||
<span>Loading report…</span>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
/* ── Tablet: tabs in a 3-per-row grid for tidier wrapping ── */
|
||||
@@media (max-width: 1024px) {
|
||||
.inv-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.inv-tab-btn {
|
||||
min-width: 0; /* let grid control the width */
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile: 2 per row, smaller text/padding ── */
|
||||
@@media (max-width: 600px) {
|
||||
.inv-tabs {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.inv-tab-btn {
|
||||
padding: 10px 8px;
|
||||
font-size: .8rem;
|
||||
gap: 5px;
|
||||
}
|
||||
/* optional: hide icons on very small screens to save space */
|
||||
.inv-tab-btn i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* ── Very small phones: stack one per row ── */
|
||||
@@media (max-width: 380px) {
|
||||
.inv-tabs {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@media (max-width: 768px) {
|
||||
.rpt-section .rpt-table, .two-col .rpt-table
|
||||
|
||||
{
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* stack the two-column section vertically on mobile */
|
||||
.two-col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.col-left {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #d6eaec;
|
||||
}
|
||||
/* KPI strip: 2 per row instead of 4 */
|
||||
.kpi-strip {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.kpi-cell:nth-child(2) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// ── shared helpers ──
|
||||
function csvCell(v) {
|
||||
const s = String(v ?? "");
|
||||
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}
|
||||
|
||||
function downloadBlob(content, filename, mime) {
|
||||
const blob = new Blob([content], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = filename;
|
||||
document.body.appendChild(a); a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function _esc(s) {
|
||||
return String(s ?? "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
}
|
||||
|
||||
function _fmtDate(raw) {
|
||||
if (!raw) return "—";
|
||||
const d = new Date(raw);
|
||||
return isNaN(d) ? raw : d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
||||
}
|
||||
|
||||
</script>
|
||||
@ -96,6 +96,7 @@
|
||||
.inv-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 6px;
|
||||
@ -104,7 +105,8 @@
|
||||
}
|
||||
|
||||
.inv-tab-btn {
|
||||
flex: 1;
|
||||
flex: 1 1 auto; /* grow, but allow shrinking and wrapping */
|
||||
min-width: 140px; /* keep each tab readable before it wraps */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<script src="~/js/cachebuster.js"></script>
|
||||
@* <script src="~/js/cachebuster.js"></script> *@
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/lib/jquery/dist/jquery371.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@using CPRNIMS.WebApps
|
||||
@using CPRNIMS.WebApps.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, FastReport.Web
|
||||
@using FastReport.Web
|
||||
|
||||
Loading…
Reference in New Issue
Block a user