Adding validaiton for existing suppliers to prevent from duplication

This commit is contained in:
rowell_m_soriano 2026-04-27 09:18:38 +08:00
parent d1c9c4b52b
commit b02af975ac
24 changed files with 1426 additions and 437 deletions

View File

@ -1,10 +1,12 @@
using CPRNIMS.Domain.Services.ICanvass;
using CPRNIMS.Domain.Services;
using CPRNIMS.Domain.Services.ICanvass;
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.ViewModel.Canvass;
using System;
using System.Collections.Generic;
using System.Linq;
@ -16,9 +18,9 @@ namespace CPRNIMS.Domain.Contracts.Canvass
public interface ICanvass : ISupplier
{
#region Post Put
Task<RFQ> PostPerSupplierToken(CanvassDto CanvassDto);
Task<Result<StartCanvassResponse>> StartCanvass(CanvassVM canvass,CancellationToken ct);
Task<RFQ> PostPerSupplierToken(ForCanvassDto CanvassDto);
Task<ForCanvassFollowUp> PutSupplierCanvass(long canvassSupplierId);
Task<PRCanvassDetail> PostCanvass(CanvassDto CanvassDto);
Task<SupplierResponse> PostPutSupplier(CanvassDto CanvassDto);
Task<SupplierResponse> PostTaggingSupplier(CanvassDto CanvassDto);
Task<SupplierResponse> PostApprovedSupp(CanvassDto CanvassDto);
@ -34,7 +36,7 @@ namespace CPRNIMS.Domain.Contracts.Canvass
Task<List<WOResponse>> GetCanvassWOResponse(CanvassDto CanvassDto);
Task<List<WOResponseById>> GetWOResponseBySuppId(CanvassDto CanvassDto);
Task<List<SupplierResponseDto>> GetSupplierById(CanvassDto CanvassDto);
Task<List<RFQReference>> GetRFQ(CanvassDto CanvassDto);
Task<List<RFQReference>> GetRFQ(ForCanvassDto CanvassDto);
Task<List<BiddingItem>> GetSupplierBid(CanvassDto CanvassDto);
Task<List<RFQPerSupplier>> GetSupplierBidByItem(CanvassDto CanvassDto);
Task<List<SupplierBidById>> GetSupplierBidById(CanvassDto CanvassDto);

View File

@ -12,10 +12,12 @@ namespace CPRNIMS.Domain.Services.ICanvass
{
public interface ISupplier
{
Task<List<ItemWithoutSupplier>> GetItemWithoutSupplier();
Task<Result<SupplierResponse>> PostSupplierAsync(SupplierRequest request, CancellationToken ct);
Task SendRFQ(SupplierEmailRequest supplierEmailRequest);
Task<bool> SearchingUpdate(long pRDetailsId);
Task<List<ForAISearchingTagging>> GetForAISearchingTagging();
Task<List<Suppliers>> GetSuppliers(CancellationToken ct);
Task<List<ForAISearchingTagging>> GetForAISearchingTagging(CancellationToken ct);
Task<List<ItemWithoutSupplier>> GetItemWithoutSupplier(CancellationToken ct);
Task<List<SupplierForCanvass>> GetSupplierForCanvass(int supplierId, string userName,CancellationToken ct);
Task<Result<SupplierResponse>> PostPutSupplierAsync(SupplierRequest request, CancellationToken ct);
Task<bool> SendRFQ(SupplierEmailRequest supplierEmailRequest);
Task DeleteAsync(long pRDetailsId, CancellationToken ct);
}
}

View File

@ -1,4 +1,5 @@
using CPRNIMS.Infrastructure.Dto.PO;
using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Dto.PO;
using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Entities.Common;
using CPRNIMS.Infrastructure.Entities.LocalDb.NonInvent;
@ -37,7 +38,7 @@ namespace CPRNIMS.Domain.Contracts.PO
Task<List<SystemSettings>> GetLatestPO2(PODto pODto);
Task<List<DocRequired>> GetDocRequired(PODto pODto);
Task<List<OtherCharges>> GetOtherCharges(PODto itemDto);
Task<List<Suppliers>> GetSuppliers(PODto itemDto);
Task<List<SupplierResponseDto>> GetSuppliers(PODto itemDto);
Task<List<CreatedPO>> GetCreatedPO(PODto pODto);
Task<List<POItemDetail>> GetPOItemDetail(PODto pODto);
Task<List<CreatedPO>> GetMyCreatedPO(PODto pODto);

View File

@ -14,10 +14,11 @@ namespace CPRNIMS.Domain.Profile.Canvass
public SupplierRequestProfile()
{
CreateMap<SupplierRequest, Suppliers>();
CreateMap<Suppliers, SupplierResponse>();
CreateMap<SupplierResponse, SupplierRequest>().ReverseMap();
CreateMap<StartCanvassRequest, ForAISearchingTagging>();
CreateMap<ForAISearchingTagging, StartCanvassResponse>().ReverseMap();
}
}
}

View File

