AI Driven Searching and tagging with button and UI/UX enhancement in canvassing partial only
This commit is contained in:
parent
3825a986f6
commit
d1c9c4b52b
@ -2,6 +2,7 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass;
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass.Response;
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
using CPRNIMS.Infrastructure.Entities.Canvass;
|
||||
using CPRNIMS.Infrastructure.Entities.Purchasing;
|
||||
using System;
|
||||
@ -32,16 +33,17 @@ namespace CPRNIMS.Domain.Contracts.Canvass
|
||||
Task<List<PRCanvassDetail>> GetCanvassById(CanvassDto CanvassDto);
|
||||
Task<List<WOResponse>> GetCanvassWOResponse(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<BiddingItem>> GetSupplierBid(CanvassDto CanvassDto);
|
||||
Task<List<RFQPerSupplier>> GetSupplierBidByItem(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>> GetCanvassPerSupplierId(CanvassDto itemCodeDto);
|
||||
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<CanvassGroupByPRNo>> GetCanvassGroupByPRNo(CanvassDto CanvassDto);
|
||||
Task<List<PRCanvassDetail>> GetCanvassByItemNo(CanvassDto CanvassDto);
|
||||
@ -51,7 +53,7 @@ namespace CPRNIMS.Domain.Contracts.Canvass
|
||||
Task<List<ForCanvass>> GetForCanvassPerItem(CanvassDto CanvassDto);
|
||||
Task<int> GetCanvassNo();
|
||||
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<AlternativeOfferDetails>> GetAlternativeOfferByPRDetailId(CanvassDto itemDto);
|
||||
Task<List<AllForCanvass>> GetAllForCanvass();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass.Response;
|
||||
using CPRNIMS.Infrastructure.Entities.Canvass;
|
||||
using CPRNIMS.Infrastructure.Entities.Purchasing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -15,5 +16,6 @@ namespace CPRNIMS.Domain.Services.ICanvass
|
||||
Task<Result<SupplierResponse>> PostSupplierAsync(SupplierRequest request, CancellationToken ct);
|
||||
Task SendRFQ(SupplierEmailRequest supplierEmailRequest);
|
||||
Task<bool> SearchingUpdate(long pRDetailsId);
|
||||
Task<List<ForAISearchingTagging>> GetForAISearchingTagging();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using CPRNIMS.Infrastructure.ViewModel.Common;
|
||||
using CPRNIMS.Infrastructure.Entities.Canvass;
|
||||
|
||||
namespace CPRNIMS.Domain.Profile.Canvass
|
||||
@ -15,13 +13,10 @@ namespace CPRNIMS.Domain.Profile.Canvass
|
||||
{
|
||||
public SupplierRequestProfile()
|
||||
{
|
||||
// 1. THIS IS THE MISSING LINK: Request -> Entity
|
||||
CreateMap<SupplierRequest, Suppliers>();
|
||||
|
||||
// 2. Entity -> Response
|
||||
CreateMap<Suppliers, SupplierResponse>();
|
||||
|
||||
// 3. Response <-> Request (Use ReverseMap to handle both directions automatically)
|
||||
CreateMap<SupplierResponse, SupplierRequest>().ReverseMap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
using AutoMapper;
|
||||
using Azure.Core;
|
||||
using CPRNIMS.Domain.Services.ICanvass;
|
||||
using CPRNIMS.Infrastructure.Database;
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass;
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass.Request;
|
||||
using CPRNIMS.Infrastructure.Dto.Canvass.Response;
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
using CPRNIMS.Infrastructure.Entities.Canvass;
|
||||
using CPRNIMS.Infrastructure.Entities.Purchasing;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
@ -49,14 +48,136 @@ namespace CPRNIMS.Domain.Services.Canvass
|
||||
|
||||
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
|
||||
.FromSqlRaw($"EXEC GetCanvassPerSupplier @UserId",
|
||||
new SqlParameter("@UserId", CanvassDto.UserId))
|
||||
.ToListAsync();
|
||||
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("@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)
|
||||
{
|
||||
@ -113,27 +234,24 @@ namespace CPRNIMS.Domain.Services.Canvass
|
||||
|
||||
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}")
|
||||
.ToListAsync();
|
||||
|
||||
// 2. Map the List of entities to a List of Response objects
|
||||
// AutoMapper handles collections automatically if the types are configured
|
||||
return _mapper.Map<List<SupplierResponse>>(suppliers);
|
||||
return suppliers ?? new List<SupplierResponseDto>();
|
||||
}
|
||||
|
||||
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",
|
||||
new SqlParameter("@SupplierId", CanvassDto.SupplierId))
|
||||
.ToListAsync();
|
||||
// 2. Map the List of entities to a List of Response objects
|
||||
// AutoMapper handles collections automatically if the types are configured
|
||||
return _mapper.Map<List<SupplierResponse>>(item);
|
||||
|
||||
return items ?? new List<SupplierResponseDto>();
|
||||
}
|
||||
public async Task<List<RFQReference>> GetRFQ(CanvassDto CanvassDto)
|
||||
{
|
||||
@ -192,6 +310,15 @@ namespace CPRNIMS.Domain.Services.Canvass
|
||||
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()
|
||||
{
|
||||
var allItems = await _dbContext.ItemWithoutSuppliers
|
||||
@ -230,15 +357,15 @@ namespace CPRNIMS.Domain.Services.Canvass
|
||||
|
||||
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",
|
||||
new SqlParameter("@UserId", CanvassDto.UserId),
|
||||
new SqlParameter("@SupplierId", CanvassDto.SupplierId))
|
||||
.ToListAsync();
|
||||
|
||||
return _mapper.Map<List<SupplierResponse>>(items);
|
||||
return items ?? new List<SupplierResponseDto>();
|
||||
}
|
||||
public async Task<List<MyPRWOCanvass>> GetMyPRWOCanvass(CanvassDto itemDto)
|
||||
{
|
||||
|
||||
@ -27,10 +27,14 @@ namespace CPRNIMS.Domain.Services.Canvass
|
||||
}
|
||||
|
||||
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
|
||||
var (searchContent, supplierUrls) = await SearchTavilyAsync(itemName, itemDescription);
|
||||
var (searchContent, supplierUrls) = await SearchTavilyAsync(itemName, itemDescription, locality);
|
||||
|
||||
// Step 2: Fetch contact pages from discovered URLs
|
||||
var contactContent = await FetchContactPagesAsync(supplierUrls);
|
||||
@ -42,11 +46,11 @@ namespace CPRNIMS.Domain.Services.Canvass
|
||||
return suppliers;
|
||||
}
|
||||
|
||||
// ── Tavily ──────────────────────────────────────────────────────────────
|
||||
// ── Tavily ──
|
||||
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
|
||||
{
|
||||
@ -97,7 +101,7 @@ namespace CPRNIMS.Domain.Services.Canvass
|
||||
return (fullText, urls);
|
||||
}
|
||||
|
||||
// ── Fetch Contact Pages ──────────────────────────────────────────────────
|
||||
// ── Fetch Contact Pages ───
|
||||
private async Task<string> FetchContactPagesAsync(List<string> baseUrls)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
@ -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.PR;
|
||||
using System;
|
||||
@ -11,10 +12,10 @@ namespace CPRNIMS.Domain.UIContracts.Canvass
|
||||
{
|
||||
public interface ICanvass
|
||||
{
|
||||
#region
|
||||
Task<List<CanvassVM>> GetSupplierBid(User user, CanvassVM viewModel);
|
||||
Task<List<CanvassVM>> GetSupplierBidByItem(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>> GetItemSupplierWOEmail(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>?> GetCanvassGroupByPRNo(User user, CanvassVM viewModel);
|
||||
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> PostPutSupplier(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> PostPutItemTagging(User user, CanvassVM viewModel);
|
||||
Task<CanvassVM> UnlockFormLink(User user, CanvassVM viewModel);
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using CPRNIMS.Domain.UIContracts.Canvass;
|
||||
using CPRNIMS.Domain.UIContracts.Common;
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using CPRNIMS.Infrastructure.Models.Account;
|
||||
using CPRNIMS.Infrastructure.Models.Common;
|
||||
@ -123,6 +124,54 @@ namespace CPRNIMS.Domain.UIServices.Canvass
|
||||
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
|
||||
|
||||
#region Get
|
||||
@ -135,12 +184,7 @@ namespace CPRNIMS.Domain.UIServices.Canvass
|
||||
{
|
||||
return await SendGetApiRequest(user, viewModel,
|
||||
_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)
|
||||
{
|
||||
return await SendGetApiRequest(user, viewModel,
|
||||
@ -236,6 +280,16 @@ namespace CPRNIMS.Domain.UIServices.Canvass
|
||||
return await SendGetApiRequest(user, viewModel,
|
||||
_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
|
||||
#region Post Put
|
||||
public async Task<CanvassVM> PostCanvass(User user, CanvassVM viewModel)
|
||||
@ -288,6 +342,7 @@ namespace CPRNIMS.Domain.UIServices.Canvass
|
||||
return await SendPostApiRequest(user, viewModel,
|
||||
_configuration["LLI:NonInvent:CanvassMgmt:UnlockFormLink"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.Common;
|
||||
using CPRNIMS.Infrastructure.Entities.Finance;
|
||||
@ -65,7 +66,9 @@ namespace CPRNIMS.Infrastructure.Database
|
||||
public virtual DbSet<ForRR> ForRRs { get; set; }
|
||||
public virtual DbSet<RR> RRs { get; set; }
|
||||
public virtual DbSet<Canvass> Canvasses { get; set; }
|
||||
public DbSet<SupplierResponseDto> SupplierResponses { get; set; }
|
||||
public DbSet<SupplierItems> SupplierItems { get; set; }
|
||||
public DbSet<ItemsForTagging> ItemsForTaggings { get; set; }
|
||||
public virtual DbSet<ForCanvassFollowUp> ForCanvassFollowUps { get; set; }
|
||||
public virtual DbSet<WOResponse> WOResponses { 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<CanvassDetail> CanvassDetails { 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<ForCanvass> ForCanvasses { get; set; }
|
||||
public virtual DbSet<ForPO> ForPOs { get; set; }
|
||||
|
||||
@ -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 System;
|
||||
using System.Collections.Generic;
|
||||
@ -9,7 +10,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Canvass
|
||||
{
|
||||
public class CanvassDto : CommonProperties
|
||||
public class CanvassDto : CanvassRequestSearch
|
||||
{
|
||||
public byte PaymentTermsId { get; set; } = 0;
|
||||
public bool IsArchived { get; set; } = false;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
14
CPRNIMS.Infrastructure/Dto/Common/PagedRequest.cs
Normal file
14
CPRNIMS.Infrastructure/Dto/Common/PagedRequest.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.Common
|
||||
{
|
||||
public class PagedRequest
|
||||
{
|
||||
public int PageNumber { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
}
|
||||
20
CPRNIMS.Infrastructure/Dto/Common/PagedResult.cs
Normal file
20
CPRNIMS.Infrastructure/Dto/Common/PagedResult.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.Dto.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>();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
23
CPRNIMS.Infrastructure/Entities/Canvass/ItemsForTagging.cs
Normal file
23
CPRNIMS.Infrastructure/Entities/Canvass/ItemsForTagging.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -13,9 +13,6 @@ namespace CPRNIMS.Infrastructure.Entities.Canvass
|
||||
public int SupplierId { get; set; }
|
||||
public string? SupplierName { 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? AggreItemName { get; set; }
|
||||
public string? AggreItemNo { get; set; }
|
||||
|
||||
@ -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 System;
|
||||
using System.Collections.Generic;
|
||||
@ -8,8 +9,12 @@ using System.Threading.Tasks;
|
||||
|
||||
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 PRDetailsId { get; set; }
|
||||
public long CanvassId { get; set; }
|
||||
|
||||
@ -72,7 +72,7 @@ namespace CPRNIMS.WebApi.Controllers.Account
|
||||
company = user.Company,
|
||||
success = true,
|
||||
messCode = 1,
|
||||
message = "Yehey!"
|
||||
message = "Success"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -99,6 +99,14 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
|
||||
nameof(GetCanvassPerSupplier), false
|
||||
);
|
||||
}
|
||||
[HttpPost("GetItemsForTagging")]
|
||||
public async Task<IActionResult> GetItemsForTagging(CanvassDto itemCodeDto)
|
||||
{
|
||||
return await ExecuteWithErrorHandling(
|
||||
() => _canvass.GetItemsForTagging(itemCodeDto),
|
||||
nameof(GetItemsForTagging), false
|
||||
);
|
||||
}
|
||||
[HttpPost("GetCanvassPerSupplierId")]
|
||||
public async Task<IActionResult> GetCanvassPerSupplierId(CanvassDto itemCodeDto)
|
||||
{
|
||||
@ -454,16 +462,16 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
|
||||
public async Task<IActionResult> PostSearchSupplierAndSend(CancellationToken ct)
|
||||
{
|
||||
// #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.");
|
||||
|
||||
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
|
||||
var suppliers = await _supplierSearchService
|
||||
.SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription);
|
||||
.SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription, item.IsInternational);
|
||||
|
||||
if (!suppliers.Any())
|
||||
{
|
||||
@ -537,107 +545,6 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
|
||||
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")]
|
||||
public async Task<IActionResult> PostSuggestedSupp(CanvassDto CanvassDto)
|
||||
|
||||
@ -2,12 +2,9 @@
|
||||
using CPRNIMS.Domain.Contracts.Receiving;
|
||||
using CPRNIMS.Domain.Services;
|
||||
using CPRNIMS.Infrastructure.Dto.Items;
|
||||
using CPRNIMS.Infrastructure.Helper;
|
||||
using CPRNIMS.Infrastructure.ViewModel.Receiving;
|
||||
using CPRNIMS.WebApi.Controllers.Base;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace CPRNIMS.WebApi.Controllers.Receiving
|
||||
{
|
||||
|
||||
@ -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),
|
||||
ProjectCode VARCHAR(50) NOT NULL,
|
||||
ProjectName VARCHAR(200) NOT NULL,
|
||||
|
||||
@ -4,30 +4,47 @@
|
||||
"ValidIssuer": "https://lloydwebapi.lloydlab.com:2021",
|
||||
"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": {
|
||||
"ForgotPassword": "https://llipurchasingnoninventory.com:8080/",
|
||||
"SupplierForm": "https://llipurchasingnoninventory.com:8083/"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=194.233.78.55;Database=CPRNIMS;User Id=welladmin;Password=P@sSW0rd2024!!!;Encrypt=false;",
|
||||
"LocalPurchConn": "Server=DESKTOP-RQGHQQN;Initial Catalog=PurchasingSystem;Integrated Security=SSPI;TrustServerCertificate=True;"
|
||||
//"LocalPurchConn": "Server=172.16.19.238;Database=PurchasingSystem;User Id=lli-mdld;Password=LLi-88Kk5&/mgH]m;Encrypt=False;"
|
||||
"DefaultConnection": "Server=212.47.72.54;Database=CPRNIMS;User Id=LRMS26;Password=P@ssw0rd26;Encrypt=False;",
|
||||
"LocalPurchConn": "Server=DESKTOP-RQGHQQN;Database=PurchasingSystem;User Id=LRMS25;Password=P@ssw0rd26;Encrypt=False;"
|
||||
},
|
||||
"SMTP": {
|
||||
"DisplayName": "no-reply-cwms@lloydlab.com",
|
||||
"ToReceiver": "NA",
|
||||
"OutgoingPort": "587",
|
||||
"IncomingPort": "110",
|
||||
"SenderEmail": "cwms@lloydlab.com",
|
||||
"Password": "LLi-H@^{3bp14>4*",
|
||||
"Server": "mail.lloydlab.com",
|
||||
"SenderEmail": "lli.mis2025@gmail.com",
|
||||
"Password": "vcwq nesk rsqb zxbf",
|
||||
"Server": "smtp.gmail.com",
|
||||
"SenderName": "CWMS",
|
||||
"UserName": "cwms@lloydlab.com",
|
||||
"UserName": "lli.mis2025@gmail.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"
|
||||
},
|
||||
|
||||
"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": {
|
||||
"LogLevel": {
|
||||
|
||||
@ -59,7 +59,6 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Common\Helper\" />
|
||||
<Folder Include="Properties\NewFolder\" />
|
||||
<Folder Include="Views\Components\CanvassMgmt\" />
|
||||
<Folder Include="wwwroot\Content\Uploads\PRAttachment\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ using CPRNIMS.Infrastructure.Helper;
|
||||
using CPRNIMS.Infrastructure.ViewModel.Canvass;
|
||||
using CPRNIMS.WebApps.Controllers.Base;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CPRNIMS.Infrastructure.Dto.Common;
|
||||
|
||||
namespace CPRNIMS.WebApps.Controllers.Canvass
|
||||
{
|
||||
@ -201,11 +202,55 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
|
||||
response = await _canvass.GetSupplierItemWOEmail(GetUser(), viewModels);
|
||||
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();
|
||||
response = await _canvass.GetCanvassPerSupplier(GetUser(), viewModels);
|
||||
return GetResponse(response);
|
||||
var dto = new CanvassVM
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -306,6 +351,10 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
|
||||
}
|
||||
#endregion
|
||||
#region Views
|
||||
public IActionResult GetCanvassingTabPage(int id)
|
||||
{
|
||||
return ViewComponent("CanvassingTabPage", new { canvassingTabPageId = id });
|
||||
}
|
||||
public async Task<IActionResult> Suppliers()
|
||||
{
|
||||
return await IsAuthenTicated();
|
||||
|
||||
@ -10,8 +10,8 @@ using CPRNIMS.WebApps.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using System.Diagnostics;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Web;
|
||||
|
||||
@ -156,8 +156,8 @@ namespace CPRNIMS.WebApps.Controllers
|
||||
|
||||
DateTime expirationTime = DateTime.UtcNow.AddHours(2);
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = handler.ReadJwtToken(login.token);
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwtToken = handler.ReadJsonWebToken(login.token);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1177
CPRNIMS.WebApps/Views/CanvassMgmt/Canvass - Copy.cshtml
Normal file
1177
CPRNIMS.WebApps/Views/CanvassMgmt/Canvass - Copy.cshtml
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,36 +1,115 @@
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="table-container shadow-lg p-3 mb-3 bg-white rounded">
|
||||
<div class="header-container">
|
||||
<h2>For Canvass List Per Supplier</h2>
|
||||
@await Html.PartialAsync("PagesView/Canvass/_CanvassStyles")
|
||||
@await Html.PartialAsync("PagesView/Canvass/_CanvassHelpers")
|
||||
|
||||
<div class="canvass-wrapper">
|
||||
|
||||
@* {{-- 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>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
@ -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>`)
|
||||
*@
|
||||
@ -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>`)
|
||||
*@
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||
}
|
||||
|
||||
function escAttr(s) {
|
||||
return String(s).replace(/"/g,""").replace(/'/g,"'");
|
||||
}
|
||||
|
||||
/* ── 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>
|
||||
@ -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>
|
||||
@ -3,7 +3,7 @@
|
||||
<title>@ViewData["Title"] - LLI Purchasing Non-Inventory System</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.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 href="~/lib/font-awesome/css/all.css" rel="stylesheet" />
|
||||
<link href="~/lib/font-awesome/css/all.min.css" rel="stylesheet" />
|
||||
|
||||
@ -102,6 +102,7 @@
|
||||
"GetCanvassPerSupplierId": "api/CanvassMgmt/GetCanvassPerSupplierId/",
|
||||
"GetCanvassGroupByPRNo": "api/CanvassMgmt/GetCanvassGroupByPRNo/",
|
||||
"GetAlternativeOfferByPRDetailId": "api/CanvassMgmt/GetAlternativeOfferByPRDetailId/",
|
||||
"GetItemsForTagging": "api/CanvassMgmt/GetItemsForTagging/",
|
||||
"PostItemWOSupplierEmail": "api/CanvassMgmt/PostItemWOSupplierEmail/",
|
||||
"PostPutSupplier": "api/CanvassMgmt/PostPutSupplier/",
|
||||
"PostTaggingSupplier": "api/CanvassMgmt/PostTaggingSupplier/",
|
||||
|
||||
513
CPRNIMS.WebApps/wwwroot/css/Canvass/ForCanvass.css
Normal file
513
CPRNIMS.WebApps/wwwroot/css/Canvass/ForCanvass.css
Normal 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;
|
||||
}
|
||||
@ -60,20 +60,7 @@ input.form-control {
|
||||
table.dataTable tbody tr {
|
||||
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 {
|
||||
border-bottom: 1px solid rgba(0, 150, 136, 0.08) !important;
|
||||
padding: 12px !important;
|
||||
@ -419,7 +406,6 @@ tbody tr:last-child td:last-child {
|
||||
@media print {
|
||||
* {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
}
|
||||
}
|
||||
@ -432,3 +418,21 @@ tbody tr:last-child td:last-child {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@keyframes ftDots {
|
||||
0% {
|
||||
content: '.';
|
||||
}
|
||||
|
||||
33% {
|
||||
content: '..';
|
||||
}
|
||||
|
||||
66% {
|
||||
content: '...';
|
||||
}
|
||||
|
||||
100% {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
5
CPRNIMS.WebApps/wwwroot/lib/bootstrap-icons/font/bootstrap-icons.min.css
vendored
Normal file
5
CPRNIMS.WebApps/wwwroot/lib/bootstrap-icons/font/bootstrap-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user