AI Driven Searching and tagging with button and UI/UX enhancement in canvassing partial only

This commit is contained in:
rowell_m_soriano 2026-04-22 17:21:45 +08:00
parent 3825a986f6
commit d1c9c4b52b
39 changed files with 4396 additions and 226 deletions

View File

@ -2,6 +2,7 @@
using CPRNIMS.Infrastructure.Dto.Canvass; using CPRNIMS.Infrastructure.Dto.Canvass;
using CPRNIMS.Infrastructure.Dto.Canvass.Request; using CPRNIMS.Infrastructure.Dto.Canvass.Request;
using CPRNIMS.Infrastructure.Dto.Canvass.Response; using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Entities.Canvass; using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Entities.Purchasing; using CPRNIMS.Infrastructure.Entities.Purchasing;
using System; using System;
@ -32,16 +33,17 @@ namespace CPRNIMS.Domain.Contracts.Canvass
Task<List<PRCanvassDetail>> GetCanvassById(CanvassDto CanvassDto); Task<List<PRCanvassDetail>> GetCanvassById(CanvassDto CanvassDto);
Task<List<WOResponse>> GetCanvassWOResponse(CanvassDto CanvassDto); Task<List<WOResponse>> GetCanvassWOResponse(CanvassDto CanvassDto);
Task<List<WOResponseById>> GetWOResponseBySuppId(CanvassDto CanvassDto); Task<List<WOResponseById>> GetWOResponseBySuppId(CanvassDto CanvassDto);
Task<List<SupplierResponse>> GetSupplierById(CanvassDto CanvassDto); Task<List<SupplierResponseDto>> GetSupplierById(CanvassDto CanvassDto);
Task<List<RFQReference>> GetRFQ(CanvassDto CanvassDto); Task<List<RFQReference>> GetRFQ(CanvassDto CanvassDto);
Task<List<BiddingItem>> GetSupplierBid(CanvassDto CanvassDto); Task<List<BiddingItem>> GetSupplierBid(CanvassDto CanvassDto);
Task<List<RFQPerSupplier>> GetSupplierBidByItem(CanvassDto CanvassDto); Task<List<RFQPerSupplier>> GetSupplierBidByItem(CanvassDto CanvassDto);
Task<List<SupplierBidById>> GetSupplierBidById(CanvassDto CanvassDto); Task<List<SupplierBidById>> GetSupplierBidById(CanvassDto CanvassDto);
Task<List<PerSupplier>> GetCanvassPerSupplier(CanvassDto CanvassDto); Task<PagedResult<PerSupplier>> GetCanvassPerSupplier(CanvassDto dto);
Task<PagedResult<ItemsForTagging>> GetItemsForTagging(CanvassDto dto);
Task<List<PRCanvassDetail>> GetCanvassPerSupplierEmail(CanvassDto CanvassDto); Task<List<PRCanvassDetail>> GetCanvassPerSupplierEmail(CanvassDto CanvassDto);
Task<List<PRCanvassDetail>> GetCanvassPerSupplierId(CanvassDto itemCodeDto); Task<List<PRCanvassDetail>> GetCanvassPerSupplierId(CanvassDto itemCodeDto);
Task<List<ItemListWOEmail>> GetItemSupplierWOEmail(CanvassDto CanvassDto); Task<List<ItemListWOEmail>> GetItemSupplierWOEmail(CanvassDto CanvassDto);
Task<List<SupplierResponse>> GetSupplierItemWOEmail(CanvassDto CanvassDto); Task<List<SupplierResponseDto>> GetSupplierItemWOEmail(CanvassDto CanvassDto);
Task<List<PRCanvassDetail>> GetCanvassByPRNo(CanvassDto CanvassDto); Task<List<PRCanvassDetail>> GetCanvassByPRNo(CanvassDto CanvassDto);
Task<List<CanvassGroupByPRNo>> GetCanvassGroupByPRNo(CanvassDto CanvassDto); Task<List<CanvassGroupByPRNo>> GetCanvassGroupByPRNo(CanvassDto CanvassDto);
Task<List<PRCanvassDetail>> GetCanvassByItemNo(CanvassDto CanvassDto); Task<List<PRCanvassDetail>> GetCanvassByItemNo(CanvassDto CanvassDto);
@ -51,7 +53,7 @@ namespace CPRNIMS.Domain.Contracts.Canvass
Task<List<ForCanvass>> GetForCanvassPerItem(CanvassDto CanvassDto); Task<List<ForCanvass>> GetForCanvassPerItem(CanvassDto CanvassDto);
Task<int> GetCanvassNo(); Task<int> GetCanvassNo();
Task<List<ForCanvassFollowUp>> GetCanvassForFollowUp(CanvassDto itemDto); Task<List<ForCanvassFollowUp>> GetCanvassForFollowUp(CanvassDto itemDto);
Task<List<SupplierResponse>> GetMySuppliers(CanvassDto CanvassDto); Task<List<SupplierResponseDto>> GetMySuppliers(CanvassDto CanvassDto);
Task<List<MyPRWOCanvass>> GetMyPRWOCanvass(CanvassDto itemDto); Task<List<MyPRWOCanvass>> GetMyPRWOCanvass(CanvassDto itemDto);
Task<List<AlternativeOfferDetails>> GetAlternativeOfferByPRDetailId(CanvassDto itemDto); Task<List<AlternativeOfferDetails>> GetAlternativeOfferByPRDetailId(CanvassDto itemDto);
Task<List<AllForCanvass>> GetAllForCanvass(); Task<List<AllForCanvass>> GetAllForCanvass();

View File

@ -1,5 +1,6 @@
using CPRNIMS.Infrastructure.Dto.Canvass.Request; using CPRNIMS.Infrastructure.Dto.Canvass.Request;
using CPRNIMS.Infrastructure.Dto.Canvass.Response; using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Entities.Purchasing; using CPRNIMS.Infrastructure.Entities.Purchasing;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -15,5 +16,6 @@ namespace CPRNIMS.Domain.Services.ICanvass
Task<Result<SupplierResponse>> PostSupplierAsync(SupplierRequest request, CancellationToken ct); Task<Result<SupplierResponse>> PostSupplierAsync(SupplierRequest request, CancellationToken ct);
Task SendRFQ(SupplierEmailRequest supplierEmailRequest); Task SendRFQ(SupplierEmailRequest supplierEmailRequest);
Task<bool> SearchingUpdate(long pRDetailsId); Task<bool> SearchingUpdate(long pRDetailsId);
Task<List<ForAISearchingTagging>> GetForAISearchingTagging();
} }
} }

View File

@ -5,8 +5,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoMapper;
using CPRNIMS.Infrastructure.ViewModel.Common;
using CPRNIMS.Infrastructure.Entities.Canvass; using CPRNIMS.Infrastructure.Entities.Canvass;
namespace CPRNIMS.Domain.Profile.Canvass namespace CPRNIMS.Domain.Profile.Canvass
@ -15,13 +13,10 @@ namespace CPRNIMS.Domain.Profile.Canvass
{ {
public SupplierRequestProfile() public SupplierRequestProfile()
{ {
// 1. THIS IS THE MISSING LINK: Request -> Entity
CreateMap<SupplierRequest, Suppliers>(); CreateMap<SupplierRequest, Suppliers>();
// 2. Entity -> Response
CreateMap<Suppliers, SupplierResponse>(); CreateMap<Suppliers, SupplierResponse>();
// 3. Response <-> Request (Use ReverseMap to handle both directions automatically)
CreateMap<SupplierResponse, SupplierRequest>().ReverseMap(); CreateMap<SupplierResponse, SupplierRequest>().ReverseMap();
} }
} }

View File