@ -1,4 +1,5 @@
using AutoMapper;
using CPRNIMS.Domain.Services.ICanvass;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Canvass;
using CPRNIMS.Infrastructure.Dto.Canvass.Request;
@ -7,10 +8,10 @@ using CPRNIMS.Infrastructure.Dto.Common;
using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Entities.Purchasing;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.ViewModel.Canvass;
using CPRNIMS.Infrastructure.ViewModel.Common;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.Data;
using System.Text;
using System.Threading.Tasks;
@ -30,6 +31,14 @@ namespace CPRNIMS.Domain.Services.Canvass
_mapper = mapper;
}
#region Get
public async Task<List<Suppliers>> GetSuppliers(CancellationToken ct)
{
return await _dbContext.Suppliers
.AsNoTracking()
.Where(s => s.IsActive)
.ToListAsync(ct);
}
public async Task<List<ItemListWOEmail>> GetItemSupplierWOEmail(CanvassDto CanvassDto)
{
var allItems = await _dbContext.ItemListWOEmails
@ -97,6 +106,7 @@ namespace CPRNIMS.Domain.Services.Canvass
ItemNo = Convert.ToInt64(reader["ItemNo"]),
ItemName = reader["ItemName"]?.ToString(),
ItemDescription = reader["ItemDescription"]?.ToString(),
Qty = Convert.ToDecimal(reader["Qty"]),
Department = reader["Department"]?.ToString(),
CreatedBy = reader["CreatedBy"]?.ToString(),
CreatedDate =Convert.ToDateTime(reader["CreatedDate"]),
@ -253,7 +263,7 @@ namespace CPRNIMS.Domain.Services.Canvass
return items ?? new List<SupplierResponseDto>();
}
public async Task<List<RFQReference>> GetRFQ(CanvassDto CanvassDto)
public async Task<List<RFQReference>> GetRFQ(ForCanvassDto CanvassDto)
{
var allItems = await _dbContext.RFQReferences
.FromSqlRaw($"EXEC GetRFQPerSupplier @SupplierId,@UserId",
@ -310,23 +320,34 @@ namespace CPRNIMS.Domain.Services.Canvass
return 0;
}
}
public async Task<List<ForAISearchingTagging>> GetForAISearchingTagging()
public async Task<List<ForAISearchingTagging>> GetForAISearchingTagging(CancellationToken ct)
{
var allItems = await _dbContext.ForAISearchingTaggings
.AsNoTracking()
.Take(10)
.ToListAsync();
.Take(1)
.ToListAsync(ct);
return allItems ?? new List<ForAISearchingTagging>();
}
public async Task<List<ItemWithoutSupplier>> GetItemWithoutSupplier()
public async Task<List<ItemWithoutSupplier>> GetItemWithoutSupplier(CancellationToken ct)
{
var allItems = await _dbContext.ItemWithoutSuppliers
.FromSqlRaw("EXEC GetItemWithoutSupplier")
.ToListAsync();
.ToListAsync(ct);
return allItems ?? new List<ItemWithoutSupplier>();
}
public async Task<List<SupplierForCanvass>> GetSupplierForCanvass(int supplierId,string userName,
CancellationToken ct)
{
var allItems = await _dbContext.SupplierForCanvass
.FromSqlRaw("EXEC GetSupplierForCanvass @UserName,@SupplierId",
new SqlParameter("@UserName", userName),
new SqlParameter("@SupplierId", supplierId)
).AsNoTracking().ToListAsync(ct);
return allItems ?? new List<SupplierForCanvass>();
}
public async Task<List<WOResponseById>> GetWOResponseBySuppId(CanvassDto CanvassDto)
{
var allItems = await _dbContext.WOResponseByIds
@ -335,7 +356,7 @@ namespace CPRNIMS.Domain.Services.Canvass
return allItems ?? new List<WOResponseById>();
}
public async Task<RFQ> PostPerSupplierToken(CanvassDto CanvassDto)
public async Task<RFQ> PostPerSupplierToken(ForCanvassDto CanvassDto)
{
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPerSupplierToken @UserId,@SupplierId,@PRNo,@ItemNo,@CanvassNo,@PRDetailsId",
@ -438,7 +459,7 @@ namespace CPRNIMS.Domain.Services.Canvass
}
#endregion
#region Post Put
public async Task<Result<SupplierResponse>> PostSupplierAsync(SupplierRequest request, CancellationToken ct)
public async Task<Result<SupplierResponse>> PostPutSupplierAsync(SupplierRequest request, CancellationToken ct)
{
var strategy = _dbContext.Database.CreateExecutionStrategy();
@ -449,7 +470,7 @@ namespace CPRNIMS.Domain.Services.Canvass
try
{
var supplier = await _dbContext.Suppliers
.FirstOrDefaultAsync(s => s.SupplierName == request.SupplierName, ct);
.FirstOrDefaultAsync(s => s.SupplierId == request.SupplierId, ct);
if (supplier == null)
{
@ -469,8 +490,6 @@ namespace CPRNIMS.Domain.Services.Canvass
catch (DbUpdateException ex)
{
await transaction.RollbackAsync(ct);
// handle unique constraint violation here if needed
throw;
}
});
@ -512,16 +531,6 @@ namespace CPRNIMS.Domain.Services.Canvass
new SqlParameter("@CanvassId", CanvassDto.CanvassId));
return new CanvassDetail();
}
public async Task<PRCanvassDetail> PostCanvass(CanvassDto CanvassDto)
{
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostCanvass @UserId, @SupplierId, @Status,@Remarks",
new SqlParameter("@SupplierId", CanvassDto.SupplierId != null ? CanvassDto.SupplierId : 0L),
new SqlParameter("@UserId", CanvassDto.UserId),
new SqlParameter("@Status", CanvassDto.Status),
new SqlParameter("@Remarks", CanvassDto.Remarks ?? "N/A"));
return new PRCanvassDetail();
}
public async Task<SupplierResponse> PostTaggingSupplier(CanvassDto CanvassDto)
{
var (messCode, message) = CreateOutputParams();
@ -674,8 +683,7 @@ namespace CPRNIMS.Domain.Services.Canvass
new SqlParameter("@CanvassSupplierId", canvassDto.CanvassSupplierId));
return new CanvassSupplier();
}
public async Task SendRFQ(SupplierEmailRequest supplierEmailRequest)
public async Task<bool> SendRFQ(SupplierEmailRequest supplierEmailRequest)
{
var baseTemplate = EMailTemplate("Content\\SMTPEmailContent", "SendToSupplier.cshtml");
@ -702,7 +710,9 @@ namespace CPRNIMS.Domain.Services.Canvass
AttachPath=supplierEmailRequest.AttachPath
};
await _smptHelper.SendEmailAsync(messageDetails);
if (await _smptHelper.SendEmailAsync(messageDetails))
return true;
return false;
}
public string EMailTemplate(string relativePath, string emailTemplate)
{
@ -720,14 +730,61 @@ namespace CPRNIMS.Domain.Services.Canvass
return "Template file not found";
}
}
public async Task<bool> SearchingUpdate(long pRDetailsId)
public async Task DeleteAsync(long pRDetailsId, CancellationToken ct)
{
var rowsAffected = await _dbContext.PRDetails
await _dbContext.ForAISearchingTaggings
.Where(p => p.PRDetailsId == pRDetailsId)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.IsSearched, true));
.ExecuteDeleteAsync(ct);
}
return rowsAffected > 0;
public async Task<Result<StartCanvassResponse>> StartCanvass(CanvassVM request, CancellationToken ct)
{
var detailIds = request.ForSupplierSearchList?.PRDetailsId;
if (detailIds == null || !detailIds.Any())
return Result<StartCanvassResponse>.Failure("No items provided.");
// 1. Get IDs that already exist in one call to avoid the loop-check
var existingIds = await _dbContext.ForAISearchingTaggings
.Where(f => detailIds.Contains(f.PRDetailsId))
.Select(f => f.PRDetailsId)
.ToListAsync(ct);
var newTags = new List<ForAISearchingTagging>();
var idsToUpdate = new List<long>();
for (int i = 0; i < detailIds.Count; i++)
{
var currentId = detailIds[i];
if (existingIds.Contains(currentId)) continue;
newTags.Add(new ForAISearchingTagging
{
PRDetailsId = currentId,
PRNo = request.ForSupplierSearchList.PRNo[i],
ItemNo = request.ForSupplierSearchList.ItemNo[i],
ItemName = request.ForSupplierSearchList.ItemName[i],
ItemDescription = request.ForSupplierSearchList.ItemDescription[i],
IsInternational = request.IsInternational,
FullName = request.FullName,
UserId = request.UserId
});
idsToUpdate.Add(currentId);
}
if (newTags.Any())
{
// 2. Bulk Add
await _dbContext.ForAISearchingTaggings.AddRangeAsync(newTags, ct);
await _dbContext.SaveChangesAsync(ct);
// 3. Bulk Update the flags AFTER saving the tags
await _dbContext.PRDetails
.Where(p => idsToUpdate.Contains(p.PRDetailsId))
.ExecuteUpdateAsync(s => s.SetProperty(p => p.IsSearched, true), ct);
}
return Result<StartCanvassResponse>.Success(new StartCanvassResponse { });
}
#endregion
}

View File

