diff --git a/CPRNIMS.Domain/Contracts/Canvass/ICanvass.cs b/CPRNIMS.Domain/Contracts/Canvass/ICanvass.cs index 01104fc..b7538e4 100644 --- a/CPRNIMS.Domain/Contracts/Canvass/ICanvass.cs +++ b/CPRNIMS.Domain/Contracts/Canvass/ICanvass.cs @@ -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 PostPerSupplierToken(CanvassDto CanvassDto); + Task> StartCanvass(CanvassVM canvass,CancellationToken ct); + Task PostPerSupplierToken(ForCanvassDto CanvassDto); Task PutSupplierCanvass(long canvassSupplierId); - Task PostCanvass(CanvassDto CanvassDto); Task PostPutSupplier(CanvassDto CanvassDto); Task PostTaggingSupplier(CanvassDto CanvassDto); Task PostApprovedSupp(CanvassDto CanvassDto); @@ -34,7 +36,7 @@ namespace CPRNIMS.Domain.Contracts.Canvass Task> GetCanvassWOResponse(CanvassDto CanvassDto); Task> GetWOResponseBySuppId(CanvassDto CanvassDto); Task> GetSupplierById(CanvassDto CanvassDto); - Task> GetRFQ(CanvassDto CanvassDto); + Task> GetRFQ(ForCanvassDto CanvassDto); Task> GetSupplierBid(CanvassDto CanvassDto); Task> GetSupplierBidByItem(CanvassDto CanvassDto); Task> GetSupplierBidById(CanvassDto CanvassDto); diff --git a/CPRNIMS.Domain/Contracts/Canvass/ISupplier.cs b/CPRNIMS.Domain/Contracts/Canvass/ISupplier.cs index 2394a97..b4a9c84 100644 --- a/CPRNIMS.Domain/Contracts/Canvass/ISupplier.cs +++ b/CPRNIMS.Domain/Contracts/Canvass/ISupplier.cs @@ -12,10 +12,12 @@ namespace CPRNIMS.Domain.Services.ICanvass { public interface ISupplier { - Task> GetItemWithoutSupplier(); - Task> PostSupplierAsync(SupplierRequest request, CancellationToken ct); - Task SendRFQ(SupplierEmailRequest supplierEmailRequest); - Task SearchingUpdate(long pRDetailsId); - Task> GetForAISearchingTagging(); + Task> GetSuppliers(CancellationToken ct); + Task> GetForAISearchingTagging(CancellationToken ct); + Task> GetItemWithoutSupplier(CancellationToken ct); + Task> GetSupplierForCanvass(int supplierId, string userName,CancellationToken ct); + Task> PostPutSupplierAsync(SupplierRequest request, CancellationToken ct); + Task SendRFQ(SupplierEmailRequest supplierEmailRequest); + Task DeleteAsync(long pRDetailsId, CancellationToken ct); } } diff --git a/CPRNIMS.Domain/Contracts/PO/IPurchaseOrder.cs b/CPRNIMS.Domain/Contracts/PO/IPurchaseOrder.cs index ed4c275..3f770c5 100644 --- a/CPRNIMS.Domain/Contracts/PO/IPurchaseOrder.cs +++ b/CPRNIMS.Domain/Contracts/PO/IPurchaseOrder.cs @@ -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> GetLatestPO2(PODto pODto); Task> GetDocRequired(PODto pODto); Task> GetOtherCharges(PODto itemDto); - Task> GetSuppliers(PODto itemDto); + Task> GetSuppliers(PODto itemDto); Task> GetCreatedPO(PODto pODto); Task> GetPOItemDetail(PODto pODto); Task> GetMyCreatedPO(PODto pODto); diff --git a/CPRNIMS.Domain/Profile/Canvass/SupplierRequestProfile.cs b/CPRNIMS.Domain/Profile/Canvass/SupplierRequestProfile.cs index ae37417..4e4d315 100644 --- a/CPRNIMS.Domain/Profile/Canvass/SupplierRequestProfile.cs +++ b/CPRNIMS.Domain/Profile/Canvass/SupplierRequestProfile.cs @@ -14,10 +14,11 @@ namespace CPRNIMS.Domain.Profile.Canvass public SupplierRequestProfile() { CreateMap(); - CreateMap(); CreateMap().ReverseMap(); + CreateMap(); + CreateMap().ReverseMap(); } } } diff --git a/CPRNIMS.Domain/Services/Canvass/Canvass.cs b/CPRNIMS.Domain/Services/Canvass/Canvass.cs index d4166d2..37d4531 100644 --- a/CPRNIMS.Domain/Services/Canvass/Canvass.cs +++ b/CPRNIMS.Domain/Services/Canvass/Canvass.cs @@ -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> GetSuppliers(CancellationToken ct) + { + + return await _dbContext.Suppliers + .AsNoTracking() + .Where(s => s.IsActive) + .ToListAsync(ct); + } public async Task> 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(); } - public async Task> GetRFQ(CanvassDto CanvassDto) + public async Task> 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> GetForAISearchingTagging() + public async Task> GetForAISearchingTagging(CancellationToken ct) { var allItems = await _dbContext.ForAISearchingTaggings .AsNoTracking() - .Take(10) - .ToListAsync(); + .Take(1) + .ToListAsync(ct); return allItems ?? new List(); } - public async Task> GetItemWithoutSupplier() + public async Task> GetItemWithoutSupplier(CancellationToken ct) { var allItems = await _dbContext.ItemWithoutSuppliers .FromSqlRaw("EXEC GetItemWithoutSupplier") - .ToListAsync(); + .ToListAsync(ct); return allItems ?? new List(); } + public async Task> 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(); + } public async Task> GetWOResponseBySuppId(CanvassDto CanvassDto) { var allItems = await _dbContext.WOResponseByIds @@ -335,7 +356,7 @@ namespace CPRNIMS.Domain.Services.Canvass return allItems ?? new List(); } - public async Task PostPerSupplierToken(CanvassDto CanvassDto) + public async Task 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> PostSupplierAsync(SupplierRequest request, CancellationToken ct) + public async Task> 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 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 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 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 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> StartCanvass(CanvassVM request, CancellationToken ct) + { + var detailIds = request.ForSupplierSearchList?.PRDetailsId; + if (detailIds == null || !detailIds.Any()) + return Result.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(); + var idsToUpdate = new List(); + + 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.Success(new StartCanvassResponse { }); } #endregion } diff --git a/CPRNIMS.Domain/Services/Canvass/SupplierSearchService.cs b/CPRNIMS.Domain/Services/Canvass/SupplierSearchService.cs index a249834..ede4b36 100644 --- a/CPRNIMS.Domain/Services/Canvass/SupplierSearchService.cs +++ b/CPRNIMS.Domain/Services/Canvass/SupplierSearchService.cs @@ -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" }; + /// + /// 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. + /// + public async Task FindMatchingExistingSupplierAsync( + SupplierResponse incoming, + List 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\": }\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(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(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> 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; } @@ -50,55 +170,64 @@ namespace CPRNIMS.Domain.Services.Canvass private async Task<(string content, List urls)> SearchTavilyAsync( string itemName, string itemDescription,string locality) { - var query = $"{itemName} {itemDescription} suppliers {locality} budget price contact email phone"; - - var payload = new + try { - query, - max_results = 10, - search_depth = "advanced", - include_answer = false - }; + var query = $"{itemName} {itemDescription} suppliers {locality} budget price contact email phone"; - var request = new HttpRequestMessage(HttpMethod.Post, _config["Tavily:SearchUrl"]); - request.Headers.Add("Authorization", $"Bearer {_config["Tavily: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 result = JsonSerializer.Deserialize(body, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - - var sb = new StringBuilder(); - var urls = new List(); - int i = 1; - - foreach (var r in result?.Results ?? new()) - { - // Clean text - var clean = Regex.Replace(r.Content ?? "", @"[^\x20-\x7E]", " "); - clean = Regex.Replace(clean, @"\s{3,}", " "); - if (clean.Length > 300) clean = clean[..300]; - sb.Append($"{i}. Title:{r.Title}|URL:{r.Url}|Content:{clean}|"); - - // Collect base domain URLs for contact page fetching - try + var payload = new { - var uri = new Uri(r.Url); - var baseUrl = $"{uri.Scheme}://{uri.Host}"; - if (!urls.Contains(baseUrl)) urls.Add(baseUrl); + query, + max_results = 10, + search_depth = "advanced", + include_answer = false + }; + + var request = new HttpRequestMessage(HttpMethod.Post, _config["Tavily:SearchUrl"]); + request.Headers.Add("Authorization", $"Bearer {_config["Tavily: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 result = JsonSerializer.Deserialize(body, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + var sb = new StringBuilder(); + var urls = new List(); + int i = 1; + + foreach (var r in result?.Results ?? new()) + { + // Clean text + var clean = Regex.Replace(r.Content ?? "", @"[^\x20-\x7E]", " "); + clean = Regex.Replace(clean, @"\s{3,}", " "); + if (clean.Length > 300) clean = clean[..300]; + sb.Append($"{i}. Title:{r.Title}|URL:{r.Url}|Content:{clean}|"); + + // Collect base domain URLs for contact page fetching + try + { + var uri = new Uri(r.Url); + var baseUrl = $"{uri.Scheme}://{uri.Host}"; + if (!urls.Contains(baseUrl)) urls.Add(baseUrl); + } + catch { } + i++; } - catch { } - i++; + + var fullText = sb.ToString(); + if (fullText.Length > 2000) fullText = fullText[..2000]; + + return (fullText, urls); } - - var fullText = sb.ToString(); - if (fullText.Length > 2000) fullText = fullText[..2000]; - - return (fullText, urls); + catch (Exception ex) + { + ex.ToString(); + throw; + } + } // ── Fetch Contact Pages ─── @@ -152,84 +281,173 @@ namespace CPRNIMS.Domain.Services.Canvass // ── Groq ───────────────────────────────────────────────────────────────── private async Task> 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}"; - - var payload = new + try { - model = _config["Groq:Model"] ?? "llama-3.1-8b-instant", - stream = false, - max_tokens = 2048, - temperature = 0.1, - messages = new[] + 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 { + model = _config["Groq:Model"] ?? "llama-3.1-8b-instant", + stream = false, + max_tokens = 2048, + temperature = 0.1, + messages = new[] + { new { role = "system", content = "You are a supplier data extractor. Extract real contact details from provided content. Return ONLY a valid JSON array, 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 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 response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var body = await response.Content.ReadAsStringAsync(); - var groqResp = JsonSerializer.Deserialize(body, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + var body = await response.Content.ReadAsStringAsync(); + var groqResp = JsonSerializer.Deserialize(body, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - var rawText = groqResp?.Choices?[0]?.Message?.Content ?? string.Empty; + var rawText = groqResp?.Choices?[0]?.Message?.Content ?? string.Empty; - var match = Regex.Match(rawText, @"\[[\s\S]*\]"); - if (!match.Success) return new List(); + var match = Regex.Match(rawText, @"\[[\s\S]*\]"); + if (!match.Success) return new List(); - var groqList = JsonSerializer.Deserialize>(match.Value, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) - ?? new List(); - - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - var suppliers = new List(); - - foreach (var s in groqList) - { - var key = (s.CompanyName ?? "").Trim().ToLower(); - if (string.IsNullOrEmpty(key) || seen.Contains(key)) continue; - seen.Add(key); - - if (string.IsNullOrEmpty(s.ContactEmail)) continue; - - suppliers.Add(new SupplierResponse + // Add the converter to the shared options + var jsonOptions = new JsonSerializerOptions { - SupplierName = s.CompanyName, - EmailAddress = s.ContactEmail, - ContactNo = s.PhoneNumber ?? string.Empty, - Address = s.Country ?? string.Empty, - IsActive = true, - VatInc = false, - Currency = "PHP", - CurrencyId = 1, - PaymentTermsId = 1, - PaymentTerms = "30 Days", - LeadTime = "7-14 Days", - TinNo = string.Empty, - ContactPerson = string.Empty, - Website =s.Website ?? string.Empty, - }); + PropertyNameCaseInsensitive = true, + Converters = { new FlexibleDecimalConverter() } + }; - if (suppliers.Count >= 10) break; + var groqList = JsonSerializer.Deserialize>(match.Value, jsonOptions) + ?? new List(); + + var seenNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var seenEmails = new HashSet(StringComparer.OrdinalIgnoreCase); + var seenPhones = new HashSet(StringComparer.OrdinalIgnoreCase); + var suppliers = new List(); + var allowedCountries = isInternational + ? new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Philippines", "China", "Japan", "South Korea", "Taiwan", + "India", "Singapore", "Malaysia", "Thailand", "Vietnam", + "Indonesia", "Hong Kong" + } + : new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Philippines" + }; + + foreach (var s in groqList) + { + var key = (s.CompanyName ?? "").Trim().ToLower(); + var email = (s.ContactEmail ?? "").Trim().ToLower(); + var phone = NormalizePhone(s.PhoneNumber ?? ""); + + // 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 + { + SupplierName = s.CompanyName, + EmailAddress = s.ContactEmail, + ContactNo = s.PhoneNumber ?? string.Empty, + Address = s.Country ?? string.Empty, + IsActive = true, + VatInc = false, + Currency = "PHP", + CurrencyId = 1, + PaymentTermsId = 1, + PaymentTerms = "30 Days", + LeadTime = "7-14 Days", + TinNo = string.Empty, + ContactPerson = string.Empty, + Website = s.Website ?? string.Empty, + }); + + if (suppliers.Count >= 10) break; + } + + return suppliers; } + catch (Exception ex) + { + ex.ToString(); + throw; + } + + } + private static string NormalizePhone(string phone) + { + if (string.IsNullOrWhiteSpace(phone)) return string.Empty; - return suppliers; + // 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; } } } } diff --git a/CPRNIMS.Domain/Services/PO/PurchaseOrder.cs b/CPRNIMS.Domain/Services/PO/PurchaseOrder.cs index 04042fb..c7c7892 100644 --- a/CPRNIMS.Domain/Services/PO/PurchaseOrder.cs +++ b/CPRNIMS.Domain/Services/PO/PurchaseOrder.cs @@ -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> GetSuppliers(PODto itemDto) + public async Task> 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(); + return allItems ?? new List(); } public async Task> GetPRWOCanvass(PODto itemDto) { diff --git a/CPRNIMS.Domain/UIContracts/Canvass/ICanvass.cs b/CPRNIMS.Domain/UIContracts/Canvass/ICanvass.cs index 7b76fbc..8ecadb0 100644 --- a/CPRNIMS.Domain/UIContracts/Canvass/ICanvass.cs +++ b/CPRNIMS.Domain/UIContracts/Canvass/ICanvass.cs @@ -39,6 +39,7 @@ namespace CPRNIMS.Domain.UIContracts.Canvass #endregion #region Post Put + Task PostSupplierForCanvass(User user, CanvassVM viewModel); Task PostCanvass(User user, CanvassVM viewModel); Task PostPutSupplier(User user, CanvassVM viewModel); Task PostTaggingSupplier(User user, CanvassVM viewModel); @@ -49,6 +50,7 @@ namespace CPRNIMS.Domain.UIContracts.Canvass Task PostPutMySupplier(User user, CanvassVM viewModel); Task PostPutItemTagging(User user, CanvassVM viewModel); Task UnlockFormLink(User user, CanvassVM viewModel); + Task StartCanvass(User user, CanvassVM viewModel); #endregion } } diff --git a/CPRNIMS.Domain/UIServices/Canvass/Canvass.cs b/CPRNIMS.Domain/UIServices/Canvass/Canvass.cs index 551666a..242cd35 100644 --- a/CPRNIMS.Domain/UIServices/Canvass/Canvass.cs +++ b/CPRNIMS.Domain/UIServices/Canvass/Canvass.cs @@ -343,6 +343,18 @@ namespace CPRNIMS.Domain.UIServices.Canvass _configuration["LLI:NonInvent:CanvassMgmt:UnlockFormLink"]); } + public async Task StartCanvass(User user, CanvassVM viewModel) + { + return await SendPostApiRequest(user, viewModel, + _configuration["LLI:NonInvent:CanvassMgmt:StartCanvass"]); + } + + public async Task PostSupplierForCanvass(User user, CanvassVM viewModel) + { + return await SendPostApiRequest(user, viewModel, + _configuration["LLI:NonInvent:CanvassMgmt:PostSupplierForCanvass"]); + } + #endregion } } diff --git a/CPRNIMS.Infrastructure/Database/NonInventoryDbContext.cs b/CPRNIMS.Infrastructure/Database/NonInventoryDbContext.cs index 722e3a7..cb34a23 100644 --- a/CPRNIMS.Infrastructure/Database/NonInventoryDbContext.cs +++ b/CPRNIMS.Infrastructure/Database/NonInventoryDbContext.cs @@ -66,6 +66,7 @@ namespace CPRNIMS.Infrastructure.Database public virtual DbSet ForRRs { get; set; } public virtual DbSet RRs { get; set; } public virtual DbSet Canvasses { get; set; } + public DbSet SupplierForCanvass { get; set; } public DbSet SupplierResponses { get; set; } public DbSet SupplierItems { get; set; } public DbSet ItemsForTaggings { get; set; } diff --git a/CPRNIMS.Infrastructure/Entities/Canvass/ItemsForTagging.cs b/CPRNIMS.Infrastructure/Entities/Canvass/ItemsForTagging.cs index 2bd5800..fafbc70 100644 --- a/CPRNIMS.Infrastructure/Entities/Canvass/ItemsForTagging.cs +++ b/CPRNIMS.Infrastructure/Entities/Canvass/ItemsForTagging.cs @@ -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; } } } diff --git a/CPRNIMS.Infrastructure/Entities/Canvass/SupplierForCanvass.cs b/CPRNIMS.Infrastructure/Entities/Canvass/SupplierForCanvass.cs new file mode 100644 index 0000000..53b0e58 --- /dev/null +++ b/CPRNIMS.Infrastructure/Entities/Canvass/SupplierForCanvass.cs @@ -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; } + } +} diff --git a/CPRNIMS.Infrastructure/Entities/Canvass/Suppliers.cs b/CPRNIMS.Infrastructure/Entities/Canvass/Suppliers.cs index 3ce01f9..ee558f2 100644 --- a/CPRNIMS.Infrastructure/Entities/Canvass/Suppliers.cs +++ b/CPRNIMS.Infrastructure/Entities/Canvass/Suppliers.cs @@ -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; } } } diff --git a/CPRNIMS.Infrastructure/ViewModel/Canvass/CanvassVM.cs b/CPRNIMS.Infrastructure/ViewModel/Canvass/CanvassVM.cs index 93b3a44..035d315 100644 --- a/CPRNIMS.Infrastructure/ViewModel/Canvass/CanvassVM.cs +++ b/CPRNIMS.Infrastructure/ViewModel/Canvass/CanvassVM.cs @@ -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; } } } diff --git a/CPRNIMS.Infrastructure/ViewModel/Canvass/ForSupplierSearchList.cs b/CPRNIMS.Infrastructure/ViewModel/Canvass/ForSupplierSearchList.cs new file mode 100644 index 0000000..b6d7e0d --- /dev/null +++ b/CPRNIMS.Infrastructure/ViewModel/Canvass/ForSupplierSearchList.cs @@ -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? PRDetailsId { get; set; } + public List? PRNo { get; set; } + public List? ItemNo { get; set; } + public List? ItemName { get; set; } + public List? ItemDescription { get; set; } + } +} diff --git a/CPRNIMS.Infrastructure/ViewModel/Canvass/XCheckItemVM.cs b/CPRNIMS.Infrastructure/ViewModel/Canvass/XCheckItemVM.cs deleted file mode 100644 index 9d9dd65..0000000 --- a/CPRNIMS.Infrastructure/ViewModel/Canvass/XCheckItemVM.cs +++ /dev/null @@ -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? ItemNo { get; set; } - } -} diff --git a/CPRNIMS.WebApi/Controllers/Canvass/CanvassMgmtController.cs b/CPRNIMS.WebApi/Controllers/Canvass/CanvassMgmtController.cs index 1cc5c35..2037234 100644 --- a/CPRNIMS.WebApi/Controllers/Canvass/CanvassMgmtController.cs +++ b/CPRNIMS.WebApi/Controllers/Canvass/CanvassMgmtController.cs @@ -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,96 +216,144 @@ namespace CPRNIMS.WebApi.Controllers.Canvass #endregion #region Post Put + [HttpPost("PostSupplierForCanvass")] + public async Task 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 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 PostAllCanvass() { - try + var baseTemplate = EMailTemplate("Content\\SMTPEmailContent", "SendToSupplier.cshtml"); + + var allCanvass = await _canvass.GetAllForCanvass(); + + var groupedBySupplier = allCanvass + .GroupBy(x => x.SupplierId) + .ToList(); + + foreach (var supplierGroup in groupedBySupplier) { - var baseTemplate = EMailTemplate("Content\\SMTPEmailContent", "SendToSupplier.cshtml"); + var firstItem = supplierGroup.First(); + int canvassNo = await _canvass.GetCanvassNo(); - // 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) + foreach (var rfqq in supplierGroup) { - var firstItem = supplierGroup.First(); - int canvassNo = await _canvass.GetCanvassNo(); - - // Insert all items for this supplier - foreach (var rfqq in supplierGroup) + var canvass = new ForCanvassDto { - var canvass = new CanvassDto - { - PRDetailsId = rfqq.PRDetailsId, - PRNo = rfqq.PRNo, - ItemNo = rfqq.ItemNo, - SupplierId = rfqq.SupplierId, - UserId = rfqq.UserId, - FullName = rfqq.FullName, - CanvassNo = canvassNo + 1 - }; - - await _canvass.PostPerSupplierToken(canvass); - } - - // ✅ After inserting all items for this supplier, retrieve RFQ info - var canvassInfo = new CanvassDto - { - SupplierId = firstItem.SupplierId, - UserId = firstItem.UserId, - FullName = firstItem.FullName, + PRDetailsId = rfqq.PRDetailsId, + PRNo = rfqq.PRNo, + ItemNo = rfqq.ItemNo, + SupplierId = rfqq.SupplierId, + UserId = rfqq.UserId, + FullName = rfqq.FullName, CanvassNo = canvassNo + 1 }; - var rfq = await _canvass.GetRFQ(canvassInfo); - - 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"), - Recipient = rfq[0].EmailAddress, - Message = message.ToString(), - Subject = "CLMS - Request For Quotation #PRNo: " + rfq[0].AggrePRNo, - CC = Convert.ToString(_configuration["Canvass:CC"]), - SenderEmail = _config["SMTP:SenderEmail"], - DisplayName = "lloydlabinc.com", - NewPassword = _config["SMTP:Password"], - OutGoingPort = 587, - Server = _config["SMTP:Server"], - UserName = _config["SMTP:UserName"], - IsSuccess = false, - IsCanvass = true - }; - - await _smtpHelper.SendEmailAsync(messageDetails); - - // ✅ Post canvass record after successful send - await _canvass.PostCanvass(canvassInfo); - } + await _canvass.PostPerSupplierToken(canvass); } - 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; + var canvassInfo = new ForCanvassDto + { + SupplierId = firstItem.SupplierId, + UserId = firstItem.UserId, + FullName = firstItem.FullName, + CanvassNo = canvassNo + 1 + }; + + var rfq = await _canvass.GetRFQ(canvassInfo); + + if (rfq?.Any() == true) + { + 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); + + var messageDetails = new EmailMessageDetailsVM + { + AttachPath = GetRelativePath(@"Content\Documents\Pdf\Offer_Submission_Procedure.pdf"), + Recipient = rfq[0].EmailAddress, + Message = message.ToString(), + Subject = "CLMS - Request For Quotation #PRNo: " + rfq[0].AggrePRNo, + CC = Convert.ToString(_configuration["Canvass:CC"]), + SenderEmail = _config["SMTP:SenderEmail"], + DisplayName = "lloydlabinc.com", + NewPassword = _config["SMTP:Password"], + OutGoingPort = 587, + Server = _config["SMTP:Server"], + UserName = _config["SMTP:UserName"], + IsSuccess = false, + IsCanvass = true + }; + + await _smtpHelper.SendEmailAsync(messageDetails); + } } + + return Ok(new { message = "All canvass emails sent successfully." }); } [HttpPost("PostCanvassFollowUp")] @@ -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 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(); + var existingSuppliers = _mapper.Map>(rawSuppliers); var supplierResults = new List(); - foreach (var item in response) + foreach (var item in items) { - // #2 Search Tavily + Filter with Groq - var suppliers = await _supplierSearchService - .SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription, item.IsInternational); - - if (!suppliers.Any()) + try { - await _canvass.SearchingUpdate(item.PRDetailsId); - continue; + var results = await ProcessItemAsync(item, existingSuppliers, ct); + supplierResults.AddRange(results); } + catch (Exception) { } + } - // #3 & #4 Loop each found supplier - foreach (var supplier in suppliers) + return Ok(new { totalProcessed = supplierResults.Count, suppliers = supplierResults }); + } + private async Task> ProcessItemAsync( + ForAISearchingTagging item, + List existingSuppliers, + CancellationToken ct) + { + var suppliers = await _supplierSearchService + .SearchAndFilterSuppliersAsync(item.ItemName, item.ItemDescription, item.IsInternational); + + if (!suppliers.Any()) + { + await _canvass.DeleteAsync(item.PRDetailsId, ct); + return new List(); + } + + var results = new List(); + int canvassNo = await _canvass.GetCanvassNo(); + bool anySuccess = false; + bool isProd = Convert.ToBoolean(_configuration["SMTP:IsLive"]); + + foreach (var supplier in suppliers) + { + try { - int canvassNo = await _canvass.GetCanvassNo(); + int supplierId; - var supplierRequest = _mapper.Map(supplier); - supplierRequest.ItemNo = item.ItemNo; + var matchedId = await _supplierSearchService + .FindMatchingExistingSupplierAsync(supplier, existingSuppliers); - var result = await _canvass.PostSupplierAsync(supplierRequest, ct); - if (result?.Value == null) continue; + if (matchedId.HasValue) + { + supplierId = matchedId.Value; + } + else + { + var supplierRequest = _mapper.Map(supplier); + supplierRequest.ItemNo = item.ItemNo; - var canvassDto = new CanvassDto + var result = await _canvass.PostPutSupplierAsync(supplierRequest, ct); + if (result?.Value == null) continue; + + 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 - { - 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, - }; + var emailRequest = BuildEmailRequest(item, supplier, rfq[0].Token, isProd); + await _canvass.SendRFQ(emailRequest); - await _canvass.SearchingUpdate(item.PRDetailsId); - await _canvass.SendRFQ(supplierEmailRequest); - - supplierResults.Add(new + anySuccess = true; + results.Add(new { item = item.ItemName, supplier = supplier.SupplierName, email = supplier.EmailAddress, - canvassNo = canvassDto.CanvassNo + canvassNo = canvassDto.CanvassNo, + isExisting = matchedId.HasValue }); } + catch (Exception) { continue; } } - return Ok(new - { - totalProcessed = supplierResults.Count, - suppliers = supplierResults - }); - } + 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 = _configuration["WebEndPoint:SupplierForm"] ?? "", + Token = token, + SupplierName = supplier.SupplierName, + Purchaser = item.FullName, + IsCanvass = true, + }; [HttpPost("PostSuggestedSupp")] public async Task PostSuggestedSupp(CanvassDto CanvassDto) { diff --git a/CPRNIMS.WebApi/Security/ClaimsPrincipalExtensions.cs b/CPRNIMS.WebApi/Security/ClaimsPrincipalExtensions.cs index 40b119d..80b6e90 100644 --- a/CPRNIMS.WebApi/Security/ClaimsPrincipalExtensions.cs +++ b/CPRNIMS.WebApi/Security/ClaimsPrincipalExtensions.cs @@ -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() diff --git a/CPRNIMS.WebApps/Controllers/Canvass/CanvassMgmtController.cs b/CPRNIMS.WebApps/Controllers/Canvass/CanvassMgmtController.cs index 8297b3b..2103ac5 100644 --- a/CPRNIMS.WebApps/Controllers/Canvass/CanvassMgmtController.cs +++ b/CPRNIMS.WebApps/Controllers/Canvass/CanvassMgmtController.cs @@ -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 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 PostCanvass(CanvassVM viewModel, List CanvassList) { if (CanvassList.Count > 0) @@ -57,63 +68,45 @@ namespace CPRNIMS.WebApps.Controllers.Canvass List SupplierList) { var postPutItem = new CanvassVM(); - try - { - if (SupplierList.Count > 0) - { - viewModel.SupplierList = new SupplierList - { - SupplierId = SupplierList.SelectMany(ic => ic.SupplierId).ToList(), - }; - postPutItem = await _canvass.PostTaggingSupplier(GetUser(), viewModel); - if (postPutItem.messCode != 0) - { - return Json(new { success = true }); - } - } - else - { - return Json(new { success = false, response = "Array Empty" }); - } - return Json(new { success = false }); - } - catch (Exception ex) + if (SupplierList.Count > 0) { - var message = ex.InnerException?.ToString() ?? ex.Message.ToString(); - - return Json(new { success = false, response = postPutItem.errMessage }); + viewModel.SupplierList = new SupplierList + { + SupplierId = SupplierList.SelectMany(ic => ic.SupplierId).ToList(), + }; + postPutItem = await _canvass.PostTaggingSupplier(GetUser(), viewModel); + if (postPutItem.messCode != 0) + { + return Json(new { success = true }); + } } + else + { + return Json(new { success = false, response = "Array Empty" }); + } + return Json(new { success = false }); } public async Task PostPutItemTagging(CanvassVM viewModel, List ItemList) { - var postPutItem = new CanvassVM(); - try + + if (ItemList.Count > 0) { - if (ItemList.Count > 0) + viewModel.ItemList = new ItemList { - viewModel.ItemList = new ItemList - { - ItemNo = ItemList.SelectMany(ic => ic.ItemNo).ToList(), - }; - postPutItem = await _canvass.PostPutItemTagging(GetUser(), viewModel); - if (postPutItem.messCode != 0) - { - return Json(new { success = true }); - } - } - else + ItemNo = ItemList.SelectMany(ic => ic.ItemNo).ToList(), + }; + var postPutItem = await _canvass.PostPutItemTagging(GetUser(), viewModel); + if (postPutItem.messCode != 0) { - return Json(new { success = false, response = "Array Empty" }); + return Json(new { success = true }); } - return Json(new { success = false }); } - catch (Exception ex) + else { - var message = ex.InnerException?.ToString() ?? ex.Message.ToString(); - - return Json(new { success = false, response = postPutItem.errMessage }); + return Json(new { success = false, response = "Array Empty" }); } + return Json(new { success = false }); } public async Task PostApprovedSupp(CanvassVM viewModel) { @@ -181,6 +174,31 @@ namespace CPRNIMS.WebApps.Controllers.Canvass return Json(new { success = false, Response = postPutItem.errMessage }); } + [HttpPost] + public async Task 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 GetItemSupplierWOEmail(long PRNo) diff --git a/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForCanvass.cshtml b/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForCanvass.cshtml index b427133..3af6466 100644 --- a/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForCanvass.cshtml +++ b/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForCanvass.cshtml @@ -123,8 +123,7 @@ // View Canvass // grid.innerHTML = data.map(item => - H.buildCardHtml(item, i => ` - + H.buildCardHtml(item, i => ` `) @@ -133,17 +132,41 @@ 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 sendEmail(id) { - console.log("Send email:", 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); + } + }, + error: errorHandler + }, beforeComplete(loader))); + } + }); + } fetchData(); })(); diff --git a/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForTagging.cshtml b/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForTagging.cshtml index 0a37114..0d5b7ba 100644 --- a/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForTagging.cshtml +++ b/CPRNIMS.WebApps/Views/Components/CanvassMgmt/CanvassingTabPage/ForTagging.cshtml @@ -79,7 +79,6 @@
- + + \ No newline at end of file diff --git a/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassScript.cshtml b/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassScript.cshtml index 61b8075..197cacd 100644 --- a/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassScript.cshtml +++ b/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassScript.cshtml @@ -15,7 +15,7 @@ - + diff --git a/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassStyles.cshtml b/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassStyles.cshtml index 0d16767..0836cbe 100644 --- a/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassStyles.cshtml +++ b/CPRNIMS.WebApps/Views/Shared/PagesView/Canvass/_CanvassStyles.cshtml @@ -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; + } diff --git a/CPRNIMS.WebApps/wwwroot/JsFunctions/Canvass/CanvassViewV6.js b/CPRNIMS.WebApps/wwwroot/JsFunctions/Canvass/CanvassViewV7.js similarity index 99% rename from CPRNIMS.WebApps/wwwroot/JsFunctions/Canvass/CanvassViewV6.js rename to CPRNIMS.WebApps/wwwroot/JsFunctions/Canvass/CanvassViewV7.js index a1d3700..27162c6 100644 --- a/CPRNIMS.WebApps/wwwroot/JsFunctions/Canvass/CanvassViewV6.js +++ b/CPRNIMS.WebApps/wwwroot/JsFunctions/Canvass/CanvassViewV7.js @@ -291,6 +291,8 @@ function viewSuppDetail(data) { $('#viewItemSuppliers').modal('show'); $('#viewItemSuppliers').css('z-index', 1065); + console.log('dito diba???'); + tableName = '#SupplierDataTable'; totalSelectedLabel = $('#totalSelected');