@ -1,10 +1,9 @@
using AutoMapper; using AutoMapper;
using Azure.Core;
using CPRNIMS.Domain.Services.ICanvass;
using CPRNIMS.Infrastructure.Database; using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Canvass; using CPRNIMS.Infrastructure.Dto.Canvass;
using CPRNIMS.Infrastructure.Dto.Canvass.Request; using CPRNIMS.Infrastructure.Dto.Canvass.Request;
using CPRNIMS.Infrastructure.Dto.Canvass.Response; using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Entities.Canvass; using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Entities.Purchasing; using CPRNIMS.Infrastructure.Entities.Purchasing;
using CPRNIMS.Infrastructure.Helper; using CPRNIMS.Infrastructure.Helper;
@ -49,14 +48,136 @@ namespace CPRNIMS.Domain.Services.Canvass
return allItems ?? new List<BiddingItem>(); return allItems ?? new List<BiddingItem>();
} }
public async Task<List<PerSupplier>> GetCanvassPerSupplier(CanvassDto CanvassDto) public async Task<PagedResult<ItemsForTagging>> GetItemsForTagging(CanvassDto dto)
{ {
var allItems = await _dbContext.PerSuppliers var parameters = new[]
.FromSqlRaw($"EXEC GetCanvassPerSupplier @UserId", {
new SqlParameter("@UserId", CanvassDto.UserId)) new SqlParameter("@UserId", dto.UserId),
.ToListAsync(); new SqlParameter("@SearchPRNo", dto.SearchPRNo ?? ""),
new SqlParameter("@SearchItemNo", dto.SearchItemNo ?? ""),
new SqlParameter("@SearchItemName", dto.SearchItemName ?? ""),
new SqlParameter("@SearchDepartment", dto.SearchDepartment ?? ""),
new SqlParameter("@PageNumber", dto.PageNumber),
new SqlParameter("@PageSize", dto.PageSize)
};
return allItems ?? new List<PerSupplier>(); int totalCount = 0;
var departmentList = new List<string>();
var items = new List<ItemsForTagging>();
var conn = _dbContext.Database.GetDbConnection();
await conn.OpenAsync();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"EXEC GetItemsForTagging @UserId,
@SearchPRNo,@SearchItemNo,@SearchItemName,@SearchDepartment,
@PageNumber, @PageSize";
foreach (var p in parameters) cmd.Parameters.Add(p);
cmd.CommandTimeout = 60;
using var reader = await cmd.ExecuteReaderAsync();
// Result set 1 — distinct supplier list
while (await reader.ReadAsync())
departmentList.Add(reader.GetString(0));
// Result set 2 — total count
await reader.NextResultAsync();
if (await reader.ReadAsync())
totalCount = reader.GetInt32(0);
// Result set 3 — paged rows
await reader.NextResultAsync();
while (await reader.ReadAsync())
{
items.Add(new ItemsForTagging
{
PRDetailsId = Convert.ToInt64(reader["PRDetailsId"]),
PRNo = Convert.ToInt64(reader["PRNo"]),
ItemNo = Convert.ToInt64(reader["ItemNo"]),
ItemName = reader["ItemName"]?.ToString(),
ItemDescription = reader["ItemDescription"]?.ToString(),
Department = reader["Department"]?.ToString(),
CreatedBy = reader["CreatedBy"]?.ToString(),
CreatedDate =Convert.ToDateTime(reader["CreatedDate"]),
DateNeeded = Convert.ToDateTime(reader["DateNeeded"]),
});
}
await conn.CloseAsync();
return new PagedResult<ItemsForTagging>
{
Data = items,
TotalCount = totalCount,
PageNumber = dto.PageNumber,
PageSize = dto.PageSize,
DepartmentList = departmentList
};
}
public async Task<PagedResult<PerSupplier>> GetCanvassPerSupplier(CanvassDto dto)
{
var parameters = new[]
{
new SqlParameter("@UserId", dto.UserId),
new SqlParameter("@SearchPRNo", dto.SearchPRNo ?? ""),
new SqlParameter("@SearchItemNo", dto.SearchItemNo ?? ""),
new SqlParameter("@SearchItemName", dto.SearchItemName ?? ""),
new SqlParameter("@SearchSupplier", dto.SearchSupplier ?? ""),
new SqlParameter("@PageNumber", dto.PageNumber),
new SqlParameter("@PageSize", dto.PageSize)
};
var supplierList = new List<string>();
int totalCount = 0;
var items = new List<PerSupplier>();
var conn = _dbContext.Database.GetDbConnection();
await conn.OpenAsync();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"EXEC GetCanvassPerSupplier @UserId,
@SearchPRNo,@SearchItemNo,@SearchItemName,@SearchSupplier,
@PageNumber, @PageSize";
foreach (var p in parameters) cmd.Parameters.Add(p);
cmd.CommandTimeout = 60;
using var reader = await cmd.ExecuteReaderAsync();
// Result set 1 — distinct supplier list
while (await reader.ReadAsync())
supplierList.Add(reader.GetString(0));
// Result set 2 — total count
await reader.NextResultAsync();
if (await reader.ReadAsync())
totalCount = reader.GetInt32(0);
// Result set 3 — paged rows
await reader.NextResultAsync();
while (await reader.ReadAsync())
{
items.Add(new PerSupplier
{
SupplierId = reader["SupplierId"] as int? ?? 0,
SupplierName = reader["SupplierName"]?.ToString(),
EmailAddress = reader["EmailAddress"]?.ToString(),
AggreItemName = reader["AggreItemName"]?.ToString(),
AggreItemNo = reader["AggreItemNo"]?.ToString(),
AggrePRNo = reader["AggrePRNo"]?.ToString(),
});
}
await conn.CloseAsync();
return new PagedResult<PerSupplier>
{
Data = items,
TotalCount = totalCount,
PageNumber = dto.PageNumber,
PageSize = dto.PageSize,
SupplierList = supplierList
};
} }
public async Task<List<RFQPerSupplier>> GetSupplierBidByItem(CanvassDto CanvassDto) public async Task<List<RFQPerSupplier>> GetSupplierBidByItem(CanvassDto CanvassDto)
{ {
@ -113,27 +234,24 @@ namespace CPRNIMS.Domain.Services.Canvass
return allItems ?? new List<CanvassGroupByPRNo>(); return allItems ?? new List<CanvassGroupByPRNo>();
} }
public async Task<List<SupplierResponse>> GetSupplierItemWOEmail(CanvassDto canvassDto) public async Task<List<SupplierResponseDto>> GetSupplierItemWOEmail(CanvassDto canvassDto)
{ {
// 1. Use Interpolated string to prevent SQL injection and pass the parameter safely
var suppliers = await _dbContext.Suppliers var suppliers = await _dbContext.SupplierResponses
.FromSqlInterpolated($"EXEC GetSupplierItemWOEmail @ItemNo = {canvassDto.ItemNo}") .FromSqlInterpolated($"EXEC GetSupplierItemWOEmail @ItemNo = {canvassDto.ItemNo}")
.ToListAsync(); .ToListAsync();
// 2. Map the List of entities to a List of Response objects return suppliers ?? new List<SupplierResponseDto>();
// AutoMapper handles collections automatically if the types are configured
return _mapper.Map<List<SupplierResponse>>(suppliers);
} }
public async Task<List<SupplierResponse>> GetSupplierById(CanvassDto CanvassDto) public async Task<List<SupplierResponseDto>> GetSupplierById(CanvassDto CanvassDto)
{ {
var item = await _dbContext.Suppliers var items = await _dbContext.SupplierResponses
.FromSqlRaw($"EXEC GetSupplierById @SupplierId", .FromSqlRaw($"EXEC GetSupplierById @SupplierId",
new SqlParameter("@SupplierId", CanvassDto.SupplierId)) new SqlParameter("@SupplierId", CanvassDto.SupplierId))
.ToListAsync(); .ToListAsync();
// 2. Map the List of entities to a List of Response objects
// AutoMapper handles collections automatically if the types are configured return items ?? new List<SupplierResponseDto>();
return _mapper.Map<List<SupplierResponse>>(item);
} }
public async Task<List<RFQReference>> GetRFQ(CanvassDto CanvassDto) public async Task<List<RFQReference>> GetRFQ(CanvassDto CanvassDto)
{ {
@ -192,6 +310,15 @@ namespace CPRNIMS.Domain.Services.Canvass
return 0; return 0;
} }
} }
public async Task<List<ForAISearchingTagging>> GetForAISearchingTagging()
{
var allItems = await _dbContext.ForAISearchingTaggings
.AsNoTracking()
.Take(10)
.ToListAsync();
return allItems ?? new List<ForAISearchingTagging>();
}
public async Task<List<ItemWithoutSupplier>> GetItemWithoutSupplier() public async Task<List<ItemWithoutSupplier>> GetItemWithoutSupplier()
{ {
var allItems = await _dbContext.ItemWithoutSuppliers var allItems = await _dbContext.ItemWithoutSuppliers
@ -230,15 +357,15 @@ namespace CPRNIMS.Domain.Services.Canvass
return allItems ?? new List<SupplierBidById>(); return allItems ?? new List<SupplierBidById>();
} }
public async Task<List<SupplierResponse>> GetMySuppliers(CanvassDto CanvassDto) public async Task<List<SupplierResponseDto>> GetMySuppliers(CanvassDto CanvassDto)
{ {
var items = await _dbContext.Suppliers var items = await _dbContext.SupplierResponses
.FromSqlRaw($"EXEC GetMySuppliers @UserId,@SupplierId", .FromSqlRaw($"EXEC GetMySuppliers @UserId,@SupplierId",
new SqlParameter("@UserId", CanvassDto.UserId), new SqlParameter("@UserId", CanvassDto.UserId),
new SqlParameter("@SupplierId", CanvassDto.SupplierId)) new SqlParameter("@SupplierId", CanvassDto.SupplierId))
.ToListAsync(); .ToListAsync();
return _mapper.Map<List<SupplierResponse>>(items); return items ?? new List<SupplierResponseDto>();
} }
public async Task<List<MyPRWOCanvass>> GetMyPRWOCanvass(CanvassDto itemDto) public async Task<List<MyPRWOCanvass>> GetMyPRWOCanvass(CanvassDto itemDto)
{ {

View File

@ -27,10 +27,14 @@ namespace CPRNIMS.Domain.Services.Canvass
} }
public async Task<List<SupplierResponse>> SearchAndFilterSuppliersAsync( public async Task<List<SupplierResponse>> SearchAndFilterSuppliersAsync(
string itemName, string itemDescription) string itemName, string itemDescription, bool isInternational)
{ {
string locality = "Philippines";
if (isInternational) { locality = "all over the asia including Philippines"; }
else { locality = "Philippines"; }
// Step 1: Tavily — get supplier URLs // Step 1: Tavily — get supplier URLs
var (searchContent, supplierUrls) = await SearchTavilyAsync(itemName, itemDescription); var (searchContent, supplierUrls) = await SearchTavilyAsync(itemName, itemDescription, locality);
// Step 2: Fetch contact pages from discovered URLs // Step 2: Fetch contact pages from discovered URLs
var contactContent = await FetchContactPagesAsync(supplierUrls); var contactContent = await FetchContactPagesAsync(supplierUrls);
@ -42,11 +46,11 @@ namespace CPRNIMS.Domain.Services.Canvass
return suppliers; return suppliers;
} }
// ── Tavily ────────────────────────────────────────────────────────────── // ── Tavily ──
private async Task<(string content, List<string> urls)> SearchTavilyAsync( private async Task<(string content, List<string> urls)> SearchTavilyAsync(
string itemName, string itemDescription) string itemName, string itemDescription,string locality)
{ {
var query = $"{itemName} {itemDescription} suppliers Philippines budget price contact email phone"; var query = $"{itemName} {itemDescription} suppliers {locality} budget price contact email phone";
var payload = new var payload = new
{ {
@ -97,7 +101,7 @@ namespace CPRNIMS.Domain.Services.Canvass
return (fullText, urls); return (fullText, urls);
} }
// ── Fetch Contact Pages ────────────────────────────────────────────────── // ── Fetch Contact Pages ───
private async Task<string> FetchContactPagesAsync(List<string> baseUrls) private async Task<string> FetchContactPagesAsync(List<string> baseUrls)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Models.Account; using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Models.Account;
using CPRNIMS.Infrastructure.ViewModel.Canvass; using CPRNIMS.Infrastructure.ViewModel.Canvass;
using CPRNIMS.Infrastructure.ViewModel.PR; using CPRNIMS.Infrastructure.ViewModel.PR;
using System; using System;
@ -11,10 +12,10 @@ namespace CPRNIMS.Domain.UIContracts.Canvass
{ {
public interface ICanvass public interface ICanvass
{ {
#region
Task<List<CanvassVM>> GetSupplierBid(User user, CanvassVM viewModel); Task<List<CanvassVM>> GetSupplierBid(User user, CanvassVM viewModel);
Task<List<CanvassVM>> GetSupplierBidByItem(User user, CanvassVM viewModel); Task<List<CanvassVM>> GetSupplierBidByItem(User user, CanvassVM viewModel);
Task<List<CanvassVM>> GetSupplierBidById(User user, CanvassVM viewModel); Task<List<CanvassVM>> GetSupplierBidById(User user, CanvassVM viewModel);
Task<List<CanvassVM>> GetCanvassPerSupplier(User user, CanvassVM viewModel);
Task<List<CanvassVM>> GetCanvassPerSupplierEmail(User user, CanvassVM viewModel); Task<List<CanvassVM>> GetCanvassPerSupplierEmail(User user, CanvassVM viewModel);
Task<List<CanvassVM>> GetItemSupplierWOEmail(User user, CanvassVM viewModel); Task<List<CanvassVM>> GetItemSupplierWOEmail(User user, CanvassVM viewModel);
Task<List<CanvassVM>> GetSupplierItemWOEmail(User user, CanvassVM viewModel); Task<List<CanvassVM>> GetSupplierItemWOEmail(User user, CanvassVM viewModel);
@ -32,6 +33,12 @@ namespace CPRNIMS.Domain.UIContracts.Canvass
Task<List<CanvassVM>?> GetCanvassPerSupplierId(User user, CanvassVM viewModel); Task<List<CanvassVM>?> GetCanvassPerSupplierId(User user, CanvassVM viewModel);
Task<List<CanvassVM>?> GetCanvassGroupByPRNo(User user, CanvassVM viewModel); Task<List<CanvassVM>?> GetCanvassGroupByPRNo(User user, CanvassVM viewModel);
Task<List<CanvassVM>?> GetAlternativeOfferByPRDetailId(User user, CanvassVM viewModels); Task<List<CanvassVM>?> GetAlternativeOfferByPRDetailId(User user, CanvassVM viewModels);
Task<PagedResult<CanvassVM>> GetCanvassPerSupplier(User user, CanvassVM viewModel);
Task<PagedResult<CanvassVM>> GetItemsForTagging(User user, CanvassVM dto);
#endregion
#region Post Put
Task<CanvassVM> PostCanvass(User user, CanvassVM viewModel); Task<CanvassVM> PostCanvass(User user, CanvassVM viewModel);
Task<CanvassVM> PostPutSupplier(User user, CanvassVM viewModel); Task<CanvassVM> PostPutSupplier(User user, CanvassVM viewModel);
Task<CanvassVM> PostTaggingSupplier(User user, CanvassVM viewModel); Task<CanvassVM> PostTaggingSupplier(User user, CanvassVM viewModel);
@ -42,5 +49,6 @@ namespace CPRNIMS.Domain.UIContracts.Canvass
Task<CanvassVM> PostPutMySupplier(User user, CanvassVM viewModel); Task<CanvassVM> PostPutMySupplier(User user, CanvassVM viewModel);
Task<CanvassVM> PostPutItemTagging(User user, CanvassVM viewModel); Task<CanvassVM> PostPutItemTagging(User user, CanvassVM viewModel);
Task<CanvassVM> UnlockFormLink(User user, CanvassVM viewModel); Task<CanvassVM> UnlockFormLink(User user, CanvassVM viewModel);
#endregion
} }
} }

View File

@ -1,5 +1,6 @@
using CPRNIMS.Domain.UIContracts.Canvass; using CPRNIMS.Domain.UIContracts.Canvass;
using CPRNIMS.Domain.UIContracts.Common; using CPRNIMS.Domain.UIContracts.Common;
using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Helper; using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.Models.Account; using CPRNIMS.Infrastructure.Models.Account;
using CPRNIMS.Infrastructure.Models.Common; using CPRNIMS.Infrastructure.Models.Common;
@ -123,6 +124,54 @@ namespace CPRNIMS.Domain.UIServices.Canvass
throw; throw;
} }
} }
public async Task<PagedResult<CanvassVM>> SendGetPageListApiRequest
(User user, CanvassVM viewModel, string apiEndpoint)
{
var token = await _tokenHelper.GetValidTokenAsync();
try
{
if (string.IsNullOrEmpty(token))
{
return null;
}
viewModel.UserId = user.UserId;
var jsonContent = JsonSerializer.Serialize(viewModel);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
using (var httpClient = _apiConfigurationService.CreateHttpClientWithDefaultHeaders(token))
{
HttpResponseMessage response;
response = await httpClient.PostAsync(apiEndpoint, content);
var jsonResponse = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
viewModel.messCode = 1;
var result = JsonSerializer.Deserialize<PagedResult<CanvassVM>>(jsonResponse, options);
return result;
}
else
{
var result = JsonSerializer.Deserialize<PagedResult<CanvassVM>>(jsonResponse);
viewModel.messCode = 0;
viewModel.errMessage = "Bad request";
return result;
}
}
}
catch (Exception ex)
{
throw;
}
}
#endregion #endregion
#region Get #region Get
@ -135,12 +184,7 @@ namespace CPRNIMS.Domain.UIServices.Canvass
{ {
return await SendGetApiRequest(user, viewModel, return await SendGetApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:GetSupplierBidByItem"]); _configuration["LLI:NonInvent:CanvassMgmt:GetSupplierBidByItem"]);
} }
public async Task<List<CanvassVM>> GetCanvassPerSupplier(User user, CanvassVM viewModel)
{
return await SendGetApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:GetCanvassPerSupplier"]);
}
public async Task<List<CanvassVM>> GetCanvassPerSupplierEmail(User user, CanvassVM viewModel) public async Task<List<CanvassVM>> GetCanvassPerSupplierEmail(User user, CanvassVM viewModel)
{ {
return await SendGetApiRequest(user, viewModel, return await SendGetApiRequest(user, viewModel,
@ -236,6 +280,16 @@ namespace CPRNIMS.Domain.UIServices.Canvass
return await SendGetApiRequest(user, viewModel, return await SendGetApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:GetAlternativeOfferByPRDetailId"]); _configuration["LLI:NonInvent:CanvassMgmt:GetAlternativeOfferByPRDetailId"]);
} }
public async Task<PagedResult<CanvassVM>> GetCanvassPerSupplier(User user, CanvassVM viewModel)
{
return await SendGetPageListApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:GetCanvassPerSupplier"]);
}
public async Task<PagedResult<CanvassVM>> GetItemsForTagging(User user, CanvassVM viewModel)
{
return await SendGetPageListApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:GetItemsForTagging"]);
}
#endregion #endregion
#region Post Put #region Post Put
public async Task<CanvassVM> PostCanvass(User user, CanvassVM viewModel) public async Task<CanvassVM> PostCanvass(User user, CanvassVM viewModel)
@ -288,6 +342,7 @@ namespace CPRNIMS.Domain.UIServices.Canvass
return await SendPostApiRequest(user, viewModel, return await SendPostApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:UnlockFormLink"]); _configuration["LLI:NonInvent:CanvassMgmt:UnlockFormLink"]);
} }
#endregion #endregion
} }
} }

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Entities.Account; using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Entities.Account;
using CPRNIMS.Infrastructure.Entities.Canvass; using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Entities.Common; using CPRNIMS.Infrastructure.Entities.Common;
using CPRNIMS.Infrastructure.Entities.Finance; using CPRNIMS.Infrastructure.Entities.Finance;
@ -65,7 +66,9 @@ namespace CPRNIMS.Infrastructure.Database
public virtual DbSet<ForRR> ForRRs { get; set; } public virtual DbSet<ForRR> ForRRs { get; set; }
public virtual DbSet<RR> RRs { get; set; } public virtual DbSet<RR> RRs { get; set; }
public virtual DbSet<Canvass> Canvasses { get; set; } public virtual DbSet<Canvass> Canvasses { get; set; }
public DbSet<SupplierResponseDto> SupplierResponses { get; set; }
public DbSet<SupplierItems> SupplierItems { get; set; } public DbSet<SupplierItems> SupplierItems { get; set; }
public DbSet<ItemsForTagging> ItemsForTaggings { get; set; }
public virtual DbSet<ForCanvassFollowUp> ForCanvassFollowUps { get; set; } public virtual DbSet<ForCanvassFollowUp> ForCanvassFollowUps { get; set; }
public virtual DbSet<WOResponse> WOResponses { get; set; } public virtual DbSet<WOResponse> WOResponses { get; set; }
public virtual DbSet<WOResponseById> WOResponseByIds { get; set; } public virtual DbSet<WOResponseById> WOResponseByIds { get; set; }
@ -80,6 +83,7 @@ namespace CPRNIMS.Infrastructure.Database
public virtual DbSet<RFQReference> RFQReferences { get; set; } public virtual DbSet<RFQReference> RFQReferences { get; set; }
public virtual DbSet<CanvassDetail> CanvassDetails { get; set; } public virtual DbSet<CanvassDetail> CanvassDetails { get; set; }
public virtual DbSet<PRCanvassDetail> PRCanvassDetails { get; set; } public virtual DbSet<PRCanvassDetail> PRCanvassDetails { get; set; }
public DbSet<ForAISearchingTagging> ForAISearchingTaggings { get; set; }
public virtual DbSet<CanvassGroupByPRNo> CanvassGroupByPRNos { get; set; } public virtual DbSet<CanvassGroupByPRNo> CanvassGroupByPRNos { get; set; }
public virtual DbSet<ForCanvass> ForCanvasses { get; set; } public virtual DbSet<ForCanvass> ForCanvasses { get; set; }
public virtual DbSet<ForPO> ForPOs { get; set; } public virtual DbSet<ForPO> ForPOs { get; set; }

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Entities.Common; using CPRNIMS.Infrastructure.Dto.Canvass.Request;
using CPRNIMS.Infrastructure.Entities.Common;
using CPRNIMS.Infrastructure.ViewModel.Items; using CPRNIMS.Infrastructure.ViewModel.Items;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -9,7 +10,7 @@ using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Canvass namespace CPRNIMS.Infrastructure.Dto.Canvass
{ {
public class CanvassDto : CommonProperties public class CanvassDto : CanvassRequestSearch
{ {
public byte PaymentTermsId { get; set; } = 0; public byte PaymentTermsId { get; set; } = 0;
public bool IsArchived { get; set; } = false; public bool IsArchived { get; set; } = false;

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CPRNIMS.Infrastructure.Dto.Common;
namespace CPRNIMS.Infrastructure.Dto.Canvass.Request
{
public class CanvassRequestSearch : PagedRequest
{
public string SearchPRNo { get; set; } = string.Empty;
public string SearchItemName { get; set; } = string.Empty;
public string SearchItemNo { get; set; } = string.Empty;
public string SearchCreatedBy { get; set; } = string.Empty;
public string SearchSupplier { get; set; } = string.Empty;
public string SearchDepartment { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Canvass.Response
{
public class SupplierResponseDto
{
[Key]
public int SupplierId { get; set; }
public string? SupplierName { get; set; }
public string? EmailAddress { get; set; }
public string? ContactNo { get; set; }
public string? ContactPerson { get; set; }
public string? LeadTime { get; set; }
public bool VatInc { get; set; }
public string? Currency { get; set; }
public string? PaymentTerms { get; set; }
public byte PaymentTermsId { get; set; }
public byte CurrencyId { get; set; }
public string? TinNo { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Common
{
public class PagedRequest
{
public int PageNumber { get; set; }
public int PageSize { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.Common
{
public class PagedResult<T>
{
public List<T> Data { get; set; } = new List<T>();
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public List<string> ClientList { get; set; } = new List<string>();
public List<string> SupplierList { get; set; } = new List<string>();
public List<string> CategoryList { get; set; } = new List<string>();
public List<string> DepartmentList { get; set; } = new List<string>();
}
}

View File

@ -0,0 +1,27 @@
using CPRNIMS.Infrastructure.Entities.Purchasing;
using System;
using System.Collections.Generic;
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.Canvass
{
[Table("ForAISearchingTagging")]
public class ForAISearchingTagging
{
[Key]
public Guid Id { get; set; }
public long PRDetailsId { get; set; }
public long PRNo { get; set; }
public long ItemNo { get; set; }
public bool IsInternational { get; set; }
public string ItemName { get; set; }=string.Empty;
public string ItemDescription { get; set; } =string.Empty;
public string FullName { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Entities.Canvass
{
public class ItemsForTagging
{
[Key]
public long PRDetailsId { get; set; }
public long PRNo { get; set; }
public long ItemNo { get; set; }
public string? ItemName { get; set; }
public string? ItemDescription { get; set; }
public string? Department { get; set; }
public DateTime DateNeeded { get; set; }
public DateTime CreatedDate { get; set; }
public string? CreatedBy { get; set; }
}
}

View File

@ -13,9 +13,6 @@ namespace CPRNIMS.Infrastructure.Entities.Canvass
public int SupplierId { get; set; } public int SupplierId { get; set; }
public string? SupplierName { get; set; } public string? SupplierName { get; set; }
public string? EmailAddress { get; set; } public string? EmailAddress { get; set; }
public bool IsActive { get; set; }
public string? ContactNo { get; set;}
public string? ContactPerson { get; set; }
public string? Address { get; set;} public string? Address { get; set;}
public string? AggreItemName { get; set; } public string? AggreItemName { get; set; }
public string? AggreItemNo { get; set; } public string? AggreItemNo { get; set; }

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Entities.Common; using CPRNIMS.Infrastructure.Dto.Canvass.Request;
using CPRNIMS.Infrastructure.Entities.Common;
using CPRNIMS.Infrastructure.ViewModel.Items; using CPRNIMS.Infrastructure.ViewModel.Items;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -8,8 +9,12 @@ using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.ViewModel.Canvass namespace CPRNIMS.Infrastructure.ViewModel.Canvass
{ {
public class CanvassVM : CommonProperties public class CanvassVM : CanvassRequestSearch
{ {
public string? CreatedBy { get; set; }
public string? UpdatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }
public long ItemCodeId { get; set; } public long ItemCodeId { get; set; }
public long PRDetailsId { get; set; } public long PRDetailsId { get; set; }
public long CanvassId { get; set; } public long CanvassId { get; set; }

View File

@ -72,7 +72,7 @@ namespace CPRNIMS.WebApi.Controllers.Account
company = user.Company, company = user.Company,
success = true, success = true,
messCode = 1, messCode = 1,
message = "Yehey!" message = "Success"
}); });
} }

View File

@ -99,6 +99,14 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
nameof(GetCanvassPerSupplier), false nameof(GetCanvassPerSupplier), false
); );
} }
[HttpPost("GetItemsForTagging")]
public async Task<IActionResult> GetItemsForTagging(CanvassDto itemCodeDto)
{
return await ExecuteWithErrorHandling(
() => _canvass.GetItemsForTagging(itemCodeDto),
nameof(GetItemsForTagging), false
);
}
[HttpPost("GetCanvassPerSupplierId")] [HttpPost("GetCanvassPerSupplierId")]
public async Task<IActionResult> GetCanvassPerSupplierId(CanvassDto itemCodeDto) public async Task<IActionResult> GetCanvassPerSupplierId(CanvassDto itemCodeDto)
{ {
@ -454,16 +462,16 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
public async Task<IActionResult> PostSearchSupplierAndSend(CancellationToken ct) public async Task<IActionResult> PostSearchSupplierAndSend(CancellationToken ct)
{ {
// #1 Get top 10 items without suppliers — must process all // #1 Get top 10 items without suppliers — must process all
var response = await _canvass.GetItemWithoutSupplier(); var response = await _canvass.GetForAISearchingTagging();
if (response == null || !response.Any()) return BadRequest("No items found."); if (response == null || !response.Any()) return BadRequest("No items found.");
var supplierResults = new List<object>(); var supplierResults = new List<object>();
foreach (var item in response) // ✅ FIX #1: use loop variable, not response[0] foreach (var item in response)
{ {
// #2 Search Tavily + Filter with Groq // #2 Search Tavily + Filter with Groq
var suppliers = await _supplierSearchService var suppliers = await _supplierSearchService
.SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription); .SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription, item.IsInternational);
if (!suppliers.Any()) if (!suppliers.Any())
{ {
@ -537,107 +545,6 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
suppliers = supplierResults suppliers = supplierResults
}); });
} }
/* [HttpPost("PostSearchSupplierAndSend")]
public async Task<IActionResult> PostSearchSupplierAndSend(CancellationToken ct)
{
// #1 Get top 10 item without suppliers must be process all
var response = await _canvass.GetItemWithoutSupplier();
if (response == null || !response.Any()) return BadRequest("No items found.");
var supplierResults = new List<object>();
foreach (var supplierList in response)
{
var item = response[0];
// #2 Search Tavily + Filter with Groq
var suppliers = await _supplierSearchService
.SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription);
if (!suppliers.Any())
{
await _canvass.SearchingUpdate(supplierList.PRDetailsId);
continue;
}
var results = new List<object>();
// #3 & #4 Loop each found supplier
foreach (var supplier in suppliers)
{
int canvassNo = await _canvass.GetCanvassNo();
// Map SupplierResponse → SupplierRequest before saving
var supplierRequest = _mapper.Map<SupplierRequest>(supplier);
supplierRequest.ItemNo=item.ItemNo;
var result = await _canvass.PostSupplierAsync(supplierRequest, ct);
if (result?.Value == null) continue;
var canvassDto = new CanvassDto
{
PRDetailsId = item.PRDetailsId,
PRNo = item.PRNo,
ItemNo = item.ItemNo,
SupplierId = result.Value.SupplierId,
UserId = item.UserId,
FullName = item.FullName,
CanvassNo = ++canvassNo,
};
// Generate token for supplier form link
await _canvass.PostPerSupplierToken(canvassDto);
// Get RFQ details
var rfq = await _canvass.GetRFQ(canvassDto);
if (rfq == null || !rfq.Any()) continue;
// Send RFQ email
var email = rfq[0].EmailAddress;
var supplierEmailRequest = new SupplierEmailRequest
{
AttachPath = GetRelativePath(@"Content\\Documents\\Pdf\\Offer_Submission_Procedure.pdf"),
Recipient = "rmsoriano@lloydlab.com",//rfq[0].EmailAddress, // this will be implemented later
Subject = $"CLMS - Request For Quotation #PRNo: {rfq[0].AggrePRNo}",
CC = Convert.ToString(_configuration["Canvass:CC"] ?? ""),
SenderEmail = _config["SMTP:SenderEmail"],
DisplayName = "lloydlabinc.com",
Password = _config["SMTP:Password"],
OutGoingPort = 587,
Server = _config["SMTP:Server"],
UserName = _config["SMTP:UserName"],
FormLink= Convert.ToString(_configuration["WebEndPoint:SupplierForm"] ?? ""),
Token = rfq[0].Token,
SupplierName= result.Value.SupplierName,
Purchaser= item.FullName,
IsSuccess = false,
IsCanvass = true,
};
await _canvass.SearchingUpdate(supplierList.PRDetailsId);
await _canvass.SendRFQ(supplierEmailRequest);
results.Add(new
{
supplier = supplier.SupplierName,
email = supplier.EmailAddress,
canvassNo = canvassDto.CanvassNo
});
}
return Ok(new
{
item = item.ItemName,
processed = results.Count,
suppliers = results
});
}
return Ok();
}*/
[HttpPost("PostSuggestedSupp")] [HttpPost("PostSuggestedSupp")]
public async Task<IActionResult> PostSuggestedSupp(CanvassDto CanvassDto) public async Task<IActionResult> PostSuggestedSupp(CanvassDto CanvassDto)

View File

@ -2,12 +2,9 @@
using CPRNIMS.Domain.Contracts.Receiving; using CPRNIMS.Domain.Contracts.Receiving;
using CPRNIMS.Domain.Services; using CPRNIMS.Domain.Services;
using CPRNIMS.Infrastructure.Dto.Items; using CPRNIMS.Infrastructure.Dto.Items;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.ViewModel.Receiving; using CPRNIMS.Infrastructure.ViewModel.Receiving;
using CPRNIMS.WebApi.Controllers.Base; using CPRNIMS.WebApi.Controllers.Base;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Runtime.InteropServices;
using System.Text;
namespace CPRNIMS.WebApi.Controllers.Receiving namespace CPRNIMS.WebApi.Controllers.Receiving
{ {

View File

@ -1,4 +1,16 @@
CREATE TABLE ProjectCodes( CREATE TABLE ForAISearchingTagging(
Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
PRDetailsId BIGINT NOT NULL,
PRNo BIGINT NOT NULL,
ItemNo BIGINT NOT NULL,
ItemName VARCHAR(100),
IsInternational BIT DEFAULT(0),
ItemDescription VARCHAR(500),
FullName VARCHAR(500) NOT NULL,
UserId VARCHAR(450) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE()
);
CREATE TABLE ProjectCodes(
ProjectCodeId INT PRIMARY KEY IDENTITY(1,1), ProjectCodeId INT PRIMARY KEY IDENTITY(1,1),
ProjectCode VARCHAR(50) NOT NULL, ProjectCode VARCHAR(50) NOT NULL,
ProjectName VARCHAR(200) NOT NULL, ProjectName VARCHAR(200) NOT NULL,

View File

@ -4,30 +4,47 @@
"ValidIssuer": "https://lloydwebapi.lloydlab.com:2021", "ValidIssuer": "https://lloydwebapi.lloydlab.com:2021",
"Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xasd707" "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xasd707"
}, },
"Tavily": {
"ApiKey": "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",
"SearchUrl": "https://api.tavily.com/search"
},
"Groq": {
"ApiKey": "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",
"ApiUrl": "https://api.groq.com/openai/v1/chat/completions",
"Model": "llama-3.1-8b-instant"
},
"WebEndPoint": { "WebEndPoint": {
"ForgotPassword": "https://llipurchasingnoninventory.com:8080/", "ForgotPassword": "https://llipurchasingnoninventory.com:8080/",
"SupplierForm": "https://llipurchasingnoninventory.com:8083/" "SupplierForm": "https://llipurchasingnoninventory.com:8083/"
}, },
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=194.233.78.55;Database=CPRNIMS;User Id=welladmin;Password=P@sSW0rd2024!!!;Encrypt=false;", "DefaultConnection": "Server=212.47.72.54;Database=CPRNIMS;User Id=LRMS26;Password=P@ssw0rd26;Encrypt=False;",
"LocalPurchConn": "Server=DESKTOP-RQGHQQN;Initial Catalog=PurchasingSystem;Integrated Security=SSPI;TrustServerCertificate=True;" "LocalPurchConn": "Server=DESKTOP-RQGHQQN;Database=PurchasingSystem;User Id=LRMS25;Password=P@ssw0rd26;Encrypt=False;"
//"LocalPurchConn": "Server=172.16.19.238;Database=PurchasingSystem;User Id=lli-mdld;Password=LLi-88Kk5&/mgH]m;Encrypt=False;"
}, },
"SMTP": { "SMTP": {
"DisplayName": "no-reply-cwms@lloydlab.com", "DisplayName": "no-reply-cwms@lloydlab.com",
"ToReceiver": "NA", "ToReceiver": "NA",
"OutgoingPort": "587", "OutgoingPort": "587",
"IncomingPort": "110", "IncomingPort": "110",
"SenderEmail": "cwms@lloydlab.com", "SenderEmail": "lli.mis2025@gmail.com",
"Password": "LLi-H@^{3bp14>4*", "Password": "vcwq nesk rsqb zxbf",
"Server": "mail.lloydlab.com", "Server": "smtp.gmail.com",
"SenderName": "CWMS", "SenderName": "CWMS",
"UserName": "cwms@lloydlab.com", "UserName": "lli.mis2025@gmail.com",
"CC": "rmsoriano@lloydlab.com;", "CC": "rmsoriano@lloydlab.com;",
"PurchasingCC": ";rmsoriano@lloydlab.com;iigajasan@lloydlab.com;laoreo@lloydlab.com;avaustria@lloydlab.com;hdbautista@lloydlab.com;pur_canvasser2@lloydlab.com;pur_canvasser4@lloydlab.com;purchasing@lloydlab.com;jmmaulawin@lloydlab.com" "PurchasingCC": ";rmsoriano@lloydlab.com;iigajasan@lloydlab.com;laoreo@lloydlab.com;avaustria@lloydlab.com;hdbautista@lloydlab.com;pur_canvasser2@lloydlab.com;pur_canvasser4@lloydlab.com;purchasing@lloydlab.com;jmmaulawin@lloydlab.com"
}, },
"Canvass": { "Canvass": {
"CC": "iigajasan@lloydlab.com;rmsoriano@lloydlab.com;laoreo@lloydlab.com;avaustria@lloydlab.com;hdbautista@lloydlab.com;pur_canvasser2@lloydlab.com;pur_canvasser4@lloydlab.com;purchasing@lloydlab.com;jmmaulawin@lloydlab.com" "CC": "iigajasan@lloydlab.com;rmsoriano@lloydlab.com;laoreo@lloydlab.com;avaustria@lloydlab.com;hdbautista@lloydlab.com;pur_canvasser2@lloydlab.com;pur_canvasser4@lloydlab.com;purchasing@lloydlab.com;jmmaulawin@lloydlab.com",
"EmailSettings": {
"ExcludedEmails": [
"jmcariaga@lloydlab.com",
"projctrl@lloydlab.com",
"test@test.com",
"admin@internal.com"
]
}
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View File

@ -59,7 +59,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Common\Helper\" /> <Folder Include="Common\Helper\" />
<Folder Include="Properties\NewFolder\" /> <Folder Include="Properties\NewFolder\" />
<Folder Include="Views\Components\CanvassMgmt\" />
<Folder Include="wwwroot\Content\Uploads\PRAttachment\" /> <Folder Include="wwwroot\Content\Uploads\PRAttachment\" />
</ItemGroup> </ItemGroup>

View File

@ -4,6 +4,7 @@ using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.ViewModel.Canvass; using CPRNIMS.Infrastructure.ViewModel.Canvass;
using CPRNIMS.WebApps.Controllers.Base; using CPRNIMS.WebApps.Controllers.Base;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using CPRNIMS.Infrastructure.Dto.Common;
namespace CPRNIMS.WebApps.Controllers.Canvass namespace CPRNIMS.WebApps.Controllers.Canvass
{ {
@ -201,11 +202,55 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
response = await _canvass.GetSupplierItemWOEmail(GetUser(), viewModels); response = await _canvass.GetSupplierItemWOEmail(GetUser(), viewModels);
return GetResponse(response); return GetResponse(response);
} }
public async Task<IActionResult> GetCanvassPerSupplier() public async Task<IActionResult> GetCanvassPerSupplier(string searchPRNo = "", string searchItemNo = "",
string searchSupplier = "", string searchItemName = "", int pageNumber = 1, int pageSize = 10)
{ {
var viewModels = new CanvassVM(); var dto = new CanvassVM
response = await _canvass.GetCanvassPerSupplier(GetUser(), viewModels); {
return GetResponse(response); SearchPRNo = searchPRNo,
SearchItemName = searchItemName,
SearchItemNo = searchItemNo,
SearchSupplier = searchSupplier,
PageNumber = pageNumber,
PageSize = pageSize
};
var result = await _canvass.GetCanvassPerSupplier(GetUser(), dto);
int draw = int.TryParse(Request.Query["draw"], out int d) ? d : 1;
return Json(new
{
draw = draw,
recordsTotal = result.TotalCount,
recordsFiltered = result.TotalCount,
data = result.Data,
supplierList = result.SupplierList
});
}
public async Task<IActionResult> GetItemsForTagging(string searchPRNo = "", string searchItemNo = "",
string searchItemName = "",string searchDepartment="", int pageNumber = 1, int pageSize = 10)
{
var dto = new CanvassVM
{
SearchPRNo = searchPRNo,
SearchItemName = searchItemName,
SearchItemNo = searchItemNo,
SearchDepartment = searchDepartment,
PageNumber = pageNumber,
PageSize = pageSize
};
var result = await _canvass.GetItemsForTagging(GetUser(), dto);
int draw = int.TryParse(Request.Query["draw"], out int d) ? d : 1;
return Json(new
{
draw = draw,
recordsTotal = result.TotalCount,
recordsFiltered = result.TotalCount,
data = result.Data,
departmentList = result.DepartmentList
});
} }
public async Task<IActionResult> GetCanvassPerSupplierEmail(CanvassVM viewModel) public async Task<IActionResult> GetCanvassPerSupplierEmail(CanvassVM viewModel)
{ {
@ -306,6 +351,10 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
} }
#endregion #endregion
#region Views #region Views
public IActionResult GetCanvassingTabPage(int id)
{
return ViewComponent("CanvassingTabPage", new { canvassingTabPageId = id });
}
public async Task<IActionResult> Suppliers() public async Task<IActionResult> Suppliers()
{ {
return await IsAuthenTicated(); return await IsAuthenTicated();

View File

@ -10,8 +10,8 @@ using CPRNIMS.WebApps.Models;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.JsonWebTokens;
using System.Diagnostics; using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Web; using System.Web;
@ -156,8 +156,8 @@ namespace CPRNIMS.WebApps.Controllers
DateTime expirationTime = DateTime.UtcNow.AddHours(2); DateTime expirationTime = DateTime.UtcNow.AddHours(2);
var handler = new JwtSecurityTokenHandler(); var handler = new JsonWebTokenHandler();
var jwtToken = handler.ReadJwtToken(login.token); var jwtToken = handler.ReadJsonWebToken(login.token);
var claims = new List<Claim> var claims = new List<Claim>
{ {

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
namespace CPRNIMS.WebApps.ViewComponents.Canvass
{
public class CanvassingTabPageViewComponent : ViewComponent
{
public IViewComponentResult Invoke(int canvassingTabPageId)
{
string viewName = canvassingTabPageId switch
{
1 => "~/Views/Components/CanvassMgmt/CanvassingTabPage/ForTagging.cshtml",
2 => "~/Views/Components/CanvassMgmt/CanvassingTabPage/ForCanvass.cshtml",
3 => "~/Views/Components/CanvassMgmt/CanvassingTabPage/ForApproval.cshtml",
_ => "~/Views/Components/CanvassMgmt/CanvassingTabPage/Completed.cshtml"
};
return View(viewName);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,115 @@
<body> @await Html.PartialAsync("PagesView/Canvass/_CanvassStyles")
<div class="container-fluid"> @await Html.PartialAsync("PagesView/Canvass/_CanvassHelpers")
<div class="table-container shadow-lg p-3 mb-3 bg-white rounded">
<div class="header-container"> <div class="canvass-wrapper">
<h2>For Canvass List Per Supplier</h2>
@* {{-- HEADER --}} *@
<div class="cv-header">
<div class="cv-header-inner">
<i class="fas fa-file-invoice cv-header-icon"></i>
<div>
<h1>Canvass Management</h1>
<p>Manage purchase canvass requests by supplier, status, and comparison</p>
</div> </div>
<br />
<table id="PRTable" class="row-border" cellspacing="0" width="100%" style="table-layout:fixed;">
<colgroup>
<col style="width:18% !important">
<col style="width:20% !important;">
<col style="width:9% !important">
<col style="width:9% !important">
<col style="width:35% !important">
<col style="width:9% !important">
</colgroup>
<thead>
<tr>
<th>SupplierName</th>
<th>EmailAddress</th>
<th>PRNo's'</th>
<th>ItemNo's</th>
<th>ItemName's</th>
<th>Action</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div> </div>
<input hidden id="supplierId" />
<script src="~/jsfunctions/canvass/CanvassV4.js"></script>
@await Html.PartialAsync("PagesView/Canvass/_ForCanvass")
<link href="~/css/canvass/canvass.css" rel="stylesheet" />
</div> </div>
</body>
@* {{-- TAB NAV --}}
{{-- data-tab-id matches the ViewComponent switch:
1=ForTagging, 2=ForCanvass, 3=ForApproval, 4=Completed --}} *@
<div class="cv-tabs" role="tablist">
<button class="cv-tab-btn active" data-tab-id="1" role="tab">
<i class="fas fa-user-tag"></i> For Tagging
</button>
<button class="cv-tab-btn" data-tab-id="2" role="tab">
<i class="fas fa-store"></i> For Canvass
</button>
<button class="cv-tab-btn" data-tab-id="3" role="tab">
<i class="fas fa-clock"></i> For Approval
</button>
<button class="cv-tab-btn" data-tab-id="4" role="tab">
<i class="fas fa-check-circle"></i> Completed
</button>
</div>
@* {{-- DYNAMIC TAB CONTENT — loaded via AJAX --}} *@
<div id="cv-tab-content">
<div class="cv-tab-loading">
<div class="cv-spinner"></div>
<span>Loading…</span>
</div>
</div>
</div>
<input hidden id="supplierId" />
@await Html.PartialAsync("PagesView/Canvass/_CanvassScript")
<script>
(function () {
"use strict";
const tabContent = document.getElementById("cv-tab-content");
const tabBtns = document.querySelectorAll(".cv-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="cv-tab-loading">
<div class="cv-spinner"></div><span>Loading…</span></div>`;
try {
const res = await fetch(`/CanvassMgmt/GetCanvassingTabPage?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="cv-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");
// Copy attributes (type, src, etc.) if any
Array.from(oldScript.attributes).forEach(attr =>
newScript.setAttribute(attr.name, attr.value));
newScript.textContent = oldScript.textContent;
oldScript.replaceWith(newScript);
});
}
// Wire tab clicks
tabBtns.forEach(btn =>
btn.addEventListener("click", () => loadTab(parseInt(btn.dataset.tabId, 10)))
);
// Auto-load the first tab on page ready
loadTab(1);
})();
</script>

View File

@ -0,0 +1,25 @@
@* ~/Views/Components/CanvassMgmt/CanvassingTabPage/Completed.cshtml
Tab 4 — Completed
Rendered by CanvassingTabPageViewComponent (id = 4, default).
Wire up your endpoint + card builder here following the same pattern as ForCanvass.cshtml.
*@
<div class="cv-placeholder">
<i class="fas fa-check-circle"></i>
<h3>Completed</h3>
<p>Wire up your endpoint and card builder here — follow the same pattern as ForCanvass.cshtml.</p>
</div>
@*
── PATTERN TO FOLLOW ──────────────────────────────────────────────────────
1. Copy the filter bar + grid + pagination HTML from ForCanvass.cshtml.
2. Change all id prefixes from "fc-" to "co-" to avoid DOM conflicts.
3. In the <script> block change the fetch URL to your completed endpoint.
4. Customise the card footer (e.g. View Summary / Download PDF).
Example footer for this tab:
H.buildCardHtml(item, i => `
<button class="cv-btn cv-btn-primary btn-summary" data-id="${i.supplierId}">
<i class="fas fa-file-contract"></i> View Summary
</button>`)
*@

View File

@ -0,0 +1,28 @@
@* ~/Views/Components/CanvassMgmt/CanvassingTabPage/ForApproval.cshtml
Tab 3 — For Approval
Rendered by CanvassingTabPageViewComponent (id = 3).
Wire up your endpoint + card builder here following the same pattern as ForCanvass.cshtml.
*@
<div class="cv-placeholder">
<i class="fas fa-clock"></i>
<h3>For Approval</h3>
<p>Wire up your endpoint and card builder here — follow the same pattern as ForCanvass.cshtml.</p>
</div>
@*
── PATTERN TO FOLLOW ──────────────────────────────────────────────────────
1. Copy the filter bar + grid + pagination HTML from ForCanvass.cshtml.
2. Change all id prefixes from "fc-" to "fa-" to avoid DOM conflicts.
3. In the <script> block change the fetch URL to your approval endpoint.
4. Customise the card footer buttons (e.g. Approve / Reject).
Example footer for this tab:
H.buildCardHtml(item, i => `
<button class="cv-btn cv-btn-primary btn-approve" data-id="${i.supplierId}">
<i class="fas fa-check"></i> Approve
</button>
<button class="cv-btn cv-btn-outline btn-reject" data-id="${i.supplierId}" style="border-color:#ff5c5c;color:#ff5c5c;">
<i class="fas fa-times"></i> Reject
</button>`)
*@

View File

@ -0,0 +1,150 @@
@* {{-- FILTER BAR --}} *@
<div class="cv-filters">
<div class="cv-search-box">
<i class="fas fa-hashtag"></i>
<input type="text" id="fc-srchItemNo" placeholder="Item Number..." />
</div>
<div class="cv-search-box">
<i class="fas fa-box"></i>
<input type="text" id="fc-srchItemName" placeholder="Item Name..." />
</div>
<div class="cv-search-box">
<i class="fas fa-file-alt"></i>
<input type="text" id="fc-srchPRNo" placeholder="PR Number..." />
</div>
@* {{-- Supplier dropdown --}} *@
<div class="cv-supplier-wrap" id="fc-supplierWrap">
<div class="cv-sup-trigger">
<span class="cv-sup-left">
<i class="fas fa-store"></i>
<span class="cv-sup-lbl">All Suppliers</span>
</span>
<i class="fas fa-chevron-down cv-sup-caret"></i>
</div>
<div class="cv-sup-dropdown">
<div class="cv-sup-searchbox">
<i class="fas fa-search"></i>
<input type="text" placeholder="Search supplier..." autocomplete="off" />
</div>
<div class="cv-sup-list">
<div class="cv-sup-opt active" data-value="">
<i class="fas fa-th-large"></i> All Suppliers
</div>
</div>
</div>
</div>
<div class="cv-filter-right">
<span class="cv-pgsz-lbl">Show</span>
<select class="cv-pgsz-sel" id="fc-pageSize">
<option value="6">6 per page</option>
<option value="12" selected>12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
</select>
<span class="cv-result-count" id="fc-resultCount">0 results</span>
</div>
</div>
@* {{-- CARD GRID --}} *@
<div class="cv-grid" id="fc-grid">
<div class="cv-state" style="grid-column:1/-1">
<div class="cv-spinner"></div><p>Loading…</p>
</div>
</div>
@* {{-- PAGINATION --}} *@
<div class="cv-pagination">
<span class="cv-pg-info" id="fc-pageInfo"></span>
<div class="cv-pg-btns" id="fc-pageButtons"></div>
</div>
<script>
(function () {
"use strict";
const H = window.CanvassHelpers;
const s = { page: 1, pageSize: 12, totalCount: 0, supplier: "", searchPR: "", searchItem: "", searchName: "", timer: null };
const grid = document.getElementById("fc-grid");
const countEl = document.getElementById("fc-resultCount");
const pageInfo = document.getElementById("fc-pageInfo");
const pageBtns = document.getElementById("fc-pageButtons");
const inItemNo = document.getElementById("fc-srchItemNo");
const inName = document.getElementById("fc-srchItemName");
const inPR = document.getElementById("fc-srchPRNo");
const inSize = document.getElementById("fc-pageSize");
const sup = H.initSupplierDropdown(
document.getElementById("fc-supplierWrap"),
val => { s.supplier = val; s.page = 1; fetchData(); }
);
[inItemNo, inName, inPR].forEach(el =>
el.addEventListener("input", () => {
clearTimeout(s.timer);
s.timer = setTimeout(() => {
s.searchItem = inItemNo.value.trim();
s.searchName = inName.value.trim();
s.searchPR = inPR.value.trim();
s.page = 1; fetchData();
}, 350);
})
);
inSize.addEventListener("change", () => { s.pageSize = parseInt(inSize.value,10); s.page=1; fetchData(); });
async function fetchData() {
grid.innerHTML = `<div class="cv-state" style="grid-column:1/-1"><div class="cv-spinner"></div><p>Loading…</p></div>`;
const p = new URLSearchParams({ searchPRNo: s.searchPR, searchItemNo: s.searchItem, searchItemName: s.searchName, searchSupplier: s.supplier, pageNumber: s.page, pageSize: s.pageSize, draw: Date.now() });
try {
const res = await fetch(`/CanvassMgmt/GetCanvassPerSupplier?${p}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
s.totalCount = json.recordsTotal ?? 0;
sup.setItems(json.supplierList);
renderCards(json.data ?? []);
H.renderPagination(pageBtns, pageInfo, s, pg => { s.page=pg; fetchData(); });
countEl.textContent = `${s.totalCount.toLocaleString()} result${s.totalCount !== 1 ? "s" : ""}`;
} catch (err) {
console.error(err);
grid.innerHTML = `<div class="cv-state" style="grid-column:1/-1"><i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i><p>Failed to load data.</p></div>`;
}
}
function renderCards(data) {
if (!data.length) {
grid.innerHTML = `<div class="cv-state" style="grid-column:1/-1"><i class="fas fa-inbox"></i><p>No records found.</p></div>`;
return;
}
// <button class="cv-btn cv-btn-primary btn-canvass" data-id="${i.supplierId}">
// <i class="fas fa-eye"></i> View Canvass
// </button>
grid.innerHTML = data.map(item =>
H.buildCardHtml(item, i => `
<button class="cv-btn cv-btn-outline btn-email" data-id="${i.supplierId}">
<i class="fas fa-paper-plane"></i> Send Email
</button>`)
).join("");
grid.querySelectorAll(".btn-canvass").forEach(b =>
b.addEventListener("click", () => openCanvass(b.dataset.id)));
grid.querySelectorAll(".btn-email").forEach(b =>
b.addEventListener("click", () => sendEmail(b.dataset.id)));
}
function openCanvass(id) {
document.getElementById("supplierId").value = id;
// e.g. $('#canvassModal').modal('show');
console.log("Open canvass:", id);
}
function sendEmail(id) {
console.log("Send email:", id);
}
fetchData();
})();
</script>

View File

@ -0,0 +1,870 @@
<div class="cv-filters">
<div class="cv-search-box">
<i class="fas fa-file-alt"></i>
<input type="text" id="ft-srchPRNo" placeholder="PR Number..." />
</div>
<div class="cv-search-box">
<i class="fas fa-hashtag"></i>
<input type="text" id="ft-srchItemNo" placeholder="Item Number..." />
</div>
<div class="cv-search-box">
<i class="fas fa-box"></i>
<input type="text" id="ft-srchItemName" placeholder="Item Name..." />
</div>
<div class="cv-department-wrap" id="ft-departmentWrap">
<div class="cv-dep-trigger">
<span class="cv-dep-left">
<i class="fas fa-building"></i>
<span class="cv-dep-lbl">All Departments</span>
</span>
<i class="fas fa-chevron-down cv-dep-caret"></i>
</div>
<div class="cv-dep-dropdown">
<div class="cv-dep-searchbox">
<i class="fas fa-search"></i>
<input type="text" placeholder="Search department..." autocomplete="off" />
</div>
<div class="cv-dep-list">
<div class="cv-dep-opt active" data-value="">
<i class="fas fa-th-large"></i> All Departments
</div>
</div>
</div>
</div>
<div class="cv-filter-right">
<span class="cv-pgsz-lbl">Show</span>
<select class="cv-pgsz-sel" id="ft-pageSize">
<option value="6">6 per page</option>
<option value="12" selected>12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
</select>
<span class="cv-result-count" id="ft-resultCount">0 results</span>
</div>
</div>
@* {{-- ── SELECTION ACTION BAR (hidden until ≥1 checked) ── --}} *@
<div class="ft-selection-bar" id="ft-selectionBar">
<div class="ft-sel-left">
<span class="ft-sel-badge" id="ft-selCount">0 selected</span>
<button class="ft-sel-clear" id="ft-selClear" title="Deselect all">
<i class="fas fa-times"></i> Clear
</button>
<button class="ft-sel-all" id="ft-selAll" title="Select all on this page">
<i class="fas fa-check-double"></i> Select All
</button>
</div>
<div class="ft-sel-right">
<span class="ft-sel-hint">Search suppliers:</span>
<button class="ft-canvass-btn ft-btn-local" id="ft-btnLocal">
<i class="fas fa-map-marker-alt"></i>
<span>Local</span>
</button>
<button class="ft-canvass-btn ft-btn-international" id="ft-btnIntl">
<i class="fas fa-globe"></i>
<span>International</span>
</button>
</div>
</div>
<div class="cv-grid" id="ft-grid">
<div class="cv-state" style="grid-column:1/-1">
<div class="cv-spinner"></div><p>Loading...</p>
</div>
</div>
<div class="cv-pagination">
<span class="cv-pg-info" id="ft-pageInfo"></span>
<div class="cv-pg-btns" id="ft-pageButtons"></div>
</div>
<style>
/* ── department dropdown (unchanged) ─────────────── */
.cv-department-wrap {
position: relative;
flex: 1;
min-width: 210px;
max-width: 280px;
}
.cv-dep-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 100%;
min-height: 40px;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
background: #fff;
cursor: pointer;
user-select: none;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
transition: border-color .2s;
}
.cv-dep-trigger:hover, .cv-dep-trigger.open {
border-color: var(--teal-mid);
}
.cv-dep-left {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
color: var(--text-dark);
}
.cv-dep-left i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-dep-lbl {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cv-dep-caret {
color: var(--text-muted);
font-size: .72rem;
flex-shrink: 0;
transition: transform .2s;
}
.cv-dep-trigger.open .cv-dep-caret {
transform: rotate(180deg);
}
.cv-dep-dropdown {
display: none;
position: absolute;
top: calc(100% + 5px);
left: 0;
right: 0;
z-index: 1000;
background: #fff;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-drop);
overflow: hidden;
}
.cv-dep-dropdown.open {
display: flex;
flex-direction: column;
}
.cv-dep-searchbox {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
.cv-dep-searchbox i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-dep-searchbox input {
border: none;
outline: none;
background: transparent;
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
color: var(--text-dark);
width: 100%;
}
.cv-dep-searchbox input::placeholder {
color: var(--text-muted);
}
.cv-dep-list {
max-height: 250px;
overflow-y: auto;
overscroll-behavior: contain;
}
.cv-dep-list::-webkit-scrollbar {
width: 4px;
}
.cv-dep-list::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
.cv-dep-opt {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
cursor: pointer;
font-size: .85rem;
color: var(--text-dark);
transition: background .15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cv-dep-opt i {
color: var(--text-muted);
font-size: .78rem;
flex-shrink: 0;
}
.cv-dep-opt:hover {
background: var(--teal-pale);
}
.cv-dep-opt.active {
background: var(--teal-pale);
color: var(--teal-dark);
font-weight: 600;
}
.cv-dep-opt.active i {
color: var(--teal-mid);
}
/* ── card header (ForTagging specific) ───────────── */
.ft-card-hd {
background: linear-gradient(135deg, #1a3a4a 0%, #1e5468 100%);
padding: 14px 16px 12px;
position: relative;
}
.ft-pr-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: rgba(255,255,255,.15);
border-radius: 50px;
padding: 3px 10px;
font-size: .7rem;
font-weight: 700;
color: rgba(255,255,255,.9);
letter-spacing: .04em;
text-transform: uppercase;
margin-bottom: 6px;
}
.ft-item-name {
font-family: 'Space Grotesk', sans-serif;
font-size: 1rem;
font-weight: 700;
color: #fff;
line-height: 1.25;
word-break: break-word;
}
.ft-item-no {
font-size: .77rem;
color: rgba(255,255,255,.65);
margin-top: 4px;
display: flex;
align-items: center;
gap: 5px;
}
.ft-department {
font-size: .77rem;
color: rgba(255,255,255,.65);
margin-top: 4px;
display: flex;
align-items: center;
gap: 5px;
}
/* ── card checkbox ───────────────────────────────── */
.ft-card-checkbox-wrap {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
}
.ft-card-checkbox {
width: 20px;
height: 20px;
accent-color: var(--teal-mid);
cursor: pointer;
border-radius: 4px;
}
/* ── card selected state ─────────────────────────── */
.cv-card.ft-selected {
outline: 2px solid var(--teal-mid);
outline-offset: 1px;
box-shadow: 0 0 0 4px rgba(14,124,134,.12), var(--shadow-hover);
}
.cv-card.ft-selected .ft-card-hd {
background: linear-gradient(135deg, #0d5c63 0%, #0e7c86 100%);
}
/* ── meta / description boxes ────────────────────── */
.ft-meta-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.ft-meta-box {
background: var(--teal-pale);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 9px 11px;
display: flex;
flex-direction: column;
gap: 3px;
}
.ft-meta-lbl {
font-size: .67rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
font-weight: 700;
display: flex;
align-items: center;
gap: 4px;
}
.ft-meta-val {
font-size: .83rem;
color: var(--teal-dark);
font-weight: 600;
line-height: 1.4;
word-break: break-word;
}
.ft-desc-box {
background: #f8fafb;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 9px 11px;
}
.ft-desc-lbl {
font-size: .67rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
font-weight: 700;
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.ft-desc-val {
font-size: .83rem;
color: var(--text-dark);
line-height: 1.5;
}
.ft-date-badge {
display: inline-flex;
align-items: center;
gap: 5px;
border-radius: 50px;
padding: 4px 12px;
font-size: .75rem;
font-weight: 600;
white-space: nowrap;
background: #fff3cd;
border: 1px solid #ffc107;
color: #856404;
}
.ft-date-badge.overdue {
background: #ffe0e0;
border-color: #ff5c5c;
color: #c0392b;
}
.ft-date-badge.ok {
background: #d4edda;
border-color: #28a745;
color: #155724;
}
.ft-selection-bar {
display: none; /* hidden by default — shown via JS */
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
background: linear-gradient(135deg, #0d5c63, #0e7c86);
border-radius: var(--radius-lg);
padding: 12px 18px;
margin-bottom: 16px;
box-shadow: 0 4px 16px rgba(13,92,99,.22);
animation: ftBarSlideIn .22s ease;
}
.ft-selection-bar.visible {
display: flex;
}
@@keyframes ftBarSlideIn {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.ft-sel-left {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.ft-sel-right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.ft-sel-badge {
background: rgba(255,255,255,.22);
color: #fff;
font-weight: 700;
font-size: .82rem;
padding: 5px 14px;
border-radius: 50px;
white-space: nowrap;
min-width: 90px;
text-align: center;
}
.ft-sel-clear, .ft-sel-all {
background: rgba(255,255,255,.12);
border: 1px solid rgba(255,255,255,.25);
color: rgba(255,255,255,.88);
font-family: 'DM Sans', sans-serif;
font-size: .8rem;
font-weight: 600;
padding: 5px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: background .18s;
}
.ft-sel-clear:hover, .ft-sel-all:hover {
background: rgba(255,255,255,.22);
color: #fff;
}
.ft-sel-hint {
font-size: .8rem;
color: rgba(255,255,255,.72);
font-weight: 500;
white-space: nowrap;
}
/* Local / International buttons */
.ft-canvass-btn {
display: flex;
align-items: center;
gap: 7px;
padding: 8px 18px;
border: none;
border-radius: var(--radius-sm);
font-family: 'DM Sans', sans-serif;
font-size: .84rem;
font-weight: 700;
cursor: pointer;
transition: all .2s;
white-space: nowrap;
}
.ft-btn-local {
background: #fff;
color: var(--teal-dark);
box-shadow: 0 2px 8px rgba(0,0,0,.12);
}
.ft-btn-local:hover {
background: var(--teal-pale);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,.16);
}
.ft-btn-international {
background: #f0a500;
color: #fff;
box-shadow: 0 2px 8px rgba(240,165,0,.3);
}
.ft-btn-international:hover {
background: #d4920a;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(240,165,0,.4);
}
/* loading state for canvass buttons */
.ft-canvass-btn.loading {
opacity: .65;
pointer-events: none;
cursor: not-allowed;
}
.ft-canvass-btn.loading span::after {
content: '…';
animation: ftDots 1s steps(3, end) infinite;
}
</style>
<script>
(function () {
"use strict";
const H = window.CanvassHelpers;
const s = {
page: 1, pageSize: 12, totalCount: 0,
searchPR: "", searchItem: "", searchName: "",
department: "",
timer: null,
// key = prDetailsId (string), value = { prDetailsId, prNo, itemName }
selected: new Map()
};
const grid = document.getElementById("ft-grid");
const countEl = document.getElementById("ft-resultCount");
const pageInfo = document.getElementById("ft-pageInfo");
const pageBtns = document.getElementById("ft-pageButtons");
const inPR = document.getElementById("ft-srchPRNo");
const inItemNo = document.getElementById("ft-srchItemNo");
const inName = document.getElementById("ft-srchItemName");
const inSize = document.getElementById("ft-pageSize");
// Selection bar elements
const selBar = document.getElementById("ft-selectionBar");
const selCount = document.getElementById("ft-selCount");
const btnClear = document.getElementById("ft-selClear");
const btnSelAll = document.getElementById("ft-selAll");
const btnLocal = document.getElementById("ft-btnLocal");
const btnIntl = document.getElementById("ft-btnIntl");
// ── Department dropdown ──────────────────────────────
const dep = H.initDepartmentDropdown(
document.getElementById("ft-departmentWrap"),
val => { s.department = val; s.page = 1; fetchData(); }
);
// ── Search inputs ────────────────────────────────────
[inPR, inItemNo, inName].forEach(el =>
el.addEventListener("input", () => {
clearTimeout(s.timer);
s.timer = setTimeout(() => {
s.searchPR = inPR.value.trim();
s.searchItem = inItemNo.value.trim();
s.searchName = inName.value.trim();
s.page = 1; fetchData();
}, 350);
})
);
inSize.addEventListener("change", () => {
s.pageSize = parseInt(inSize.value, 10);
s.page = 1; fetchData();
});
// ── Selection bar controls ───────────────────────────
btnClear.addEventListener("click", () => {
s.selected.clear();
syncCheckboxes();
updateSelectionBar();
});
btnSelAll.addEventListener("click", () => {
grid.querySelectorAll(".cv-card[data-id]").forEach(card => {
const id = card.dataset.id;
if (!s.selected.has(id)) {
s.selected.set(id, {
prDetailsId: id,
prNo: card.dataset.prno,
itemName: card.dataset.itemname
});
}
});
syncCheckboxes();
updateSelectionBar();
});
btnLocal.addEventListener("click", () => fireCanvass("Local"));
btnIntl.addEventListener("click", () => fireCanvass("International"));
// ── Sync checkbox visual state to s.selected ────────
function syncCheckboxes() {
grid.querySelectorAll(".cv-card[data-id]").forEach(card => {
const cb = card.querySelector(".ft-card-checkbox");
if (!cb) return;
const checked = s.selected.has(card.dataset.id);
cb.checked = checked;
card.classList.toggle("ft-selected", checked);
});
}
function updateSelectionBar() {
const count = s.selected.size;
if (count > 0) {
selCount.textContent = count + " item" + (count !== 1 ? "s" : "") + " selected";
selBar.classList.add("visible");
} else {
selBar.classList.remove("visible");
}
}
// ── Fire to backend ──────────────────────────────────
async function fireCanvass(locationType) {
const items = Array.from(s.selected.values());
if (!items.length) return;
const btn = locationType === "Local" ? btnLocal : btnIntl;
btn.classList.add("loading");
const origLabel = btn.querySelector("span").textContent;
btn.querySelector("span").textContent = "Sending";
try {
const res = await fetch("/CanvassMgmt/StartCanvass", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
locationType: locationType,
items: items.map(i => ({
prDetailsId: i.prDetailsId,
prNo: i.prNo,
itemName: i.itemName
}))
})
});
if (!res.ok) throw new Error("HTTP " + res.status);
await res.json();
// Clear selection after successful fire
s.selected.clear();
syncCheckboxes();
updateSelectionBar();
showToast(
(locationType === "Local"
? '<i class="fas fa-map-marker-alt"></i> Local canvass started for '
: '<i class="fas fa-globe"></i> International canvass started for ')
+ items.length + ' item' + (items.length !== 1 ? 's' : '') + '.',
"success"
);
// Refresh grid so processed items reflect updated state
fetchData();
} catch (err) {
console.error("fireCanvass error:", err);
showToast('<i class="fas fa-exclamation-triangle"></i> Failed to start canvass. Please try again.', "error");
} finally {
btn.classList.remove("loading");
btn.querySelector("span").textContent = origLabel;
}
}
// ── Toast notification ───────────────────────────────
function showToast(message, type) {
const existing = document.getElementById("ft-toast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.id = "ft-toast";
toast.className = "ft-toast ft-toast-" + type;
toast.innerHTML = message;
// Insert toast just above the grid
grid.parentNode.insertBefore(toast, grid);
// Auto-dismiss after 4 seconds with fade-out
setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateY(-6px)";
setTimeout(() => toast.remove(), 300);
}, 4000);
}
// ── Fetch data from backend ──────────────────────────
async function fetchData() {
grid.innerHTML = '<div class="cv-state" style="grid-column:1/-1"><div class="cv-spinner"></div><p>Loading...</p></div>';
const p = new URLSearchParams({
searchPRNo: s.searchPR,
searchItemNo: s.searchItem,
searchItemName: s.searchName,
searchDepartment: s.department,
pageNumber: s.page,
pageSize: s.pageSize,
draw: Date.now()
});
try {
const res = await fetch("/CanvassMgmt/GetItemsForTagging?" + p);
if (!res.ok) throw new Error("HTTP " + res.status);
const json = await res.json();
s.totalCount = json.recordsTotal || 0;
dep.setItems(json.departmentList);
renderCards(json.data || []);
H.renderPagination(pageBtns, pageInfo, s, pg => { s.page = pg; fetchData(); });
countEl.textContent = s.totalCount.toLocaleString() + " result" + (s.totalCount !== 1 ? "s" : "");
// Re-sync checkboxes so previously selected items
// still appear checked if they reappear on same page
syncCheckboxes();
} catch (err) {
console.error(err);
grid.innerHTML = '<div class="cv-state" style="grid-column:1/-1"><i class="fas fa-exclamation-triangle" style="color:#ff5c5c"></i><p>Failed to load data.</p></div>';
}
}
// ── Render cards ─────────────────────────────────────
function renderCards(data) {
if (!data.length) {
grid.innerHTML = '<div class="cv-state" style="grid-column:1/-1"><i class="fas fa-inbox"></i><p>No records found.</p></div>';
return;
}
const esc = H.escHtml;
const attr = H.escAttr;
const today = new Date(); today.setHours(0, 0, 0, 0);
grid.innerHTML = data.map(function (item) {
var createdDate = fmtDate(item.createdDate);
var dateNeeded = fmtDate(item.dateNeeded);
var needDate = item.dateNeeded ? new Date(item.dateNeeded) : null;
var daysLeft = needDate ? Math.ceil((needDate - today) / 86400000) : null;
var badgeClass = "ok";
var badgeLabel = "Needed: " + dateNeeded;
if (daysLeft !== null) {
if (daysLeft < 0) {
badgeClass = "overdue";
badgeLabel += " \u2014 Overdue by " + Math.abs(daysLeft) + "d";
} else {
badgeLabel += " \u2014 " + daysLeft + "d left";
}
}
// Check if this card was previously selected (persists across pages)
var isChecked = s.selected.has(String(item.prDetailsId));
return '<div class="cv-card' + (isChecked ? ' ft-selected' : '') + '"'
+ ' data-id="' + attr(String(item.prDetailsId)) + '"'
+ ' data-prno="' + attr(String(item.prNo || "")) + '"'
+ ' data-itemname="' + attr(item.itemName || "") + '">'
// ── Card header ──────────────────────────
+ '<div class="ft-card-hd">'
+ '<label class="ft-card-checkbox-wrap" title="Select this item">'
+ '<input type="checkbox" class="ft-card-checkbox"'
+ (isChecked ? ' checked' : '') + ' />'
+ '</label>'
+ '<div class="ft-pr-badge"><i class="fas fa-file-alt"></i> PR #' + esc(String(item.prNo || "-")) + '</div>'
+ '<div class="ft-item-name">' + esc(item.itemName || "-") + '</div>'
+ '<div class="ft-item-no"><i class="fas fa-hashtag"></i> Item No: ' + esc(String(item.itemNo || "-")) + '</div>'
+ '<div class="ft-department"><i class="fas fa-building-user"></i> Department: ' + esc(String(item.department || "-")) + '</div>'
+ '</div>'
// ── Card body ────────────────────────────
+ '<div class="cv-card-body">'
+ '<div class="ft-desc-box">'
+ '<div class="ft-desc-lbl"><i class="fas fa-align-left"></i> Description</div>'
+ '<div class="ft-desc-val">' + esc(item.itemDescription || "-") + '</div>'
+ '</div>'
+ '<div class="ft-meta-grid">'
+ '<div class="ft-meta-box"><span class="ft-meta-lbl"><i class="fas fa-user"></i> Created By</span>'
+ '<span class="ft-meta-val">' + esc(item.createdBy || "-") + '</span></div>'
+ '<div class="ft-meta-box"><span class="ft-meta-lbl"><i class="fas fa-calendar-plus"></i> Created Date</span>'
+ '<span class="ft-meta-val">' + esc(createdDate) + '</span></div>'
+ '</div>'
+ '<div><span class="ft-date-badge ' + badgeClass + '"><i class="fas fa-clock"></i> ' + esc(badgeLabel) + '</span></div>'
+ '</div>'
// ── Card footer ──────────────────────────
+ '<div class="cv-card-ft">'
+ '<button class="cv-btn cv-btn-primary btn-tag"'
+ ' data-id="' + attr(String(item.prDetailsId)) + '"'
+ ' data-prno="' + attr(String(item.prNo || "")) + '"'
+ ' data-itemname="' + attr(item.itemName || "") + '">'
+ '<i class="fas fa-user-tag"></i> Tag Supplier'
+ '</button>'
+ '</div>'
+ '</div>';
}).join("");
// ── Wire checkbox clicks ─────────────────────────
grid.querySelectorAll(".cv-card[data-id]").forEach(function (card) {
const cb = card.querySelector(".ft-card-checkbox");
if (!cb) return;
// Clicking the checkbox label area
cb.addEventListener("change", function () {
const id = card.dataset.id;
if (cb.checked) {
s.selected.set(id, {
prDetailsId: id,
prNo: card.dataset.prno,
itemName: card.dataset.itemname
});
} else {
s.selected.delete(id);
}
card.classList.toggle("ft-selected", cb.checked);
updateSelectionBar();
});
});
// ── Wire action button clicks ────────────────────
grid.querySelectorAll(".btn-tag").forEach(function (b) {
b.addEventListener("click", function () {
tagSupplier(b.dataset.id, b.dataset.prno, b.dataset.itemname);
});
});
}
// ── Helpers ──────────────────────────────────────────
function fmtDate(raw) {
if (!raw) return "-";
try {
return new Date(raw).toLocaleDateString("en-PH", {
year: "numeric", month: "short", day: "2-digit"
});
} catch (e) { return String(raw); }
}
function tagSupplier(prDetailsId, prNo, itemName) {
console.log("Tag supplier | PRDetailsId:", prDetailsId, "| PR#:", prNo, "| Item:", itemName);
}
fetchData();
})();
</script>

View File

@ -0,0 +1,233 @@
<script>
window.CanvassHelpers = (function () {
"use strict";
/* ── String helpers ──────────────────────────────── */
function splitAggr(raw) {
if (!raw) return [];
return raw.replace(/<br\s*\/?>/gi, ",").split(",").map(s => s.trim()).filter(Boolean);
}
function escHtml(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
function escAttr(s) {
return String(s).replace(/"/g,"&quot;").replace(/'/g,"&#39;");
}
/* ── Pagination helpers ──────────────────────────── */
function buildPageRange(cur, total) {
if (total <= 7) return Array.from({length: total}, (_, i) => i + 1);
if (cur <= 4) return [1, 2, 3, 4, 5, "…", total];
if (cur >= total - 3) return [1, "…", total-4, total-3, total-2, total-1, total];
return [1, "…", cur-1, cur, cur+1, "…", total];
}
function mkPageBtn(html, disabled, active) {
const b = document.createElement("button");
b.className = "cv-pg-btn" + (active ? " active" : "");
b.innerHTML = html; b.disabled = !!disabled;
return b;
}
function renderPagination(container, infoEl, state, onPageChange) {
const { page, pageSize, totalCount } = state;
const totalPages = Math.ceil(totalCount / pageSize) || 1;
const from = Math.min((page - 1) * pageSize + 1, totalCount);
const to = Math.min(page * pageSize, totalCount);
infoEl.textContent = totalCount
? `Showing ${from.toLocaleString()}${to.toLocaleString()} of ${totalCount.toLocaleString()}`
: "No records";
container.innerHTML = "";
const prev = mkPageBtn('<i class="fas fa-chevron-left"></i>', page <= 1);
prev.addEventListener("click", () => { if (page > 1) onPageChange(page - 1); });
container.appendChild(prev);
buildPageRange(page, totalPages).forEach(p => {
if (p === "…") {
const d = document.createElement("span");
d.className = "cv-pg-btn"; d.style.cursor = "default"; d.textContent = "…";
container.appendChild(d); return;
}
const b = mkPageBtn(p, false, p === page);
b.addEventListener("click", () => onPageChange(p));
container.appendChild(b);
});
const next = mkPageBtn('<i class="fas fa-chevron-right"></i>', page >= totalPages);
next.addEventListener("click", () => { if (page < totalPages) onPageChange(page + 1); });
container.appendChild(next);
}
/* ── Generic searchable dropdown factory ─────────────────────────────
*
* Works for BOTH suppliers and departments (or any list).
* CSS class prefixes are passed in so each dropdown is fully independent.
*
* param wrap HTMLElement — the root wrapper element
* param onChange Function — called with the selected value string
* param opts Object — optional overrides:
* allLabel : string label for "All" option (default "All")
* icon : string FA icon class (default "fa-th-large / fa-store")
* cssPrefix : string CSS class prefix (default "cv-sup")
*
* CSS classes expected inside `wrap`:
* {prefix}-trigger, {prefix}-dropdown, {prefix}-lbl,
* {prefix}-searchbox > input, {prefix}-list
*
* Returns: { setItems(list), getCurrent() }
──────────────────────────────────────────────────────────────────── */
function initSearchDropdown(wrap, onChange, opts) {
opts = opts || {};
const prefix = opts.cssPrefix || "cv-sup";
const allLabel = opts.allLabel || "All";
const allIcon = opts.allIcon || "fas fa-th-large";
const itemIcon = opts.itemIcon || "fas fa-store";
const trigger = wrap.querySelector(`.${prefix}-trigger`);
const dropdown = wrap.querySelector(`.${prefix}-dropdown`);
const label = wrap.querySelector(`.${prefix}-lbl`);
const search = wrap.querySelector(`.${prefix}-searchbox input`);
const list = wrap.querySelector(`.${prefix}-list`);
if (!trigger || !dropdown || !label || !search || !list) {
console.warn("initSearchDropdown: one or more required elements not found in", wrap);
return { setItems: () => {}, getCurrent: () => "" };
}
let allItems = [];
let current = "";
trigger.addEventListener("click", () => {
const open = dropdown.classList.toggle("open");
trigger.classList.toggle("open", open);
if (open) { search.value = ""; renderOpts(allItems); search.focus(); }
});
document.addEventListener("click", e => {
if (!wrap.contains(e.target)) {
dropdown.classList.remove("open");
trigger.classList.remove("open");
}
});
search.addEventListener("input", () => {
const q = search.value.trim().toLowerCase();
renderOpts(q ? allItems.filter(s => s.toLowerCase().includes(q)) : allItems);
});
list.addEventListener("click", e => {
const opt = e.target.closest("[data-value]");
if (!opt) return;
current = opt.dataset.value;
label.textContent = current || allLabel;
dropdown.classList.remove("open");
trigger.classList.remove("open");
list.querySelectorAll("[data-value]").forEach(o =>
o.classList.toggle("active", o.dataset.value === current));
onChange(current);
});
function renderOpts(items) {
const allHtml = `<div class="${prefix}-opt${current === "" ? " active" : ""}" data-value="">
<i class="${allIcon}"></i> ${escHtml(allLabel)}
</div>`;
if (!items.length) {
list.innerHTML = allHtml + `<div style="padding:12px;text-align:center;font-size:.85rem;color:#6b8890">No results found</div>`;
return;
}
list.innerHTML = allHtml + items.map(n =>
`<div class="${prefix}-opt${n === current ? " active" : ""}" data-value="${escAttr(n)}">
<i class="${itemIcon}"></i> ${escHtml(n)}
</div>`
).join("");
}
function setItems(newList) {
allItems = (newList || []).filter(Boolean);
renderOpts(allItems);
}
return { setItems, getCurrent: () => current };
}
/* ── Convenience wrappers (keep back-compat names) ───────────────── */
function initSupplierDropdown(wrap, onChange) {
return initSearchDropdown(wrap, onChange, {
cssPrefix: "cv-sup",
allLabel: "All Suppliers",
allIcon: "fas fa-th-large",
itemIcon: "fas fa-store"
});
}
function initDepartmentDropdown(wrap, onChange) {
return initSearchDropdown(wrap, onChange, {
cssPrefix: "cv-dep",
allLabel: "All Departments",
allIcon: "fas fa-th-large",
itemIcon: "fas fa-building"
});
}
/* ── Card HTML builder ───────────────────────────── */
function buildCardHtml(item, footerHtml) {
const prNos = splitAggr(item.aggrePRNo);
const itemNos = splitAggr(item.aggreItemNo);
const names = splitAggr(item.aggreItemName);
const MAX = 3;
const vis = names.slice(0, MAX);
const more = names.length - MAX;
return `
<div class="cv-card">
<div class="cv-card-hd">
<div class="cv-card-code">Supplier #${item.supplierId}</div>
<div class="cv-card-name">${escHtml(item.supplierName ?? "—")}</div>
<div class="cv-card-email">
<i class="fas fa-envelope"></i>
<span>${escHtml(item.emailAddress ?? "—")}</span>
</div>
</div>
<div class="cv-card-body">
<div class="cv-agg-row">
<div class="cv-agg-badge">
<span class="cv-agg-lbl"><i class="fas fa-file-alt"></i> PR No's</span>
<span class="cv-agg-val">${escHtml(prNos.join(", ") || "—")}</span>
</div>
<div class="cv-agg-badge">
<span class="cv-agg-lbl"><i class="fas fa-hashtag"></i> Item No's</span>
<span class="cv-agg-val">${escHtml(itemNos.join(", ") || "—")}</span>
</div>
</div>
<div>
<div class="cv-item-lbl"><i class="fas fa-box"></i> Item Names</div>
<div class="cv-item-tags">
${vis.map(n => `<span class="cv-item-tag" title="${escAttr(n)}">${escHtml(n)}</span>`).join("")}
${more > 0 ? `<span class="cv-item-more">+${more} more</span>` : ""}
${names.length === 0 ? `<span style="font-size:.82rem;color:var(--text-muted)">—</span>` : ""}
</div>
</div>
</div>
<div class="cv-card-ft">${footerHtml(item)}</div>
</div>`;
}
return {
splitAggr,
escHtml,
escAttr,
buildPageRange,
mkPageBtn,
renderPagination,
buildCardHtml,
initSearchDropdown,
initSupplierDropdown,
initDepartmentDropdown,
};
})();
</script>

View File

@ -0,0 +1,756 @@
<style>
:root {
--teal-dark: #0d5c63;
--teal-mid: #0e7c86;
--teal-light: #18a8b5;
--teal-pale: #e6f7f8;
--text-dark: #1a2e35;
--text-muted: #6b8890;
--border: #d6eaec;
--card-bg: #ffffff;
--bg-page: #f0f6f7;
--radius-lg: 14px;
--radius-sm: 8px;
--shadow-card: 0 2px 12px rgba(13,92,99,.10), 0 1px 3px rgba(0,0,0,.06);
--shadow-hover: 0 6px 24px rgba(13,92,99,.18);
--shadow-drop: 0 8px 28px rgba(13,92,99,.20);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* ── WRAPPER ─────────────────────────────── */
.canvass-wrapper {
font-family: 'DM Sans', sans-serif;
background: var(--bg-page);
color: var(--text-dark);
width: 100%;
padding: 20px 24px 48px;
}
@@media (max-width: 768px) {
.canvass-wrapper {
padding: 12px 12px 36px;
}
}
/* ── HEADER ── */
.cv-header {
background: linear-gradient( 270deg, #004d40, #00695c, #00897b, #00acc1, #00897b, #00695c, #004d40 );
background-size: 300% 100%;
animation: gradientShimmer 6s ease infinite;
padding: 26px 30px 22px;
border-radius: var(--radius-lg);
box-shadow: 0 6px 20px rgba(0, 150, 136, 0.35);
position: relative;
overflow: hidden;
margin-bottom: 18px;
margin-top:-60px;
}
.cv-header::before {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.cv-header-inner {
position: relative;
z-index: 1;
display: flex;
align-items: flex-start;
gap: 14px;
}
.cv-header-icon {
font-size: 2rem;
color: rgba(255,255,255,.85);
margin-top: 2px;
}
.cv-header h1 {
font-family: 'Space Grotesk', sans-serif;
font-size: 1.7rem;
font-weight: 700;
color: #fff;
line-height: 1.2;
}
.cv-header p {
font-size: .875rem;
color: rgba(255,255,255,.72);
margin-top: 4px;
}
@@media (max-width: 768px) {
.cv-header h1 {
font-size: 1.35rem;
}
}
/* ── TAB NAV ─── */
.cv-tabs {
display: flex;
gap: 6px;
background: #fff;
border-radius: var(--radius-lg);
padding: 6px;
box-shadow: var(--shadow-card);
margin-bottom: 18px;
}
.cv-tab-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 11px 18px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
white-space: nowrap;
}
.cv-tab-btn i {
font-size: .88rem;
}
.cv-tab-btn:hover {
background: var(--teal-pale);
color: var(--teal-dark);
}
.cv-tab-btn.active {
background: var(--teal-mid);
color: #fff;
box-shadow: 0 2px 8px rgba(14,124,134,.35);
}
.cv-tab-btn.loading {
opacity: .6;
pointer-events: none;
}
/* ── TAB CONTENT AREA ──── */
#cv-tab-content {
min-height: 300px;
}
.cv-tab-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 80px 20px;
color: var(--text-muted);
font-size: .9rem;
}
/* ── SHARED FILTER BAR ─── */
.cv-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: stretch;
background: #fff;
border-radius: var(--radius-lg);
padding: 14px 16px;
box-shadow: var(--shadow-card);
margin-bottom: 16px;
}
.cv-search-box {
display: flex;
align-items: center;
gap: 8px;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 0 12px;
flex: 1;
min-width: 140px;
background: #fff;
transition: border-color .2s;
}
.cv-search-box:focus-within {
border-color: var(--teal-mid);
}
.cv-search-box i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-search-box input {
border: none;
outline: none;
background: transparent;
padding: 9px 0;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark);
width: 100%;
}
.cv-search-box input::placeholder {
color: var(--text-muted);
}
/* ── SUPPLIER DROPDOWN ── */
.cv-supplier-wrap {
position: relative;
flex: 1;
min-width: 210px;
max-width: 280px;
}
@@media (max-width: 768px) {
.cv-supplier-wrap {
max-width: 100%;
}
}
.cv-sup-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 100%;
min-height: 40px;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
background: #fff;
cursor: pointer;
user-select: none;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
transition: border-color .2s;
}
.cv-sup-trigger:hover, .cv-sup-trigger.open {
border-color: var(--teal-mid);
}
.cv-sup-left {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
color: var(--text-dark);
}
.cv-sup-left i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-sup-lbl {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cv-sup-caret {
color: var(--text-muted);
font-size: .72rem;
flex-shrink: 0;
transition: transform .2s;
}
.cv-sup-trigger.open .cv-sup-caret {
transform: rotate(180deg);
}
.cv-sup-dropdown {
display: none;
position: absolute;
top: calc(100% + 5px);
left: 0;
right: 0;
z-index: 1000;
background: #fff;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-drop);
overflow: hidden;
}
.cv-sup-dropdown.open {
display: flex;
flex-direction: column;
}
.cv-sup-searchbox {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
.cv-sup-searchbox i {
color: var(--text-muted);
font-size: .8rem;
flex-shrink: 0;
}
.cv-sup-searchbox input {
border: none;
outline: none;
background: transparent;
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
color: var(--text-dark);
width: 100%;
}
.cv-sup-searchbox input::placeholder {
color: var(--text-muted);
}
.cv-sup-list {
max-height: 250px;
overflow-y: auto;
overscroll-behavior: contain;
}
.cv-sup-list::-webkit-scrollbar {
width: 4px;
}
.cv-sup-list::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
.cv-sup-opt {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
cursor: pointer;
font-size: .85rem;
color: var(--text-dark);
transition: background .15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cv-sup-opt i {
color: var(--text-muted);
font-size: .78rem;
flex-shrink: 0;
}
.cv-sup-opt:hover {
background: var(--teal-pale);
}
.cv-sup-opt.active {
background: var(--teal-pale);
color: var(--teal-dark);
font-weight: 600;
}
.cv-sup-opt.active i {
color: var(--teal-mid);
}
.cv-sup-empty {
padding: 14px 12px;
font-size: .85rem;
color: var(--text-muted);
text-align: center;
}
/* ── FILTER RIGHT ────────────────────────── */
.cv-filter-right {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
flex-shrink: 0;
}
@@media (max-width: 768px) {
.cv-filter-right {
margin-left: 0;
width: 100%;
}
}
.cv-pgsz-lbl {
font-size: .8rem;
color: var(--text-muted);
font-weight: 500;
}
.cv-pgsz-sel {
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 10px;
background: #fff;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark);
cursor: pointer;
outline: none;
transition: border-color .2s;
}
.cv-pgsz-sel:focus {
border-color: var(--teal-mid);
}
.cv-result-count {
font-size: .8rem;
font-weight: 600;
background: var(--teal-pale);
color: var(--teal-dark);
padding: 6px 14px;
border-radius: 50px;
white-space: nowrap;
}
/* ── CARD GRID ───────────────────────────── */
.cv-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 18px;
min-height: 220px;
}
@@media (max-width: 768px) {
.cv-grid {
grid-template-columns: 1fr;
}
}
@@media (min-width: 769px) and (max-width: 1100px) {
.cv-grid {
grid-template-columns: repeat(2,1fr);
}
}
/* ── CARD ────────────────────────────────── */
.cv-card {
background: var(--card-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
border: 1px solid var(--border);
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow .25s, transform .25s;
}
.cv-card:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
.cv-card-hd {
background: linear-gradient(135deg, var(--teal-dark), var(--teal-mid));
padding: 14px 16px 12px;
}
.cv-card-code {
font-size: .7rem;
color: rgba(255,255,255,.62);
font-weight: 600;
letter-spacing: .06em;
text-transform: uppercase;
margin-bottom: 3px;
}
.cv-card-name {
font-family: 'Space Grotesk', sans-serif;
font-size: 1rem;
font-weight: 700;
color: #fff;
line-height: 1.25;
word-break: break-word;
}
.cv-card-email {
font-size: .77rem;
color: rgba(255,255,255,.68);
margin-top: 5px;
display: flex;
align-items: flex-start;
gap: 5px;
word-break: break-all;
}
.cv-card-email i {
margin-top: 2px;
flex-shrink: 0;
}
.cv-card-body {
padding: 14px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.cv-agg-row {
display: flex;
gap: 8px;
}
.cv-agg-badge {
flex: 1;
background: var(--teal-pale);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 9px 11px;
display: flex;
flex-direction: column;
gap: 3px;
}
.cv-agg-lbl {
font-size: .67rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
font-weight: 700;
display: flex;
align-items: center;
gap: 4px;
}
.cv-agg-val {
font-size: .83rem;
color: var(--teal-dark);
font-weight: 600;
line-height: 1.45;
word-break: break-word;
}
.cv-item-lbl {
font-size: .67rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
font-weight: 700;
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 6px;
}
.cv-item-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.cv-item-tag {
display: inline-block;
padding: 3px 9px;
background: #f3f8f9;
border: 1px solid var(--border);
border-radius: 4px;
font-size: .78rem;
color: var(--text-dark);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cv-item-more {
display: inline-block;
padding: 3px 9px;
background: var(--teal-pale);
border: 1px solid var(--border);
border-radius: 4px;
font-size: .78rem;
color: var(--teal-dark);
font-weight: 600;
}
.cv-card-ft {
padding: 10px 16px 14px;
display: flex;
gap: 8px;
border-top: 1px solid var(--border);
}
.cv-btn {
flex: 1;
padding: 9px 14px;
border-radius: var(--radius-sm);
border: none;
cursor: pointer;
font-family: 'DM Sans', sans-serif;
font-size: .82rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all .2s;
}
.cv-btn-primary {
background: var(--teal-mid);
color: #fff;
}
.cv-btn-primary:hover {
background: var(--teal-dark);
}
.cv-btn-outline {
background: transparent;
color: var(--teal-dark);
border: 1.5px solid var(--teal-mid);
}
.cv-btn-outline:hover {
background: var(--teal-pale);
}
/* ── STATE / SPINNER ─────────────────────── */
.cv-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 20px;
gap: 14px;
}
.cv-state i {
font-size: 2.4rem;
color: var(--border);
}
.cv-state p {
color: var(--text-muted);
font-size: .9rem;
}
.cv-spinner {
width: 34px;
height: 34px;
border: 3px solid var(--border);
border-top-color: var(--teal-mid);
border-radius: 50%;
animation: cvspin .7s linear infinite;
}
@@keyframes cvspin {
to {
transform: rotate(360deg);
}
}
/* ── PAGINATION ──────────────────────────── */
.cv-pagination {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
padding: 14px 18px;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
}
.cv-pg-info {
font-size: .82rem;
color: var(--text-muted);
}
.cv-pg-btns {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.cv-pg-btn {
min-width: 36px;
height: 36px;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
border: 1.5px solid var(--border);
background: #fff;
color: var(--text-dark);
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
font-weight: 600;
cursor: pointer;
transition: all .18s;
}
.cv-pg-btn:hover:not(:disabled) {
border-color: var(--teal-mid);
color: var(--teal-mid);
background: var(--teal-pale);
}
.cv-pg-btn.active {
background: var(--teal-mid);
border-color: var(--teal-mid);
color: #fff;
}
.cv-pg-btn:disabled {
opacity: .35;
cursor: default;
}
/* ── PLACEHOLDER PANEL ───────────────────── */
.cv-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
gap: 14px;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
}
.cv-placeholder i {
font-size: 2.8rem;
color: var(--border);
}
.cv-placeholder h3 {
font-family: 'Space Grotesk', sans-serif;
font-size: 1.1rem;
color: var(--text-dark);
}
.cv-placeholder p {
font-size: .875rem;
color: var(--text-muted);
}
</style>

View File

@ -3,7 +3,7 @@
<title>@ViewData["Title"] - LLI Purchasing Non-Inventory System</title> <title>@ViewData["Title"] - LLI Purchasing Non-Inventory System</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/sideBarStyleV2.css" /> <link rel="stylesheet" href="~/css/sideBarStyleV2.css" />
<link rel="stylesheet" href="~/css/siteV6.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/siteV7.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/spinner.css" /> <link rel="stylesheet" href="~/css/spinner.css" />
<link href="~/lib/font-awesome/css/all.css" rel="stylesheet" /> <link href="~/lib/font-awesome/css/all.css" rel="stylesheet" />
<link href="~/lib/font-awesome/css/all.min.css" rel="stylesheet" /> <link href="~/lib/font-awesome/css/all.min.css" rel="stylesheet" />

View File

@ -102,6 +102,7 @@
"GetCanvassPerSupplierId": "api/CanvassMgmt/GetCanvassPerSupplierId/", "GetCanvassPerSupplierId": "api/CanvassMgmt/GetCanvassPerSupplierId/",
"GetCanvassGroupByPRNo": "api/CanvassMgmt/GetCanvassGroupByPRNo/", "GetCanvassGroupByPRNo": "api/CanvassMgmt/GetCanvassGroupByPRNo/",
"GetAlternativeOfferByPRDetailId": "api/CanvassMgmt/GetAlternativeOfferByPRDetailId/", "GetAlternativeOfferByPRDetailId": "api/CanvassMgmt/GetAlternativeOfferByPRDetailId/",
"GetItemsForTagging": "api/CanvassMgmt/GetItemsForTagging/",
"PostItemWOSupplierEmail": "api/CanvassMgmt/PostItemWOSupplierEmail/", "PostItemWOSupplierEmail": "api/CanvassMgmt/PostItemWOSupplierEmail/",
"PostPutSupplier": "api/CanvassMgmt/PostPutSupplier/", "PostPutSupplier": "api/CanvassMgmt/PostPutSupplier/",
"PostTaggingSupplier": "api/CanvassMgmt/PostTaggingSupplier/", "PostTaggingSupplier": "api/CanvassMgmt/PostTaggingSupplier/",

View File

@ -0,0 +1,513 @@
:root {
--teal-dark: #0d5c63;
--teal-mid: #0e7c86;
--teal-light: #18a8b5;
--teal-pale: #e6f7f8;
--accent: #ff5c5c;
--text-dark: #1a2e35;
--text-muted: #6b8890;
--border: #d6eaec;
--card-bg: #ffffff;
--bg-page: #f0f6f7;
--radius-lg: 14px;
--radius-sm: 8px;
--shadow-card: 0 2px 12px rgba(13,92,99,.10), 0 1px 3px rgba(0,0,0,.06);
--shadow-hover: 0 6px 24px rgba(13,92,99,.18);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body, .canvass-wrapper {
font-family: 'DM Sans', sans-serif;
background: var(--bg-page);
color: var(--text-dark);
}
/* ── HEADER ─────────────────────────────────────────── */
.cv-header {
background: linear-gradient(135deg, var(--teal-dark) 0%, var(--teal-mid) 55%, var(--teal-light) 100%);
padding: 28px 32px 24px;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
position: relative;
overflow: hidden;
}
.cv-header::before {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.cv-header-inner {
position: relative;
z-index: 1;
display: flex;
align-items: flex-start;
gap: 14px;
}
.cv-header-icon {
font-size: 2rem;
color: rgba(255,255,255,.85);
margin-top: 2px;
}
.cv-header h1 {
font-family: 'Space Grotesk', sans-serif;
font-size: 1.75rem;
font-weight: 700;
color: #fff;
line-height: 1.2;
}
.cv-header p {
font-size: .875rem;
color: rgba(255,255,255,.75);
margin-top: 4px;
}
/* ── OUTER WRAPPER ───────────────────────────────────── */
.canvass-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 24px 20px 40px;
}
/* ── TAB NAV ─────────────────────────────────────────── */
.cv-tabs {
display: flex;
gap: 6px;
background: #fff;
border-radius: var(--radius-lg);
padding: 6px;
box-shadow: var(--shadow-card);
margin-bottom: 20px;
overflow-x: auto;
}
.cv-tab-btn {
flex: 1;
min-width: 160px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 18px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
white-space: nowrap;
}
.cv-tab-btn i {
font-size: .95rem;
}
.cv-tab-btn:hover {
background: var(--teal-pale);
color: var(--teal-dark);
}
.cv-tab-btn.active {
background: var(--teal-mid);
color: #fff;
box-shadow: 0 2px 8px rgba(14,124,134,.35);
}
/* ── TAB PANELS ─────────────────────────────────────── */
.cv-panel {
display: none;
}
.cv-panel.active {
display: block;
}
/* ── FILTER BAR ─────────────────────────────────────── */
.cv-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 18px;
}
.cv-search-box {
display: flex;
align-items: center;
background: #fff;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 0 12px;
gap: 8px;
flex: 1;
min-width: 160px;
max-width: 260px;
transition: border-color .2s;
}
.cv-search-box:focus-within {
border-color: var(--teal-mid);
}
.cv-search-box i {
color: var(--text-muted);
font-size: .85rem;
}
.cv-search-box input {
border: none;
outline: none;
background: transparent;
padding: 9px 0;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark);
width: 100%;
}
.cv-search-box input::placeholder {
color: var(--text-muted);
}
.cv-page-size {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.cv-page-size label {
font-size: .8rem;
color: var(--text-muted);
font-weight: 500;
}
.cv-page-size select {
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
color: var(--text-dark);
cursor: pointer;
outline: none;
background: #fff;
transition: border-color .2s;
}
.cv-page-size select:focus {
border-color: var(--teal-mid);
}
.cv-result-count {
font-size: .8rem;
color: var(--text-muted);
font-weight: 500;
white-space: nowrap;
}
/* ── SUPPLIER PILLS ─────────────────────────────────── */
.cv-supplier-pills {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.cv-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 50px;
border: 1.5px solid var(--border);
background: #fff;
color: var(--text-muted);
font-size: .8rem;
font-weight: 600;
cursor: pointer;
transition: all .2s;
white-space: nowrap;
}
.cv-pill i {
font-size: .8rem;
}
.cv-pill:hover {
border-color: var(--teal-mid);
color: var(--teal-dark);
background: var(--teal-pale);
}
.cv-pill.active {
border-color: var(--teal-mid);
color: #fff;
background: var(--teal-mid);
}
/* ── CARD GRID ──────────────────────────────────────── */
.cv-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 18px;
min-height: 200px;
}
/* ── SINGLE CARD ────────────────────────────────────── */
.cv-card {
background: var(--card-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
border: 1px solid var(--border);
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow .25s, transform .25s;
}
.cv-card:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
.cv-card-header {
background: linear-gradient(135deg, var(--teal-dark), var(--teal-mid));
padding: 14px 16px 12px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}
.cv-card-header-left {
flex: 1;
min-width: 0;
}
.cv-card-supplier-code {
font-size: .72rem;
color: rgba(255,255,255,.7);
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
margin-bottom: 2px;
}
.cv-card-supplier-name {
font-family: 'Space Grotesk', sans-serif;
font-size: 1rem;
font-weight: 700;
color: #fff;
line-height: 1.25;
word-break: break-word;
}
.cv-card-email {
font-size: .78rem;
color: rgba(255,255,255,.7);
margin-top: 4px;
display: flex;
align-items: center;
gap: 5px;
}
.cv-card-body {
padding: 14px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.cv-agg-row {
display: flex;
gap: 8px;
}
.cv-agg-badge {
flex: 1;
background: var(--teal-pale);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.cv-agg-badge .label {
font-size: .68rem;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--text-muted);
font-weight: 600;
}
.cv-agg-badge .value {
font-size: .82rem;
color: var(--teal-dark);
font-weight: 600;
word-break: break-all;
line-height: 1.3;
}
.cv-item-names {
font-size: .82rem;
color: var(--text-dark);
line-height: 1.55;
}
.cv-item-names .item-label {
font-size: .68rem;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--text-muted);
font-weight: 600;
margin-bottom: 4px;
}
.cv-card-footer {
padding: 10px 16px 14px;
display: flex;
gap: 8px;
border-top: 1px solid var(--border);
}
.cv-btn {
flex: 1;
padding: 8px 14px;
border-radius: var(--radius-sm);
border: none;
cursor: pointer;
font-family: 'DM Sans', sans-serif;
font-size: .82rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all .2s;
}
.cv-btn-primary {
background: var(--teal-mid);
color: #fff;
}
.cv-btn-primary:hover {
background: var(--teal-dark);
}
.cv-btn-outline {
background: transparent;
color: var(--teal-dark);
border: 1.5px solid var(--teal-mid);
}
.cv-btn-outline:hover {
background: var(--teal-pale);
}
/* ── LOADING / EMPTY ────────────────────────────────── */
.cv-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
}
.cv-state i {
font-size: 2.5rem;
color: var(--border);
}
.cv-state p {
color: var(--text-muted);
font-size: .9rem;
}
.cv-spinner {
width: 36px;
height: 36px;
border: 3px solid var(--border);
border-top-color: var(--teal-mid);
border-radius: 50%;
animation: spin .7s linear infinite;
}
@@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ── PAGINATION ─────────────────────────────────────── */
.cv-pagination {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-top: 24px;
padding: 12px 0;
}
.cv-pagination-info {
font-size: .82rem;
color: var(--text-muted);
}
.cv-page-btns {
display: flex;
gap: 4px;
}
.cv-page-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
border: 1.5px solid var(--border);
background: #fff;
color: var(--text-dark);
font-family: 'DM Sans', sans-serif;
font-size: .85rem;
font-weight: 600;
cursor: pointer;
transition: all .18s;
}
.cv-page-btn:hover:not(:disabled) {
border-color: var(--teal-mid);
color: var(--teal-mid);
background: var(--teal-pale);
}
.cv-page-btn.active {
background: var(--teal-mid);
border-color: var(--teal-mid);
color: #fff;
}
.cv-page-btn:disabled {
opacity: .35;
cursor: default;
}

View File

@ -60,20 +60,7 @@ input.form-control {
table.dataTable tbody tr { table.dataTable tbody tr {
transition: background 0.2s ease; transition: background 0.2s ease;
} }
/*
table.dataTable tbody tr:nth-child(odd) {
background-color: #f9fffe !important;
}
table.dataTable tbody tr:nth-child(even) {
background-color: #ffffff !important;
}
table.dataTable tbody tr:hover {
background-color: #e0f7f5 !important;
cursor: pointer;
}
*/
table.dataTable tbody td { table.dataTable tbody td {
border-bottom: 1px solid rgba(0, 150, 136, 0.08) !important; border-bottom: 1px solid rgba(0, 150, 136, 0.08) !important;
padding: 12px !important; padding: 12px !important;
@ -419,7 +406,6 @@ tbody tr:last-child td:last-child {
@media print { @media print {
* { * {
-webkit-print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
print-color-adjust: exact !important; print-color-adjust: exact !important;
} }
} }
@ -432,3 +418,21 @@ tbody tr:last-child td:last-child {
border-radius: 15px; border-radius: 15px;
box-shadow: 0 4px 10px rgba(0,0,0,0.2); box-shadow: 0 4px 10px rgba(0,0,0,0.2);
} }
@keyframes ftDots {
0% {
content: '.';
}
33% {
content: '..';
}
66% {
content: '...';
}
100% {
content: '';
}
}

File diff suppressed because one or more lines are too long