@ -3,9 +3,11 @@ using CPRNIMS.Infrastructure.Dto.Canvass.Result;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -19,7 +21,125 @@ namespace CPRNIMS.Domain.Services.Canvass
// Common contact page suffixes to try
private static readonly string[] ContactPaths =
{ "/contact", "/contact-us", "/pages/contact-us", "/about/contact", "/about" };
/// <summary>
/// Uses Groq to fuzzy-match a new supplier against existing ones.
/// Handles rebranding, spacing in phone numbers, name variations, etc.
/// Returns the matched existing SupplierId, or null if no match.
/// </summary>
public async Task<int?> FindMatchingExistingSupplierAsync(
SupplierResponse incoming,
List<SupplierResponse> existingSuppliers)
{
if (!existingSuppliers.Any()) return null;
// ── Layer 1: Exact C# match (fast, free, no API call) ──────────────
var incomingEmail = (incoming.EmailAddress ?? "").Trim().ToLower();
var incomingPhone = NormalizePhone(incoming.ContactNo ?? "");
var incomingDomain = ExtractDomain(incoming.Website ?? "");
foreach (var s in existingSuppliers)
{
var existEmail = (s.EmailAddress ?? "").Trim().ToLower();
var existPhone = NormalizePhone(s.ContactNo ?? "");
var existDomain = ExtractDomain(s.Website ?? "");
if (!string.IsNullOrEmpty(incomingEmail) && incomingEmail == existEmail)
return s.SupplierId;
if (!string.IsNullOrEmpty(incomingPhone) && incomingPhone == existPhone)
return s.SupplierId;
if (!string.IsNullOrEmpty(incomingDomain) && incomingDomain == existDomain)
return s.SupplierId;
}
// ── Layer 2: Fuzzy C# pre-filter — narrow to top candidates ────────
var incomingName = (incoming.SupplierName ?? "").ToLower();
var candidates = existingSuppliers
.Where(s =>
{
var name = (s.SupplierName ?? "").ToLower();
// Keep if first word matches (e.g. "Linde" in "Linde PH" vs "Linde Philippines")
var incomingFirstWord = incomingName.Split(' ').FirstOrDefault() ?? "";
var existFirstWord = name.Split(' ').FirstOrDefault() ?? "";
return !string.IsNullOrEmpty(incomingFirstWord)
&& incomingFirstWord.Length > 2 // ignore short words like "co", "ph"
&& existFirstWord.StartsWith(incomingFirstWord, StringComparison.OrdinalIgnoreCase);
})
.Take(5) // max 5 candidates to Groq — well within token limit
.Select(s => new
{
s.SupplierId,
s.SupplierName,
s.EmailAddress,
s.ContactNo,
s.Website
})
.ToList();
// No fuzzy candidates found — it's a new supplier
if (!candidates.Any()) return null;
// ── Layer 3: Groq fuzzy match — only on small candidate list ────────
var incomingJson = JsonSerializer.Serialize(new
{
incoming.SupplierName,
incoming.EmailAddress,
incoming.ContactNo,
incoming.Website
});
var candidatesJson = JsonSerializer.Serialize(candidates);
var prompt =
"TASK: Determine if the INCOMING supplier already exists in the CANDIDATES list.\n\n" +
"MATCHING RULES (any one is enough):\n" +
"1. Same email address (case-insensitive).\n" +
"2. Same phone number after stripping spaces, dashes, country codes.\n" +
"3. Same company despite rebranding, abbreviation, or spacing differences.\n" +
"4. Same website domain (ignore www, http/https).\n\n" +
"If matched: respond ONLY { \"matched\": true, \"supplierId\": <number> }\n" +
"If not matched: respond ONLY { \"matched\": false, \"supplierId\": null }\n" +
"No explanation. No markdown. JSON only.\n\n" +
$"INCOMING:\n{incomingJson}\n\n" +
$"CANDIDATES:\n{candidatesJson}";
var payload = new
{
model = _config["Groq:Model"] ?? "llama-3.1-8b-instant",
stream = false,
max_tokens = 50,
temperature = 0,
messages = new[]
{
new { role = "system", content = "You are a supplier deduplication engine. Return ONLY valid JSON. No markdown. No explanation." },
new { role = "user", content = prompt }
}
};
var request = new HttpRequestMessage(HttpMethod.Post, _config["Groq:ApiUrl"]);
request.Headers.Add("Authorization", $"Bearer {_config["Groq:ApiKey"]}");
request.Content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
var groqResp = JsonSerializer.Deserialize<GroqResponse>(body,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var rawText = groqResp?.Choices?[0]?.Message?.Content ?? string.Empty;
rawText = Regex.Replace(rawText, @"```[a-z]*|```", "").Trim();
var match = JsonSerializer.Deserialize<GroqMatchResult>(rawText,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return match?.Matched == true ? match.SupplierId : null;
}
public SupplierSearchService(HttpClient httpClient, IConfiguration config)
{
_httpClient = httpClient;
@ -29,9 +149,9 @@ namespace CPRNIMS.Domain.Services.Canvass
public async Task<List<SupplierResponse>> SearchAndFilterSuppliersAsync(
string itemName, string itemDescription, bool isInternational)
{
string locality = "Philippines";
if (isInternational) { locality = "all over the asia including Philippines"; }
else { locality = "Philippines"; }
var locality = isInternational
? "all over Asia including Philippines"
: "Philippines";
// Step 1: Tavily — get supplier URLs
var (searchContent, supplierUrls) = await SearchTavilyAsync(itemName, itemDescription, locality);
@ -41,7 +161,7 @@ namespace CPRNIMS.Domain.Services.Canvass
// Step 3: Combine search + contact content, send to Groq
var combined = searchContent + " CONTACT_PAGES_DATA: " + contactContent;
var suppliers = await FilterWithGroqAsync(itemName, itemDescription, combined);
var suppliers = await FilterWithGroqAsync(itemName, itemDescription, combined,isInternational);
return suppliers;
}
@ -49,6 +169,8 @@ namespace CPRNIMS.Domain.Services.Canvass
// ── Tavily ──
private async Task<(string content, List<string> urls)> SearchTavilyAsync(
string itemName, string itemDescription,string locality)
{
try
{
var query = $"{itemName} {itemDescription} suppliers {locality} budget price contact email phone";
@ -100,6 +222,13 @@ namespace CPRNIMS.Domain.Services.Canvass
return (fullText, urls);
}
catch (Exception ex)
{
ex.ToString();
throw;
}
}
// ── Fetch Contact Pages ───
private async Task<string> FetchContactPagesAsync(List<string> baseUrls)
@ -152,16 +281,42 @@ namespace CPRNIMS.Domain.Services.Canvass
// ── Groq ─────────────────────────────────────────────────────────────────
private async Task<List<SupplierResponse>> FilterWithGroqAsync(
string itemName, string itemDescription, string searchContent)
string itemName, string itemDescription, string searchContent, bool isInternational)
{
var prompt = $"Extract top 10 unique suppliers for: {itemName} {itemDescription}. " +
"Prioritize Philippines suppliers first. " +
"IMPORTANT: Look carefully in CONTACT_PAGES_DATA section for real phone numbers and emails. " +
"Extract exact email addresses and phone numbers found. " +
"For domains without contact data found, infer email as sales@domain or info@domain. " +
"Prefer budget-friendly suppliers. No duplicates. " +
"Return ONLY a raw JSON array: company_name, country, phone_number, contact_email, website, estimated_price_usd, item_specifications. " +
$"Null for missing. JSON array only. Data: {searchContent}";
try
{
var localityRule = isInternational
? "1. Include suppliers from Philippines first, then other Asian countries (e.g. China, Japan, South Korea, Taiwan, India, Singapore).\n"
: "1. STRICT: Include ONLY suppliers based in the Philippines. Exclude ANY supplier from other countries — even if they ship to Philippines. If a supplier's country is not Philippines, skip it entirely.\n";
var prompt =
$"TASK: Extract up to 10 unique suppliers that sell: [{itemName}] — {itemDescription}.\n\n" +
"RULES:\n" +
localityRule +
"2. Prefer budget-friendly suppliers with known pricing.\n" +
"3. DEDUPLICATION (strict): Each entry must have a unique company_name, contact_email, AND phone_number.\n" +
" - If two entries share the same email OR phone number, keep only the first.\n" +
" - If two inferred emails resolve to the same address, keep only one.\n" +
"4. CONTACT EXTRACTION:\n" +
" - Look in the CONTACT_PAGES_DATA section for real emails and phone numbers.\n" +
" - Use exact values found. Do not fabricate contact details.\n" +
" - If no email is found for a domain, infer: sales@domain.com or info@domain.com.\n" +
" - If no phone is found, use null — do not guess.\n" +
"5. estimated_price_usd MUST be a number (e.g. 12.50) or null. NEVER a string.\n" +
"6. Exclude any supplier with no company_name or no contact_email.\n\n" +
"OUTPUT FORMAT:\n" +
"Return ONLY a valid raw JSON array. No markdown. No explanation. No extra text.\n" +
"Each object must have exactly these fields:\n" +
" company_name (string)\n" +
" country (string)\n" +
" phone_number (string | null)\n" +
" contact_email (string | null)\n" +
" website (string | null)\n" +
" estimated_price_usd (number | null)\n" +
" item_specifications (string | null)\n\n" +
$"DATA:\n{searchContent}";
var payload = new
{
@ -193,20 +348,52 @@ namespace CPRNIMS.Domain.Services.Canvass
var match = Regex.Match(rawText, @"\[[\s\S]*\]");
if (!match.Success) return new List<SupplierResponse>();
var groqList = JsonSerializer.Deserialize<List<GroqSupplierResult>>(match.Value,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
// Add the converter to the shared options
var jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters = { new FlexibleDecimalConverter() }
};
var groqList = JsonSerializer.Deserialize<List<GroqSupplierResult>>(match.Value, jsonOptions)
?? new List<GroqSupplierResult>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenEmails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenPhones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var suppliers = new List<SupplierResponse>();
var allowedCountries = isInternational
? new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Philippines", "China", "Japan", "South Korea", "Taiwan",
"India", "Singapore", "Malaysia", "Thailand", "Vietnam",
"Indonesia", "Hong Kong"
}
: new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Philippines"
};
foreach (var s in groqList)
{
var key = (s.CompanyName ?? "").Trim().ToLower();
if (string.IsNullOrEmpty(key) || seen.Contains(key)) continue;
seen.Add(key);
var email = (s.ContactEmail ?? "").Trim().ToLower();
var phone = NormalizePhone(s.PhoneNumber ?? "");
if (string.IsNullOrEmpty(s.ContactEmail)) continue;
// Skip if no company name
if (string.IsNullOrEmpty(key)) continue;
// Skip if no email
if (string.IsNullOrEmpty(email)) continue;
// ✅ Skip if company name, email, OR phone already seen
if (seenNames.Contains(key)) continue;
if (seenEmails.Contains(email)) continue;
if (!string.IsNullOrEmpty(phone) && seenPhones.Contains(phone)) continue;
seenNames.Add(key);
seenEmails.Add(email);
if (!string.IsNullOrEmpty(phone)) seenPhones.Add(phone);
suppliers.Add(new SupplierResponse
{
@ -231,5 +418,36 @@ namespace CPRNIMS.Domain.Services.Canvass
return suppliers;
}
catch (Exception ex)
{
ex.ToString();
throw;
}
}
private static string NormalizePhone(string phone)
{
if (string.IsNullOrWhiteSpace(phone)) return string.Empty;
// Strip everything except digits
var digits = Regex.Replace(phone, @"\D", "");
// Remove leading country code "1" for US/CA numbers (11 digits starting with 1)
if (digits.Length == 11 && digits.StartsWith("1"))
digits = digits[1..];
return digits;
}
private static string ExtractDomain(string url)
{
if (string.IsNullOrWhiteSpace(url)) return string.Empty;
try
{
if (!url.StartsWith("http")) url = "https://" + url;
var host = new Uri(url).Host;
return host.StartsWith("www.") ? host[4..] : host;
}
catch { return string.Empty; }
}
}
}

View File

@ -1,5 +1,6 @@
using CPRNIMS.Domain.Contracts.PO;
using CPRNIMS.Infrastructure.Database;
using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Dto.PO;
using CPRNIMS.Infrastructure.Dto.PR;
using CPRNIMS.Infrastructure.Entities.Canvass;
@ -73,15 +74,15 @@ namespace CPRNIMS.Domain.Services.PO
var charges = await _dbContext.OtherCharges.ToListAsync();
return charges;
}
public async Task<List<Suppliers>> GetSuppliers(PODto itemDto)
public async Task<List<SupplierResponseDto>> GetSuppliers(PODto itemDto)
{
var allItems = await _dbContext.Suppliers
var allItems = await _dbContext.SupplierResponses
.FromSqlRaw($"EXEC GetSuppliers @UserId,@SupplierName",
new SqlParameter("@UserId", itemDto.UserId),
new SqlParameter("@SupplierName", itemDto.SupplierName))
.ToListAsync();
return allItems ?? new List<Suppliers>();
return allItems ?? new List<SupplierResponseDto>();
}
public async Task<List<PRWOCanvass>> GetPRWOCanvass(PODto itemDto)
{

View File

@ -39,6 +39,7 @@ namespace CPRNIMS.Domain.UIContracts.Canvass
#endregion
#region Post Put
Task<CanvassVM> PostSupplierForCanvass(User user, CanvassVM viewModel);
Task<CanvassVM> PostCanvass(User user, CanvassVM viewModel);
Task<CanvassVM> PostPutSupplier(User user, CanvassVM viewModel);
Task<CanvassVM> PostTaggingSupplier(User user, CanvassVM viewModel);
@ -49,6 +50,7 @@ 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);
Task<CanvassVM> StartCanvass(User user, CanvassVM viewModel);
#endregion
}
}

View File

@ -343,6 +343,18 @@ namespace CPRNIMS.Domain.UIServices.Canvass
_configuration["LLI:NonInvent:CanvassMgmt:UnlockFormLink"]);
}
public async Task<CanvassVM> StartCanvass(User user, CanvassVM viewModel)
{
return await SendPostApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:StartCanvass"]);
}
public async Task<CanvassVM> PostSupplierForCanvass(User user, CanvassVM viewModel)
{
return await SendPostApiRequest(user, viewModel,
_configuration["LLI:NonInvent:CanvassMgmt:PostSupplierForCanvass"]);
}
#endregion
}
}

View File

@ -66,6 +66,7 @@ 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<SupplierForCanvass> SupplierForCanvass { get; set; }
public DbSet<SupplierResponseDto> SupplierResponses { get; set; }
public DbSet<SupplierItems> SupplierItems { get; set; }
public DbSet<ItemsForTagging> ItemsForTaggings { get; set; }

View File

@ -19,5 +19,6 @@ namespace CPRNIMS.Infrastructure.Entities.Canvass
public DateTime DateNeeded { get; set; }
public DateTime CreatedDate { get; set; }
public string? CreatedBy { get; set; }
public decimal Qty { get; set; }
}
}

View File

@ -0,0 +1,20 @@
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 SupplierForCanvass
{
[Key]
public long PRDetailsId { get; set; }
public int SupplierId { get; set; }
public string? EmailAddress { get; set; }
public long PRNo { get; set; }
public long ItemNo { get; set; }
public string? ItemName { get; set; }
}
}

View File

@ -14,16 +14,16 @@ namespace CPRNIMS.Infrastructure.Entities.Canvass
[Key]
public int SupplierId { get; set; }
public string SupplierName { get; set; } = string.Empty;
public string EmailAddress { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public string ContactNo { get; set; } = string.Empty;
public string ContactPerson { get; set; } = string.Empty;
public string LeadTime { get; set; } = string.Empty;
public bool IsVatable { get; set; }=false;
public byte PaymentTermsId { get; set; } = 1;
public byte CurrencyId { get; set; } = 1;
public string TinNo { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string Website { get; set; } = string.Empty;
public string? EmailAddress { get; set; }
public bool IsActive { get; set; }
public string? ContactNo { get; set; }
public string? ContactPerson { get; set; }
public string? LeadTime { get; set; }
public bool? IsVatable { get; set; }
public byte? PaymentTermsId { get; set; }
public byte? CurrencyId { get; set; }
public string? TinNo { get; set; }
public string? Address { get; set; }
public string? Website { get; set; }
}
}

View File

@ -85,6 +85,7 @@ namespace CPRNIMS.Infrastructure.ViewModel.Canvass
public SupplierList? SupplierList { get; set; }
public CanvassList? CanvassList { get; set; }
public ItemList? ItemList { get; set; }
public ForSupplierSearchList? ForSupplierSearchList { get; set; }
public string? ItemName { get; set; }
public string? URL { get; set; }
public string? Token { get; set; }
@ -113,5 +114,6 @@ namespace CPRNIMS.Infrastructure.ViewModel.Canvass
public bool IsApproved { get; set; }
public string? Description { get; set; }
public long AlternativeId { get; set; }
public bool IsInternational { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.ViewModel.Canvass
{
public class ForSupplierSearchList
{
public List<long>? PRDetailsId { get; set; }
public List<long>? PRNo { get; set; }
public List<long>? ItemNo { get; set; }
public List<string>? ItemName { get; set; }
public List<string>? ItemDescription { get; set; }
}
}

View File

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.ViewModel.Canvass
{
public class XCheckItemVM
{
public List<long>? ItemNo { get; set; }
}
}

View File

@ -5,9 +5,11 @@ using CPRNIMS.Domain.Services.Canvass;
using CPRNIMS.Infrastructure.Dto.Canvass;
using CPRNIMS.Infrastructure.Dto.Canvass.Request;
using CPRNIMS.Infrastructure.Dto.Canvass.Response;
using CPRNIMS.Infrastructure.Entities.Canvass;
using CPRNIMS.Infrastructure.Helper;
using CPRNIMS.Infrastructure.ViewModel.Canvass;
using CPRNIMS.Infrastructure.ViewModel.Common;
using CPRNIMS.WebApi.Security;
using Microsoft.AspNetCore.Mvc;
using System.Text;
@ -214,31 +216,92 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
#endregion
#region Post Put
[HttpPost("PostSupplierForCanvass")]
public async Task<IActionResult> PostSupplierForCanvass(CanvassVM canvass, CancellationToken ct)
{
var currentUser = User.ToUserClaims();
if (currentUser == null)
return Unauthorized();
var results = await _canvass.GetSupplierForCanvass(canvass.SupplierId, currentUser.UserName, ct);
var baseTemplate = EMailTemplate("Content\\SMTPEmailContent", "SendToSupplier.cshtml");
var CanvassDto = new ForCanvassDto();
int canvassNo = await _canvass.GetCanvassNo();
foreach (var item in results)
{
CanvassDto = new ForCanvassDto
{
PRDetailsId = item.PRDetailsId,
PRNo = item.PRNo,
ItemNo = item.ItemNo,
SupplierId = item.SupplierId,
UserId = currentUser.UserId,
FullName = currentUser.FullName,
CanvassNo = canvassNo + 1,
};
await _canvass.PostPerSupplierToken(CanvassDto);
}
var rfq = await _canvass.GetRFQ(CanvassDto);
var message = new StringBuilder(baseTemplate);
message.Replace("@ViewBag.FormLink", Convert.ToString(_configuration["WebEndPoint:SupplierForm"] + rfq[0].Token));
message.Replace("@ViewBag.Supplier", rfq[0].SupplierName);
message.Replace("@ViewBag.Signature", currentUser.FullName);
var messageDetails = new EmailMessageDetailsVM();
messageDetails.AttachPath = GetRelativePath(@"Content\Documents\Pdf\Offer_Submission_Procedure.pdf");
messageDetails.Recipient = rfq[0].EmailAddress;
messageDetails.Message = message.ToString();
messageDetails.Subject = "CLMS - Request For Quotation #PRNo: " + rfq[0].AggrePRNo;
messageDetails.CC = Convert.ToString(_configuration["Canvass:CC"]);
messageDetails.SenderEmail = _config["SMTP:SenderEmail"];
messageDetails.DisplayName = "lloydlabinc.com";
messageDetails.NewPassword = _config["SMTP:Password"];
messageDetails.OutGoingPort = 587;
messageDetails.Server = _config["SMTP:Server"];
messageDetails.UserName = _config["SMTP:UserName"];
messageDetails.IsSuccess = false;
messageDetails.IsCanvass = true;
await _smtpHelper.SendEmailAsync(messageDetails);
return Ok(new {success=true, data = rfq, messCode=1});
}
[HttpPost("StartCanvass")]
public async Task<IActionResult> StartCanvass([FromBody] CanvassVM canvass,CancellationToken ct)
{
var result = await _canvass.StartCanvass(canvass,ct);
if (!result.IsSuccess)
{
return BadRequest(result.Error);
}
return Ok(new { result, messCode = 1 });
}
[HttpPost("PostAllCanvass")]
public async Task<IActionResult> PostAllCanvass()
{
try
{
var baseTemplate = EMailTemplate("Content\\SMTPEmailContent", "SendToSupplier.cshtml");
// Get all canvass items
var allCanvass = await _canvass.GetAllForCanvass();
// ✅ Group all items by supplier
var groupedBySupplier = allCanvass
.GroupBy(x => x.SupplierId)
.ToList();
// Process each supplier group
foreach (var supplierGroup in groupedBySupplier)
{
var firstItem = supplierGroup.First();
int canvassNo = await _canvass.GetCanvassNo();
// Insert all items for this supplier
foreach (var rfqq in supplierGroup)
{
var canvass = new CanvassDto
var canvass = new ForCanvassDto
{
PRDetailsId = rfqq.PRDetailsId,
PRNo = rfqq.PRNo,
@ -252,8 +315,7 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
await _canvass.PostPerSupplierToken(canvass);
}
// ✅ After inserting all items for this supplier, retrieve RFQ info
var canvassInfo = new CanvassDto
var canvassInfo = new ForCanvassDto
{
SupplierId = firstItem.SupplierId,
UserId = firstItem.UserId,
@ -265,13 +327,11 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
if (rfq?.Any() == true)
{
// ✅ Prepare email body
var message = new StringBuilder(baseTemplate);
message.Replace("@ViewBag.FormLink", Convert.ToString(_configuration["WebEndPoint:SupplierForm"] + rfq[0].Token));
message.Replace("@ViewBag.Supplier", rfq[0].SupplierName);
message.Replace("@ViewBag.Signature", firstItem.FullName);
// ✅ Prepare email details
var messageDetails = new EmailMessageDetailsVM
{
AttachPath = GetRelativePath(@"Content\Documents\Pdf\Offer_Submission_Procedure.pdf"),
@ -290,21 +350,11 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
};
await _smtpHelper.SendEmailAsync(messageDetails);
// ✅ Post canvass record after successful send
await _canvass.PostCanvass(canvassInfo);
}
}
return Ok(new { message = "All canvass emails sent successfully." });
}
catch (Exception ex)
{
var errorMessage = ex.InnerException?.ToString() ?? ex.Message.ToString();
await PostErrorMessage(errorMessage, "WebApi");
throw;
}
}
[HttpPost("PostCanvassFollowUp")]
public async Task<IActionResult> PostCanvassFollowUp(CanvassDto itemDto)
@ -412,13 +462,14 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
{
var baseTemplate = EMailTemplate("Content\\SMTPEmailContent", "SendToSupplier.cshtml");
var CanvassDto = new CanvassDto();
var CanvassDto = new ForCanvassDto();
int canvassNo = await _canvass.GetCanvassNo();
foreach (var itemCartId in canvassVM.CanvassList.PRDetailsId)
{
var index = canvassVM.CanvassList.PRDetailsId.IndexOf(itemCartId);
CanvassDto = new CanvassDto
CanvassDto = new ForCanvassDto
{
PRDetailsId = canvassVM.CanvassList.PRDetailsId[index],
PRNo = canvassVM.CanvassList.PRNo[index],
@ -430,6 +481,7 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
};
await _canvass.PostPerSupplierToken(CanvassDto);
}
var rfq = await _canvass.GetRFQ(CanvassDto);
var message = new StringBuilder(baseTemplate);
@ -454,48 +506,83 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
messageDetails.IsCanvass = true;
await _smtpHelper.SendEmailAsync(messageDetails);
var pR = await _canvass.PostCanvass(CanvassDto);
return Ok(pR);
return Ok(rfq);
}
[HttpPost("PostSearchSupplierAndSend")]
public async Task<IActionResult> PostSearchSupplierAndSend(CancellationToken ct)
{
// #1 Get top 10 items without suppliers — must process all
var response = await _canvass.GetForAISearchingTagging();
if (response == null || !response.Any()) return BadRequest("No items found.");
var items = await _canvass.GetForAISearchingTagging(ct);
if (items == null || !items.Any())
return BadRequest("No items found.");
var rawSuppliers = await _canvass.GetSuppliers(ct) ?? new List<Suppliers>();
var existingSuppliers = _mapper.Map<List<SupplierResponse>>(rawSuppliers);
var supplierResults = new List<object>();
foreach (var item in response)
foreach (var item in items)
{
try
{
var results = await ProcessItemAsync(item, existingSuppliers, ct);
supplierResults.AddRange(results);
}
catch (Exception) { }
}
return Ok(new { totalProcessed = supplierResults.Count, suppliers = supplierResults });
}
private async Task<List<object>> ProcessItemAsync(
ForAISearchingTagging item,
List<SupplierResponse> existingSuppliers,
CancellationToken ct)
{
// #2 Search Tavily + Filter with Groq
var suppliers = await _supplierSearchService
.SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription, item.IsInternational);
if (!suppliers.Any())
{
await _canvass.SearchingUpdate(item.PRDetailsId);
continue;
await _canvass.DeleteAsync(item.PRDetailsId, ct);
return new List<object>();
}
// #3 & #4 Loop each found supplier
var results = new List<object>();
int canvassNo = await _canvass.GetCanvassNo();
bool anySuccess = false;
bool isProd = Convert.ToBoolean(_configuration["SMTP:IsLive"]);
foreach (var supplier in suppliers)
{
int canvassNo = await _canvass.GetCanvassNo();
try
{
int supplierId;
var matchedId = await _supplierSearchService
.FindMatchingExistingSupplierAsync(supplier, existingSuppliers);
if (matchedId.HasValue)
{
supplierId = matchedId.Value;
}
else
{
var supplierRequest = _mapper.Map<SupplierRequest>(supplier);
supplierRequest.ItemNo = item.ItemNo;
var result = await _canvass.PostSupplierAsync(supplierRequest, ct);
var result = await _canvass.PostPutSupplierAsync(supplierRequest, ct);
if (result?.Value == null) continue;
var canvassDto = new CanvassDto
supplierId = result.Value.SupplierId;
existingSuppliers.Add(result.Value);
}
var canvassDto = new ForCanvassDto
{
PRDetailsId = item.PRDetailsId,
PRNo = item.PRNo,
ItemNo = item.ItemNo,
SupplierId = result.Value.SupplierId,
SupplierId = supplierId,
UserId = item.UserId,
FullName = item.FullName,
CanvassNo = ++canvassNo,
@ -506,46 +593,46 @@ namespace CPRNIMS.WebApi.Controllers.Canvass
var rfq = await _canvass.GetRFQ(canvassDto);
if (rfq == null || !rfq.Any()) continue;
var supplierEmailRequest = new SupplierEmailRequest
var emailRequest = BuildEmailRequest(item, supplier, rfq[0].Token, isProd);
await _canvass.SendRFQ(emailRequest);
anySuccess = true;
results.Add(new
{
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"] ?? ""),
item = item.ItemName,
supplier = supplier.SupplierName,
email = supplier.EmailAddress,
canvassNo = canvassDto.CanvassNo,
isExisting = matchedId.HasValue
});
}
catch (Exception) { continue; }
}
if (anySuccess)
await _canvass.DeleteAsync(item.PRDetailsId, ct);
return results;
}
private SupplierEmailRequest BuildEmailRequest(
ForAISearchingTagging item, SupplierResponse supplier, string token,bool isProd) => new()
{
AttachPath = GetRelativePath(@"Content\Documents\Pdf\Offer_Submission_Procedure.pdf"),
Recipient = isProd ? supplier.EmailAddress : _configuration["SMTP:TestRecipient"] ?? "rmsoriano@lloydlab.com",
Subject = $"LLI - Request For Quotation #PRNo: {item.PRNo}",
CC = _configuration["Canvass:CC"] ?? "rmsoriano@lloydlab.com",
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,
FormLink = _configuration["WebEndPoint:SupplierForm"] ?? "",
Token = token,
SupplierName = supplier.SupplierName,
Purchaser = item.FullName,
IsSuccess = false,
IsCanvass = true,
};
await _canvass.SearchingUpdate(item.PRDetailsId);
await _canvass.SendRFQ(supplierEmailRequest);
supplierResults.Add(new
{
item = item.ItemName,
supplier = supplier.SupplierName,
email = supplier.EmailAddress,
canvassNo = canvassDto.CanvassNo
});
}
}
return Ok(new
{
totalProcessed = supplierResults.Count,
suppliers = supplierResults
});
}
[HttpPost("PostSuggestedSupp")]
public async Task<IActionResult> PostSuggestedSupp(CanvassDto CanvassDto)
{

View File

@ -14,8 +14,8 @@ namespace CPRNIMS.WebApi.Security
{
UserId = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? "",
UserName = user.FindFirstValue(ClaimTypes.Name) ?? "",
FullName = user.FindFirstValue("fullName") ?? "",
Company = user.FindFirstValue("company") ?? "",
FullName = user.FindFirstValue("FullName") ?? "",
Company = user.FindFirstValue("Company") ?? "",
Roles = user.FindAll(ClaimTypes.Role)
.Select(r => r.Value)
.ToList()

View File

@ -1,10 +1,11 @@
using CPRNIMS.Domain.UIContracts.Account;
using CPRNIMS.Domain.UIContracts.Canvass;
using CPRNIMS.Infrastructure.Dto.Canvass.Request;
using CPRNIMS.Infrastructure.Dto.Common;
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
{
@ -21,6 +22,16 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
_canvass = canvass;
}
#region POST PUT
public async Task<IActionResult> PostSupplierForCanvass(CanvassVM viewModel)
{
var response = await _canvass.PostSupplierForCanvass(GetUser(), viewModel);
if (response.messCode != 0)
{
return Json(new { success = true });
}
return Json(new { success = false, response = response.errMessage });
}
public async Task<IActionResult> PostCanvass(CanvassVM viewModel, List<CanvassList> CanvassList)
{
if (CanvassList.Count > 0)
@ -57,8 +68,6 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
List<SupplierList> SupplierList)
{
var postPutItem = new CanvassVM();
try
{
if (SupplierList.Count > 0)
{
viewModel.SupplierList = new SupplierList
@ -77,26 +86,17 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
}
return Json(new { success = false });
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
return Json(new { success = false, response = postPutItem.errMessage });
}
}
public async Task<IActionResult> PostPutItemTagging(CanvassVM viewModel,
List<ItemList> ItemList)
{
var postPutItem = new CanvassVM();
try
{
if (ItemList.Count > 0)
{
viewModel.ItemList = new ItemList
{
ItemNo = ItemList.SelectMany(ic => ic.ItemNo).ToList(),
};
postPutItem = await _canvass.PostPutItemTagging(GetUser(), viewModel);
var postPutItem = await _canvass.PostPutItemTagging(GetUser(), viewModel);
if (postPutItem.messCode != 0)
{
return Json(new { success = true });
@ -108,13 +108,6 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
}
return Json(new { success = false });
}
catch (Exception ex)
{
var message = ex.InnerException?.ToString() ?? ex.Message.ToString();
return Json(new { success = false, response = postPutItem.errMessage });
}
}
public async Task<IActionResult> PostApprovedSupp(CanvassVM viewModel)
{
var postPutItem = await _canvass.PostApprovedSupp(GetUser(), viewModel);
@ -181,6 +174,31 @@ namespace CPRNIMS.WebApps.Controllers.Canvass
return Json(new { success = false, Response = postPutItem.errMessage });
}
[HttpPost]
public async Task<IActionResult> StartCanvass([FromBody] StartCanvassRequest request)
{
if (request?.Items == null || request.Items.Count == 0)
return Json(new { success = false, response = "Array Empty" });
var viewModel = new CanvassVM
{
ForSupplierSearchList = new ForSupplierSearchList
{
PRDetailsId = request.Items.Select(i => i.PRDetailsId).ToList(),
PRNo = request.Items.Select(i => i.PRNo).ToList(),
ItemNo = request.Items.Select(i => i.ItemNo).ToList(),
ItemDescription = request.Items.Select(i => i.ItemDescription).ToList(),
ItemName = request.Items.Select(i => i.ItemName).ToList(),
},
IsInternational = request.IsInternational
};
var result = await _canvass.StartCanvass(GetUser(), viewModel);
if (result.messCode != 0)
return Json(new { success = true });
return Json(new { success = false });
}
#endregion
#region Get
public async Task<IActionResult> GetItemSupplierWOEmail(long PRNo)

View File

@ -124,7 +124,6 @@
// </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>`)
@ -133,16 +132,40 @@
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)));
b.addEventListener("click", () => postCanvass(b.dataset.id)));
}
function openCanvass(id) {
document.getElementById("supplierId").value = id;
// e.g. $('#canvassModal').modal('show');
console.log("Open canvass:", id);
function postCanvass(supplierId) {
loader = $('#overlay, #loader').css('z-index', 1070);
const SupplierId = supplierId;
console.log(supplierId);
showConfirmation({
title: 'RFQ Submission',
message: 'Are you sure you want to submit this request for quotation? This action cannot be undone.',
type: 'warning',
confirmText: 'Yes',
cancelText: 'No'
}).then((confirmed) => {
if (confirmed) {
$.ajax($.extend({
url: '/CanvassMgmt/PostSupplierForCanvass',
type: 'POST',
data: {
SupplierId: SupplierId
},
success: function (response) {
if (response.success) {
fetchData();
showToast('success', 'RFQ Successfully Sent!', 'success!', 4000);
} else {
fetchData();
showToast('error', response.response, 'Submission of canvass failed!', 4000);
}
function sendEmail(id) {
console.log("Send email:", id);
},
error: errorHandler
}, beforeComplete(loader)));
}
});
}
fetchData();

View File

@ -79,7 +79,6 @@
<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 {
@ -278,6 +277,15 @@
gap: 5px;
}
.ft-qty {
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);
@ -537,6 +545,94 @@
animation: ftDots 1s steps(3, end) infinite;
}
</style>
<!-- Modal viewAllSuppliers -->
<div class="modal fade" id="viewItemSuppliers"
tabindex="-1" aria-labelledby="suppliersModalLabel" aria-hidden="true"
data-bs-backdrop="static">
<div class="modal-dialog vis-dialog">
<div class="modal-content vis-content">
@* {{-- ── MODAL HEADER ── --}} *@
<div class="vis-header">
<div class="vis-header-inner">
<div class="vis-header-icon">
<i class="fas fa-store"></i>
</div>
<div>
<h5 class="vis-title" id="suppliersModalLabel">Tag Suppliers</h5>
<p class="vis-subtitle">Select suppliers to assign to this item</p>
</div>
</div>
<button type="button" class="vis-close" data-bs-dismiss="modal" aria-label="Close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body vis-body">
@* {{-- ── ITEM INFO CARD ── --}} *@
<div class="vis-item-card">
<div class="vis-item-grid">
<div class="vis-item-field">
<span class="vis-field-lbl"><i class="fas fa-hashtag"></i> Item No</span>
<span class="vis-field-val" id="item-No">—</span>
</div>
<div class="vis-item-field">
<span class="vis-field-lbl"><i class="fas fa-box"></i> Item Name</span>
<span class="vis-field-val" id="item-Name">—</span>
</div>
<div class="vis-item-field vis-item-field--accent">
<span class="vis-field-lbl"><i class="fas fa-check-circle"></i> Selected Suppliers</span>
<span class="vis-field-val vis-sel-count" id="totalSelected">0</span>
</div>
</div>
</div>
@* {{-- ── TOOLBAR ── --}}
<div class="vis-toolbar">
<button id="btnAddNewSupplier" type="button"
class="vis-btn vis-btn-success"
onclick="showModalNewUpdateSupplier(0);">
<i class="fas fa-plus"></i> Add New Supplier
</button>
</div>
*@
@* {{-- ── TABLE ── --}} *@
<div class="vis-table-wrap">
<table id="SupplierDataTable" class="row-border vis-table">
<thead>
<tr>
<th class="vis-th-check">
<input id="selectAllHeaderCheckbox" type="checkbox"
class="selectAllCheckbox vis-checkbox"
title="Select all" />
</th>
<th>Supplier Name</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
@* {{-- ── MODAL FOOTER ── --}} *@
<div class="vis-footer">
<button type="button" class="vis-btn vis-btn-ghost"
onclick="refreshCanvasTable();" data-bs-dismiss="modal">
<i class="fas fa-arrow-left"></i> Back
</button>
<button id="btnSubmitSupplier" type="button"
class="vis-btn vis-btn-primary"
onclick="postTaggingSupplier();">
<i class="fas fa-paper-plane"></i> Submit Tagging
</button>
</div>
</div>
</div>
</div>
<script>
(function () {
@ -605,9 +701,11 @@
const id = card.dataset.id;
if (!s.selected.has(id)) {
s.selected.set(id, {
prDetailsId: id,
prNo: card.dataset.prno,
itemName: card.dataset.itemname
prDetailsId: parseInt(card.dataset.id, 10),
prNo: parseInt(card.dataset.prno, 10),
itemNo: parseInt(card.dataset.itemno, 10),
itemName: card.dataset.itemname,
itemDescription: card.dataset.itemdescription
});
}
});
@ -615,8 +713,8 @@
updateSelectionBar();
});
btnLocal.addEventListener("click", () => fireCanvass("Local"));
btnIntl.addEventListener("click", () => fireCanvass("International"));
btnLocal.addEventListener("click", () => fireCanvass(false));
btnIntl.addEventListener("click", () => fireCanvass(true));
// ── Sync checkbox visual state to s.selected ────────
function syncCheckboxes() {
@ -640,25 +738,62 @@
}
// ── Fire to backend ──────────────────────────────────
async function fireCanvass(locationType) {
async function fireCanvass(isInternational) {
const items = Array.from(s.selected.values());
const btn = isInternational ? btnIntl : btnLocal;
if (!items.length) return;
const btn = locationType === "Local" ? btnLocal : btnIntl;
// ── Identity config per mode ─────────────────────────
const mode = isInternational
? {
type: 'international',
icon: '🌐',
label: 'International',
badgeClass: 'confirm-badge--intl',
type: 'warning',
title: '🌐 International AI Canvass',
message: 'The AI will search <strong>both local and international suppliers</strong> for the selected items. This action cannot be undone.',
confirmText: 'Yes, Go International',
cancelText: 'No',
}
: {
type: 'info',
icon: '📍',
label: 'Local',
badgeClass: 'confirm-badge--local',
title: '📍 Local AI Canvass',
message: 'The AI will search <strong>local suppliers only</strong> for the selected items. This action cannot be undone.',
confirmText: 'Yes, Go Local',
cancelText: 'No',
};
const confirmed = await showConfirmation({
title: mode.title,
message: mode.message,
type: mode.type,
confirmText: mode.confirmText,
cancelText: mode.cancelText,
});
if (!confirmed) return;
btn.classList.add("loading");
const origLabel = btn.querySelector("span").textContent;
btn.querySelector("span").textContent = "Sending";
btn.querySelector("span").textContent = "Sending";
try {
const res = await fetch("/CanvassMgmt/StartCanvass", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
locationType: locationType,
isInternational: isInternational,
items: items.map(i => ({
prDetailsId: i.prDetailsId,
prNo: i.prNo,
itemName: i.itemName
prDetailsId: parseInt(i.prDetailsId, 10),
prNo: parseInt(i.prNo, 10),
itemNo: parseInt(i.itemNo, 10),
itemName: i.itemName,
itemDescription: i.itemDescription
}))
})
});
@ -666,22 +801,19 @@
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();
showToast(
isInternational
? '<i class="fas fa-globe"></i> <strong>International</strong> AI canvass started for ' + items.length + ' item' + (items.length !== 1 ? 's' : '') + '.'
: '<i class="fas fa-map-marker-alt"></i> <strong>Local</strong> AI canvass started for ' + items.length + ' item' + (items.length !== 1 ? 's' : '') + '.',
isInternational ? "warning" : "success"
);
} catch (err) {
console.error("fireCanvass error:", err);
showToast('<i class="fas fa-exclamation-triangle"></i> Failed to start canvass. Please try again.', "error");
@ -779,7 +911,9 @@
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 || "") + '">'
+ ' data-itemno="' + attr(String(item.itemNo || "")) + '"'
+ ' data-itemname="' + attr(item.itemName || "") + '"'
+ ' data-itemdescription="' + attr(item.itemDescription || "") + '">'
// ── Card header ──────────────────────────
+ '<div class="ft-card-hd">'
@ -790,6 +924,7 @@
+ '<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-qty"><i class="fas fa-hashtag"></i> Qty: ' + esc(String(item.qty || "-")) + '</div>'
+ '<div class="ft-department"><i class="fas fa-building-user"></i> Department: ' + esc(String(item.department || "-")) + '</div>'
+ '</div>'
@ -813,6 +948,7 @@
+ '<button class="cv-btn cv-btn-primary btn-tag"'
+ ' data-id="' + attr(String(item.prDetailsId)) + '"'
+ ' data-prno="' + attr(String(item.prNo || "")) + '"'
+ ' data-itemno="' + attr(String(item.itemNo || "")) + '"'
+ ' data-itemname="' + attr(item.itemName || "") + '">'
+ '<i class="fas fa-user-tag"></i> Tag Supplier'
+ '</button>'
@ -831,9 +967,11 @@
const id = card.dataset.id;
if (cb.checked) {
s.selected.set(id, {
prDetailsId: id,
prNo: card.dataset.prno,
itemName: card.dataset.itemname
prDetailsId: parseInt(card.dataset.id, 10),
prNo: parseInt(card.dataset.prno, 10),
itemNo: parseInt(card.dataset.itemno, 10),
itemName: card.dataset.itemname,
itemDescription: card.dataset.itemdescription
});
} else {
s.selected.delete(id);
@ -846,7 +984,7 @@
// ── 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);
viewTagSupplier(b.dataset.itemno, b.dataset.itemname);
});
});
}
@ -861,10 +999,73 @@
} catch (e) { return String(raw); }
}
function tagSupplier(prDetailsId, prNo, itemName) {
console.log("Tag supplier | PRDetailsId:", prDetailsId, "| PR#:", prNo, "| Item:", itemName);
}
fetchData();
})();
function viewTagSupplier(itemNo, itemName) {
loader = $('#overlay, #loader').css('z-index', 1080);
$('#viewItemSuppliers').modal('show');
$('#viewItemSuppliers').css('z-index', 1065);
tableName = '#SupplierDataTable';
totalSelectedLabel = $('#totalSelected');
clearTableSelection(tableName, selectedProductsMap, () => {
totalSelectedLabel.text(0);
}, 'selected-row', '.select-all-CanvassItem-checkbox');
tableElement = $(tableName);
tableDestroy(tableElement);
ItemNo = parseInt(itemNo, 10);
document.getElementById('item-Name').innerText = itemName;
document.getElementById('item-No').innerText = ItemNo;
supplierDataTable = tableElement.DataTable({
ajax: $.extend({
url: endpoint.GetSupplierItemWOEmail,
type: 'POST',
data: { ItemNo },
}, beforeComplete(loader)),
language: {
emptyTable: "No record available"
},
initComplete: function () {
initializeTableSelection({
tableName: tableName,
dataTable: supplierDataTable,
selectedItemsMap: selectedProductsMap,
idKey: 'supplierId',
idKey2: 'itemNo',
checkboxClass: '.select-CanvassItem-checkbox',
selectAllClass: '.select-all-CanvassItem-checkbox',
selectedRowClass: 'selected-row',
updateCountCallback: function () {
totalSelectedLabel.text(getSelectedCount(selectedProductsMap));
}
});
},
columns: [{
data: 'supplierId',
title: '<input type="checkbox" class="select-all-CanvassItem-checkbox" />',
render: function () {
return '<input type="checkbox" class="select-CanvassItem-checkbox" />';
},
orderable: false,
searchable: false
},
{data: 'supplierName'}],
rowCallback: function (row, data) {
var statusCell = $('td:eq(9)', row);
var myStatus = statusCell.text().trim();
if (myStatus === 'Yes' || myStatus === 'true' || myStatus === true) {
statusCell.text('Yes').addClass('status-active');
} else {
statusCell.text('No').addClass('status-partial');
}
},
error: errorHandler
});
}
</script>

View File

@ -15,7 +15,7 @@
<script src="~/jsfunctions/common/IndexCard.js"></script>
<script src="~/jsfunctions/common/ColumnCommonV2.js"></script>
<script src="~/jsfunctions/canvass/CanvassViewV6.js"></script>
<script src="~/jsfunctions/canvass/CanvassViewV7.js"></script>
<script src="~/jsfunctions/common/termsV2.js"></script>
<script src="~/jsfunctions/canvass/PostPutV9.js"></script>
<script src="~/jsfunctions/common/PostPutV2.js"></script>

View File

@ -753,4 +753,338 @@
font-size: .875rem;
color: var(--text-muted);
}
/* ── SUPPLIER MODAL (vis = viewItemSuppliers) ────────── */
.vis-dialog {
max-width: 92vw;
width: 750px;
margin: 40px auto;
}
.vis-content {
border: none;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,.18), 0 4px 16px rgba(0,0,0,.10);
}
/* ── Header ─────────────────────────────────────────── */
.vis-header {
background: linear-gradient(135deg, #0d5c63 0%, #0e7c86 55%, #18a8b5 100%);
padding: 20px 24px 18px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.vis-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'%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");
pointer-events: none;
}
.vis-header-inner {
display: flex;
align-items: center;
gap: 14px;
position: relative;
z-index: 1;
}
.vis-header-icon {
width: 42px;
height: 42px;
background: rgba(255,255,255,.15);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: #fff;
flex-shrink: 0;
}
.vis-title {
font-family: 'Space Grotesk', sans-serif;
font-size: 1.15rem;
font-weight: 700;
color: #fff;
margin: 0;
line-height: 1.2;
}
.vis-subtitle {
font-size: .78rem;
color: rgba(255,255,255,.72);
margin: 3px 0 0;
}
.vis-close {
position: relative;
z-index: 1;
width: 34px;
height: 34px;
background: rgba(255,255,255,.12);
border: 1px solid rgba(255,255,255,.2);
border-radius: 8px;
color: rgba(255,255,255,.85);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background .18s;
font-size: .85rem;
flex-shrink: 0;
}
.vis-close:hover {
background: rgba(255,255,255,.22);
color: #fff;
}
/* ── Body ────────────────────────────────────────────── */
.vis-body {
padding: 20px 24px 8px;
background: #f0f6f7;
}
/* ── Item info card ──────────────────────────────────── */
.vis-item-card {
background: #fff;
border-radius: 12px;
border: 1px solid #d6eaec;
padding: 14px 18px;
margin-bottom: 14px;
box-shadow: 0 2px 8px rgba(13,92,99,.07);
}
.vis-item-grid {
display: flex;
gap: 0;
flex-wrap: wrap;
}
.vis-item-field {
flex: 1;
min-width: 160px;
padding: 4px 16px 4px 0;
border-right: 1px solid #d6eaec;
margin-right: 16px;
}
.vis-item-field:last-child {
border-right: none;
margin-right: 0;
}
.vis-field-lbl {
display: flex;
align-items: center;
gap: 5px;
font-size: .67rem;
text-transform: uppercase;
letter-spacing: .06em;
color: #6b8890;
font-weight: 700;
margin-bottom: 4px;
}
.vis-field-lbl i {
font-size: .65rem;
}
.vis-field-val {
font-family: 'Space Grotesk', sans-serif;
font-size: .95rem;
font-weight: 700;
color: #0d5c63;
line-height: 1.3;
word-break: break-word;
}
.vis-sel-count {
font-size: 1.3rem;
color: #c0392b;
}
/* ── Toolbar ─────────────────────────────────────────── */
.vis-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
/* ── Buttons ─────────────────────────────────────────── */
.vis-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 18px;
border-radius: 8px;
border: none;
font-family: 'DM Sans', sans-serif;
font-size: .84rem;
font-weight: 700;
cursor: pointer;
transition: all .18s;
white-space: nowrap;
}
.vis-btn-primary {
background: #0e7c86;
color: #fff;
box-shadow: 0 2px 8px rgba(14,124,134,.3);
}
.vis-btn-primary:hover {
background: #0d5c63;
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(14,124,134,.35);
}
.vis-btn-success {
background: #28a745;
color: #fff;
box-shadow: 0 2px 8px rgba(40,167,69,.25);
}
.vis-btn-success:hover {
background: #218838;
transform: translateY(-1px);
}
.vis-btn-ghost {
background: #fff;
color: #0d5c63;
border: 1.5px solid #d6eaec;
}
.vis-btn-ghost:hover {
background: #e6f7f8;
border-color: #0e7c86;
}
/* ── Table wrapper ───────────────────────────────────── */
.vis-table-wrap {
background: #fff;
border-radius: 12px;
border: 1px solid #d6eaec;
overflow: hidden;
box-shadow: 0 2px 8px rgba(13,92,99,.07);
overflow-x: auto;
}
.vis-table {
width: 100% !important;
font-family: 'DM Sans', sans-serif;
font-size: .84rem;
border-collapse: collapse;
}
.vis-table thead tr {
background: linear-gradient(135deg, #1a3a4a, #1e5468);
}
.vis-table thead th {
color: rgba(255,255,255,.88);
font-size: .7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
padding: 11px 13px;
border-bottom: none !important;
white-space: nowrap;
}
.vis-th-check {
width: 40px;
text-align: center !important;
}
.vis-table tbody tr {
border-bottom: 1px solid #edf2f3;
transition: background .12s;
}
.vis-table tbody tr:last-child {
border-bottom: none;
}
.vis-table tbody tr:hover {
background: #f0f6f7;
}
.vis-table tbody tr.selected-row {
background: #e6f7f8;
}
.vis-table tbody td {
padding: 10px 13px;
color: #1a2e35;
vertical-align: middle;
}
/* ── Checkbox styling ────────────────────────────────── */
.vis-checkbox,
.vis-table .select-CanvassItem-checkbox,
.vis-table .select-all-CanvassItem-checkbox,
#selectAllHeaderCheckbox {
width: 16px;
height: 16px;
accent-color: #0e7c86;
cursor: pointer;
}
/* ── Footer ──────────────────────────────────────────── */
.vis-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 14px 24px 18px;
background: #f0f6f7;
border-top: 1px solid #d6eaec;
}
/* ── DataTables overrides to match vis style ─────────── */
#viewItemSuppliers .dataTables_wrapper .dataTables_filter input {
border: 1.5px solid #d6eaec;
border-radius: 8px;
padding: 6px 10px;
font-family: 'DM Sans', sans-serif;
font-size: .84rem;
outline: none;
transition: border-color .18s;
}
#viewItemSuppliers .dataTables_wrapper .dataTables_filter input:focus {
border-color: #0e7c86;
}
#viewItemSuppliers .dataTables_wrapper .dataTables_info,
#viewItemSuppliers .dataTables_wrapper .dataTables_length label {
font-family: 'DM Sans', sans-serif;
font-size: .82rem;
color: #6b8890;
}
#viewItemSuppliers .dataTables_wrapper .dataTables_paginate .paginate_button {
border-radius: 6px !important;
font-family: 'DM Sans', sans-serif;
font-size: .82rem;
}
#viewItemSuppliers .dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: #0e7c86 !important;
border-color: #0e7c86 !important;
color: #fff !important;
}
#viewItemSuppliers .dataTables_wrapper {
padding: 14px 16px 10px;
}
</style>

View File

@ -291,6 +291,8 @@ function viewSuppDetail(data) {
$('#viewItemSuppliers').modal('show');
$('#viewItemSuppliers').css('z-index', 1065);
console.log('dito diba???');
tableName = '#SupplierDataTable';
totalSelectedLabel = $('#totalSelected');