PR item addition in existing PRNo

This commit is contained in:
rowell_m_soriano 2026-03-02 12:25:08 +08:00
parent 84bcc7aaa7
commit d23347c299
19 changed files with 540 additions and 160 deletions

View File

@ -31,6 +31,8 @@ namespace CPRNIMS.Domain.Contracts.PR
Task<List<PRDto>> GetApproverName(PRDto PRDto);
Task<List<PRDto>> GetApproverNameByPRNo(PRDto PRDto);
Task<List<ProjectCodes>> GetProjectCodes(PRDto pRDto);
Task<List<RemovedPR>> GetRemovedPR(PRDto pRDto);
Task<List<ApprovedPR>> GetApprovedPR(PRDto pRDto);
Task<MessageResponse> PRItemRemoval(PRDto pRDto);
Task<PRDetails> PostPRApproveReject(PRDto PRDto);
Task<PRDetails> PostPutReceiving(PRDto PRDto);
@ -39,7 +41,6 @@ namespace CPRNIMS.Domain.Contracts.PR
Task<PRDetails> PostPutDeniedItem(PRDto PRDto);
Task<AlternativeOfferDetails> PutSupplierAlterOffer(PRDto pRDto);
Task<ResponseObject> PostPutProjectCode(PRDto prDto);
Task<List<RemovedPR>> GetRemovedPR(PRDto pRDto);
Task<List<ApprovedPR>> GetApprovedPR(PRDto pRDto);
Task<ResponseObject> PostItemInPR(PRDto dto);
}
}

View File

@ -135,7 +135,8 @@ namespace CPRNIMS.Domain.Services.Items
public async Task<List<ItemList>> GetItemList(ItemCodeDto itemCode)
{
var allItems = await _dbContext.ItemList
.FromSqlRaw($"EXEC GetItemList @UserId = '{itemCode.UserId}'")
.FromSqlRaw($"EXEC GetItemList @UserId",
new SqlParameter("@UserId",itemCode.UserId))
.ToListAsync();
return allItems ?? new List<ItemList>();

View File

@ -4,16 +4,16 @@ using CPRNIMS.Infrastructure.Dto.PR;
using CPRNIMS.Infrastructure.Entities.Common;
using CPRNIMS.Infrastructure.Entities.Purchasing;
using CPRNIMS.Infrastructure.Entities.SMTP;
using CPRNIMS.Infrastructure.Models.Common;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using static CPRNIMS.Domain.Services.OutputParamMessage;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using CPRNIMS.Infrastructure.Models.Common;
using static CPRNIMS.Domain.Services.OutputParamMessage;
namespace CPRNIMS.Domain.Services.PR
{
@ -25,6 +25,37 @@ namespace CPRNIMS.Domain.Services.PR
_dbContext = dbContext;
}
#region Get
public async Task<List<NotificationById>> GetNotificationById(PRDto PRDto)
{
var allItems = await _dbContext.NotificationByIds
.FromSqlRaw("EXEC GetNotificationById @UserId,@PRDetailsId,@AppsModuleId",
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId),
new SqlParameter("@AppsModuleId", PRDto.AppsModuleId))
.ToListAsync();
return allItems ?? new List<NotificationById>();
}
public async Task<List<ProjectCodes>> GetProjectCodes(PRDto pRDto)
{
return await _dbContext.ProjectCodes.ToListAsync();
}
public async Task<List<RemovedPR>> GetRemovedPR(PRDto pRDto)
{
var allItems = await _dbContext.RemovedPRs
.FromSqlRaw("EXEC GetRemovedPR @UserId",
new SqlParameter("@UserId", pRDto.UserId)).ToListAsync();
return allItems ?? new List<RemovedPR>();
}
public async Task<List<ApprovedPR>> GetApprovedPR(PRDto pRDto)
{
var allItems = await _dbContext.ApprovedPrs
.FromSqlRaw("EXEC GetApprovedPR @UserId",
new SqlParameter("@UserId", pRDto.UserId)).ToListAsync();
return allItems ?? new List<ApprovedPR>();
}
public async Task<List<Infrastructure.Entities.Purchasing.PRList>> GetAllPR(PRDto PRDto)
{
var allItems = await _dbContext.PRLists
@ -202,90 +233,58 @@ namespace CPRNIMS.Domain.Services.PR
#region Post Put
public async Task<PRDetails> PostPRApproveReject(PRDto PRDto)
{
try
{
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPRApproveReject @UserId, @ItemNo, @Status, @PRDetailsId, @Remarks",
new SqlParameter("@ItemNo", PRDto.ItemNo != null ? PRDto.ItemNo : 0L),
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@Status", PRDto.Status),
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId),
new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A"));
return new PRDetails();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPRApproveReject @UserId, @ItemNo, @Status, @PRDetailsId, @Remarks",
new SqlParameter("@ItemNo", PRDto.ItemNo != null ? PRDto.ItemNo : 0L),
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@Status", PRDto.Status),
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId),
new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A"));
return new PRDetails();
}
public async Task<PRDetails> PutItemDetail(PRDto PRDto)
{
try
{
await _dbContext.Database
.ExecuteSqlRawAsync($"EXEC PutPRItemDetail @UserId, @ItemLocalId, @UOMId, @ItemColorId," +
$"@Qty,@ItemCategoryId,@PRDetailsId,@Remarks,@ItemName,@ItemDescription",
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId != null ? PRDto.PRDetailsId : 0L),
new SqlParameter("@ItemLocalId", PRDto.ItemLocalId),
new SqlParameter("@UOMId", PRDto.UOMId),
new SqlParameter("@ItemColorId", PRDto.ItemColorId),
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@Qty", PRDto.Qty),
new SqlParameter("@ItemCategoryId", PRDto.ItemCategoryId),
new SqlParameter("@Remarks", PRDto.Remarks),
new SqlParameter("@ItemName", PRDto.ItemName),
new SqlParameter("@ItemDescription", PRDto.ItemDescription));
return new PRDetails();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
await _dbContext.Database
.ExecuteSqlRawAsync($"EXEC PutPRItemDetail @UserId, @ItemLocalId, @UOMId, @ItemColorId," +
$"@Qty,@ItemCategoryId,@PRDetailsId,@Remarks,@ItemName,@ItemDescription",
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId != null ? PRDto.PRDetailsId : 0L),
new SqlParameter("@ItemLocalId", PRDto.ItemLocalId),
new SqlParameter("@UOMId", PRDto.UOMId),
new SqlParameter("@ItemColorId", PRDto.ItemColorId),
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@Qty", PRDto.Qty),
new SqlParameter("@ItemCategoryId", PRDto.ItemCategoryId),
new SqlParameter("@Remarks", PRDto.Remarks),
new SqlParameter("@ItemName", PRDto.ItemName),
new SqlParameter("@ItemDescription", PRDto.ItemDescription));
return new PRDetails();
}
public async Task<PRDetails> PostPutDeniedItem(PRDto PRDto)
{
try
{
await _dbContext.Database
await _dbContext.Database
.ExecuteSqlRawAsync("EXEC PostPutDeniedItem @UserId,@PRDetailsId,@Remarks",
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId),
new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A"));
return new PRDetails();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
return new PRDetails();
}
public async Task<PRDetails> PostPutReceiving(PRDto PRDto)
{
try
{
await _dbContext.Database
.ExecuteSqlRawAsync($"EXEC PostPutReceiving @UserId, @PONo, @POTypeId, @EmailAddress, @DRNo, @DocTypeId, @QuantityReceived,@RRNo,@PRDetailsId,@Remarks,@ReceivedDate,@IsCompleted",
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@PONo", PRDto.PONo),
new SqlParameter("@POTypeId", PRDto.POTypeId),
new SqlParameter("@EmailAddress", PRDto.EmailAddress),
new SqlParameter("@DRNo", PRDto.DRNo),
new SqlParameter("@DocTypeId", PRDto.DocTypeId),
new SqlParameter("@QuantityReceived", PRDto.QuantityReceived),
new SqlParameter("@RRNo", PRDto.RRNo),
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId),
new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A"),
new SqlParameter("@ReceivedDate", PRDto.ReceivedDate),
new SqlParameter("@IsCompleted", PRDto.IsCompleted));
return new PRDetails();
}
catch (SqlException ex)
{
ex.ToString();
throw;
}
await _dbContext.Database
.ExecuteSqlRawAsync($"EXEC PostPutReceiving @UserId, @PONo, @POTypeId, @EmailAddress, @DRNo, @DocTypeId, @QuantityReceived,@RRNo,@PRDetailsId,@Remarks,@ReceivedDate,@IsCompleted",
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@PONo", PRDto.PONo),
new SqlParameter("@POTypeId", PRDto.POTypeId),
new SqlParameter("@EmailAddress", PRDto.EmailAddress),
new SqlParameter("@DRNo", PRDto.DRNo),
new SqlParameter("@DocTypeId", PRDto.DocTypeId),
new SqlParameter("@QuantityReceived", PRDto.QuantityReceived),
new SqlParameter("@RRNo", PRDto.RRNo),
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId),
new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A"),
new SqlParameter("@ReceivedDate", PRDto.ReceivedDate),
new SqlParameter("@IsCompleted", PRDto.IsCompleted));
return new PRDetails();
}
public async Task<PRDetails> PutPOClose(PRDto PRDto)
{
@ -310,21 +309,14 @@ namespace CPRNIMS.Domain.Services.PR
new SqlParameter("@CanvassDetailId", pRDto.CanvassDetailId));
return new AlternativeOfferDetails();
}
public async Task<List<NotificationById>> GetNotificationById(PRDto PRDto)
private async Task<bool> IsUsingAsync(int projectCodeId)
{
var allItems = await _dbContext.NotificationByIds
.FromSqlRaw("EXEC GetNotificationById @UserId,@PRDetailsId,@AppsModuleId",
new SqlParameter("@UserId", PRDto.UserId),
new SqlParameter("@PRDetailsId", PRDto.PRDetailsId),
new SqlParameter("@AppsModuleId", PRDto.AppsModuleId))
.ToListAsync();
return allItems ?? new List<NotificationById>();
}
public async Task<List<ProjectCodes>> GetProjectCodes(PRDto pRDto)
{
return await _dbContext.ProjectCodes.ToListAsync();
return await (from pr in _dbContext.PRs
join pod in _dbContext.PODetails on pr.PRNo equals pod.PRNo
where pr.ProjectCodeId == projectCodeId
&& !pod.IsRemoved
&& pr.IsActive
select pr).AnyAsync();
}
public async Task<MessageResponse> PRItemRemoval(PRDto prDto)
{
@ -396,40 +388,27 @@ namespace CPRNIMS.Domain.Services.PR
success = true
};
}
private async Task<bool> IsUsingAsync(int projectCodeId)
public async Task<ResponseObject> PostItemInPR(PRDto dto)
{
try
var (messCode, message) = CreateOutputParams();
await _dbContext.Database.ExecuteSqlRawAsync(
"EXEC PostItemInPR @UserId,@ItemNo,@Qty,@PRNo,@MessCode OUTPUT,@Message OUTPUT",
new SqlParameter("@UserId", dto.UserId),
new SqlParameter("@ItemNo", dto.ItemNo),
new SqlParameter("@Qty", dto.Qty),
new SqlParameter("@PRNo", dto.PRNo),
messCode,
message
);
var response = new ResponseObject
{
return await (from pr in _dbContext.PRs
join pod in _dbContext.PODetails on pr.PRNo equals pod.PRNo
where pr.ProjectCodeId == projectCodeId
&& !pod.IsRemoved
&& pr.IsActive
select pr).AnyAsync();
}
catch (Exception ex)
{
ex.ToString();
throw;
}
}
public async Task<List<RemovedPR>> GetRemovedPR(PRDto pRDto)
{
var allItems = await _dbContext.RemovedPRs
.FromSqlRaw("EXEC GetRemovedPR @UserId",
new SqlParameter("@UserId", pRDto.UserId)).ToListAsync();
return allItems ?? new List<RemovedPR>();
}
public async Task<List<ApprovedPR>> GetApprovedPR(PRDto pRDto)
{
var allItems = await _dbContext.ApprovedPrs
.FromSqlRaw("EXEC GetApprovedPR @UserId",
new SqlParameter("@UserId", pRDto.UserId)).ToListAsync();
return allItems ?? new List<ApprovedPR>();
message = message.Value?.ToString(),
messCode = Convert.ToByte(messCode.Value)
};
return response;
}
#endregion

View File

@ -44,6 +44,7 @@ namespace CPRNIMS.Domain.UIContracts.PR
Task<PRVM> ApprovedSelectedPRItem(User user, PRVM viewModel);
Task<PRVM> PostPutProjectCode(User user, PRVM viewModel);
Task<PRVM> PostPutAttachment(User user, PRVM prVM);
Task<PRVM> PostItemInPR(User user, PRVM viewModel);
#endregion
}
}

View File

@ -7,7 +7,6 @@ using CPRNIMS.Infrastructure.ViewModel.PR;
using Microsoft.Extensions.Configuration;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CPRNIMS.Domain.UIServices.PR
{
@ -286,6 +285,12 @@ namespace CPRNIMS.Domain.UIServices.PR
return await SendPostApiRequest(user, prVM,
_configuration["LLI:NonInvent:PRMgmt:PostPutAttachment"]);
}
public async Task<PRVM> PostItemInPR(User user, PRVM prVM)
{
return await SendPostApiRequest(user, prVM,
_configuration["LLI:NonInvent:PRMgmt:PostItemInPR"]);
}
#endregion
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Dto.SMTP
{
public class EmailValidationResult
{
public string Email { get; set; } = string.Empty;
public bool IsValid { get; set; }
public string Reason { get; set; } = string.Empty;
public static EmailValidationResult Pass(string email) =>
new() { Email = email, IsValid = true, Reason = "OK" };
public static EmailValidationResult Fail(string email, string reason) =>
new() { Email = email, IsValid = false, Reason = reason };
}
}

View File

@ -0,0 +1,176 @@
using CPRNIMS.Infrastructure.Dto.SMTP;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace CPRNIMS.Infrastructure.Helper
{
public class EmailValidationService
{
// In-memory cache for MX lookups { domain -> (isValid, timestamp) }
private static readonly Dictionary<string, (bool IsValid, DateTime CachedAt)> _mxCache = new();
private static readonly TimeSpan _mxCacheExpiry = TimeSpan.FromHours(24);
// Persistent bounce list - load from your DB or file
private static readonly HashSet<string> _bounceList = new(StringComparer.OrdinalIgnoreCase);
// Known disposable/fake email domains (expand this list as needed)
private static readonly HashSet<string> _disposableDomains = new(StringComparer.OrdinalIgnoreCase)
{
// Disposable/temp mail providers
"mailinator.com", "guerrillamail.com", "tempmail.com", "throwaway.email",
"yopmail.com", "sharklasers.com", "guerrillamailblock.com", "grr.la",
"guerrillamail.info", "spam4.me", "trashmail.com", "trashmail.me",
"trashmail.net", "trashmail.at", "trashmail.io", "trashmail.org",
"fakeinbox.com", "mailnull.com", "spamgourmet.com", "spamgourmet.net",
"spamgourmet.org", "maildrop.cc", "dispostable.com", "mailexpire.com",
"spamex.com", "deadaddress.com", "spamfree24.org", "no-spam.ws",
"discard.email", "despam.it", "emailsensei.com", "getonemail.com",
"spamfree.eu", "spaml.com", "spammotel.com", "spamspot.com",
"tempe-mail.com", "tempinbox.com", "temp-mail.org", "emailtemporanea.net",
"crazymailing.com", "dispostable.com", "spamgob.com", "0-mail.com",
"0815.ru", "0clickemail.com", "10minutemail.com", "20minutemail.com",
"filzmail.com", "getnada.com", "incognitomail.com", "jetable.fr.nf",
"lortemail.dk", "mytempemail.com", "noclickemail.com", "nowmymail.com",
"objectmail.com", "odaymail.com", "onewaymail.com", "pookmail.com",
"privacy.net", "proxymail.eu", "rcpt.at", "rklips.com",
"shortmail.net", "sogetthis.com", "spamgob.com", "spaml.de",
"speed.1s.fr", "supergreatmail.com", "suremail.info", "tempail.com",
"tempemail.net", "temporarioemail.com.br", "thanksnospam.info",
"thisisnotmyrealemail.com", "throwam.com", "trbvm.com", "truckmail.com",
"tyldd.com", "uggsrock.com", "veryrealemail.com", "vidchart.com",
"wegwerfmail.de", "wegwerfmail.net", "wegwerfmail.org", "wh4f.org",
"whyspam.me", "willselfdestruct.com", "wronghead.com", "wuzupmail.net",
"xagloo.com", "xemaps.com", "xents.com", "xmaily.com", "xoxy.net",
"yep.it", "yogamaven.com", "yuurok.com", "zehnminutenmail.de",
"zippymail.info", "zoaxe.com", "zoemail.net", "zoemail.org"
};
// Known legitimate domains — skip MX lookup to save time
private static readonly HashSet<string> _trustedDomains = new(StringComparer.OrdinalIgnoreCase)
{
"gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "live.com",
"icloud.com", "me.com", "mac.com", "msn.com", "aol.com",
"protonmail.com", "proton.me", "zoho.com", "yandex.com",
"fastmail.com", "hey.com", "tutanota.com"
};
/// <summary>
/// Fast multi-layer email validation without SMTP handshake.
/// Average time per email: ~5-50ms (MX lookup only on unknown domains, cached after first hit).
/// </summary>
public async Task<EmailValidationResult> ValidateAsync(string email)
{
email = email.Trim().ToLowerInvariant();
// LAYER 1: Regex format check (instant)
if (!IsValidFormat(email))
return EmailValidationResult.Fail(email, "Invalid email format");
var domain = email.Split('@')[1];
// LAYER 2: Bounce list check (instant - known bad addresses from past sends)
if (_bounceList.Contains(email))
return EmailValidationResult.Fail(email, "Previously bounced email address");
// LAYER 3: Disposable/fake domain check (instant - local hashset lookup)
if (_disposableDomains.Contains(domain))
return EmailValidationResult.Fail(email, $"Disposable email domain: {domain}");
// LAYER 4: Trusted domain fast-pass (instant - skip DNS for known providers)
if (_trustedDomains.Contains(domain))
return EmailValidationResult.Pass(email);
// LAYER 5: MX Record check with caching (fast after first lookup)
bool hasMx = await HasValidMxRecordAsync(domain);
if (!hasMx)
return EmailValidationResult.Fail(email, $"No MX record found for domain: {domain}");
return EmailValidationResult.Pass(email);
}
/// <summary>
/// Validates a batch of emails concurrently for performance.
/// </summary>
public async Task<List<EmailValidationResult>> ValidateBatchAsync(IEnumerable<string> emails)
{
var tasks = emails.Select(e => ValidateAsync(e));
var results = await Task.WhenAll(tasks);
return results.ToList();
}
/// <summary>
/// Call this after a send attempt — records bounced addresses so they're
/// skipped automatically in future sends.
/// </summary>
public void RecordBounce(string email)
{
_bounceList.Add(email.Trim().ToLowerInvariant());
Console.WriteLine($"[BounceTracker] Recorded bounced email: {email}");
// TODO: Persist to your database here
// await _dbContext.BouncedEmails.AddAsync(new BouncedEmail { Address = email, Date = DateTime.UtcNow });
}
private bool IsValidFormat(string email)
{
var regex = new Regex(
@"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$",
RegexOptions.IgnoreCase
);
return regex.IsMatch(email);
}
private async Task<bool> HasValidMxRecordAsync(string domain)
{
// Return cached result if still fresh
if (_mxCache.TryGetValue(domain, out var cached))
{
if (DateTime.UtcNow - cached.CachedAt < _mxCacheExpiry)
return cached.IsValid;
}
try
{
var processInfo = new System.Diagnostics.ProcessStartInfo("nslookup")
{
Arguments = $"-type=MX {domain}",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = System.Diagnostics.Process.Start(processInfo);
if (process == null)
{
_mxCache[domain] = (true, DateTime.UtcNow); // assume valid on error
return true;
}
// Timeout nslookup after 5 seconds
var outputTask = process.StandardOutput.ReadToEndAsync();
if (await Task.WhenAny(outputTask, Task.Delay(5000)) != outputTask)
{
Console.WriteLine($"[MX] DNS lookup timed out for {domain}, assuming valid.");
_mxCache[domain] = (true, DateTime.UtcNow);
return true;
}
string output = await outputTask;
bool hasMx = Regex.IsMatch(output, @"mail exchanger", RegexOptions.IgnoreCase);
_mxCache[domain] = (hasMx, DateTime.UtcNow);
Console.WriteLine($"[MX] {domain} → {(hasMx ? "Valid MX" : "No MX found")} (cached)");
return hasMx;
}
catch (Exception ex)
{
Console.WriteLine($"[MX] Lookup error for {domain}: {ex.Message}");
_mxCache[domain] = (true, DateTime.UtcNow); // assume valid on error
return true;
}
}
}
}

View File

@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
@ -12,8 +13,10 @@ namespace CPRNIMS.Infrastructure.Helper
public class SMTPHelper
{
private readonly IConfiguration _configuration;
public SMTPHelper(IConfiguration configuration)
private readonly EmailValidationService _emailValidator;
public SMTPHelper(EmailValidationService emailValidator, IConfiguration configuration)
{
_emailValidator = emailValidator;
_configuration = configuration;
}
@ -49,58 +52,104 @@ namespace CPRNIMS.Infrastructure.Helper
return false;
}
// 👇 Reads live from appsettings.json every time — picks up changes without restart
var excludedEmails = new HashSet<string>(
_configuration.GetSection("Canvass:EmailSettings:ExcludedEmails")
.Get<List<string>>() ?? new List<string>(),
StringComparer.OrdinalIgnoreCase
);
using (MailMessage myMessage = new MailMessage())
{
// Split the recipient string by semicolon and add each email address individually
var recipients = emailMessageBody.Recipient.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var recipient in recipients)
var recipientList = emailMessageBody.Recipient
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(r => r.Trim())
.Where(r => !string.IsNullOrWhiteSpace(r))
.Where(r =>
{
if (excludedEmails.Contains(r))
{
Console.WriteLine($"[Excluded] Skipping: {r}");
return false;
}
return true;
});
// Validate remaining recipients concurrently
var validationResults = await _emailValidator.ValidateBatchAsync(recipientList);
foreach (var result in validationResults)
{
myMessage.To.Add(recipient.Trim());
if (result.IsValid)
myMessage.To.Add(result.Email);
else
Console.WriteLine($"[Skipped] {result.Reason}: {result.Email}");
}
// myMessage.To.Add(emailMessageBody.Recipient);
if (myMessage.To.Count == 0)
{
Console.WriteLine("No valid recipients after exclusion/validation. Aborting.");
return false;
}
myMessage.Sender = new MailAddress(emailMessageBody.SenderEmail);
myMessage.From = new MailAddress(emailMessageBody.SenderEmail, emailMessageBody.DisplayName);
var cc = emailMessageBody.CC.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
if(cc.Length > 0)
// CC with exclusion too
if (!string.IsNullOrWhiteSpace(emailMessageBody.CC))
{
foreach (var ccs in cc)
var ccList = emailMessageBody.CC
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Trim())
.Where(c => !string.IsNullOrWhiteSpace(c))
.Where(c =>
{
if (excludedEmails.Contains(c))
{
Console.WriteLine($"[Excluded CC] Skipping: {c}");
return false;
}
return true;
});
var ccValidationResults = await _emailValidator.ValidateBatchAsync(ccList);
foreach (var result in ccValidationResults)
{
myMessage.CC.Add(ccs.Trim());
if (result.IsValid)
myMessage.CC.Add(result.Email);
else
Console.WriteLine($"[Skipped CC] {result.Reason}: {result.Email}");
}
}
myMessage.Subject = emailMessageBody.Subject;
// Ensure the email body is correctly formatted HTML
myMessage.Subject = emailMessageBody.Subject;
myMessage.Body = emailMessageBody.Message;
myMessage.IsBodyHtml = true;
if (emailMessageBody.IsCanvass)
if (emailMessageBody.IsCanvass && File.Exists(emailMessageBody.AttachPath))
{
if (File.Exists(emailMessageBody.AttachPath))
{
Attachment pdfAttachment = new Attachment(emailMessageBody.AttachPath);
pdfAttachment.Name = Path.GetFileName(emailMessageBody.AttachPath);
myMessage.Attachments.Add(pdfAttachment);
}
Attachment pdfAttachment = new Attachment(emailMessageBody.AttachPath);
pdfAttachment.Name = Path.GetFileName(emailMessageBody.AttachPath);
myMessage.Attachments.Add(pdfAttachment);
}
using (SmtpClient smtp = new SmtpClient(emailMessageBody.Server))
{
smtp.Port = emailMessageBody.OutGoingPort;
smtp.UseDefaultCredentials = emailMessageBody.IsSuccess;
smtp.Credentials = new System.Net.NetworkCredential(emailMessageBody.UserName, emailMessageBody.NewPassword);
smtp.Credentials = new NetworkCredential(emailMessageBody.UserName, emailMessageBody.NewPassword);
smtp.EnableSsl = true;
try
{
await smtp.SendMailAsync(myMessage);
Console.WriteLine("Email sent successfully.");
Console.WriteLine($"Email sent successfully to {myMessage.To.Count} recipient(s).");
return true;
}
catch (Exception ex)
{
var Message = ex.InnerException?.ToString() ?? ex.Message.ToString();
Console.WriteLine($"Error sending email: {Message}");
var message = ex.InnerException?.ToString() ?? ex.Message;
Console.WriteLine($"Error sending email: {message}");
return false;
}
}
@ -109,8 +158,8 @@ namespace CPRNIMS.Infrastructure.Helper
catch (Exception ex)
{
IsAuthError = true;
var Message = ex.InnerException?.ToString() ?? ex.Message.ToString();
Console.WriteLine($"Error in SendEmailAsync: {Message}");
var message = ex.InnerException?.ToString() ?? ex.Message;
Console.WriteLine($"Error in SendEmailAsync: {message}");
return false;
}
}

View File

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

View File

@ -137,5 +137,6 @@ namespace CPRNIMS.Infrastructure.ViewModel.PR
public int RemainingDays { get; set; }
public ItemReceivingList? ItemList { get; set; }
public PRList? PRList { get; set; }
public PRItemListRequest? PRItemListRequest { get; set; }
}
}

View File

@ -161,6 +161,7 @@ namespace CPRNIMS.WebApi.Common
services.AddScoped<IForgotPassword,Domain.Services.Account.ForgotPassword>();
services.AddScoped<IAccount, Account>();
services.AddScoped<SMTPHelper>();
services.AddScoped<EmailValidationService>();
}
}
}

View File

@ -38,6 +38,35 @@ namespace CPRNIMS.WebApi.Controllers.PR
_item = item;
}
#region POST PUT
[HttpPost("PostItemInPR")]
public async Task<IActionResult> PostItemInPR([FromBody] PRVM PRDto)
{
var results = new ResponseObject();
if (PRDto.PRItemListRequest?.ItemNo?.Count > 0)
{
for (int i = 0; i < PRDto.PRItemListRequest.ItemNo.Count; i++)
{
var dto = new PRDto
{
ItemNo = PRDto.PRItemListRequest.ItemNo[i],
Qty = PRDto.PRItemListRequest.Qty[i],
UserId = PRDto.UserId,
PRNo = PRDto.PRNo,
};
var result = await _pRequest.PostItemInPR(dto);
results.messCode = result.messCode;
results.message = result.message;
}
}
return Ok(new ResponseObject()
{
success = results.messCode == 1,
messCode = results.messCode,
message = results.message ?? "Operation completed successfully"
});
}
[HttpPost("PostPutAttachment")]
public async Task<IActionResult> PostPutAttachment([FromBody] PRVM PRDto)
{

View File

@ -148,6 +148,7 @@ namespace CPRNIMS.WebApps.Controllers.PR
return GetResponse(response);
}
#region Mapper
private PRList MapToPRItemList(IEnumerable<PRList> prList)
{
if (prList == null || !prList.Any())
@ -165,9 +166,28 @@ namespace CPRNIMS.WebApps.Controllers.PR
ItemNo = prList.SelectMany(ic => ic.ItemNo).ToList()
};
}
private PRItemListRequest MapToPRItemList(IEnumerable<PRItemListRequest> prItemListRequest)
{
if (prItemListRequest == null || !prItemListRequest.Any())
{
return new PRItemListRequest
{
ItemNo = new List<long>(),
Qty = new List<decimal>(),
};
}
return new PRItemListRequest
{
ItemNo = prItemListRequest.SelectMany(ic => ic.ItemNo).ToList(),
Qty = prItemListRequest.SelectMany(ic => ic.Qty).ToList()
};
}
#endregion
#endregion
#region POST PUT
public async Task<IActionResult?> UploadAttachment(IFormFile? file, [FromForm] string? oldFileName,
public async Task<IActionResult?> UploadAttachment(IFormFile? file, [FromForm]
string? oldFileName,
[FromForm] long prId)
{
var uploadsPath = Path.Combine(
@ -326,6 +346,17 @@ namespace CPRNIMS.WebApps.Controllers.PR
}
return Json(new { success = false, Response = postPutItem.Message });
}
public async Task<IActionResult> PostItemInPR(PRVM viewModel, List<PRItemListRequest> PRItemList)
{
viewModel.PRItemListRequest = MapToPRItemList(PRItemList);
var postPutItem = await _pRequest.PostItemInPR(GetUser(), viewModel);
if (postPutItem.messCode != 0)
{
return Json(new { success = true, Response = postPutItem.Message });
}
return Json(new { success = false, Response = postPutItem.Message });
}
#endregion
#region Views
public IActionResult GetDashBoardById(int DashboardId)

View File

@ -13,7 +13,7 @@
<table id="ItemTable" class="row-border" cellspacing="0" width="100%">
<thead>
<tr>
<th>ItemCodeNo.</th>
<th>ItemNo</th>
<th>ItemName</th>
<th>ItemSpecs</th>
<th>CategoryName</th>

View File

@ -24,6 +24,46 @@
</div>
</div>
<!-- Modal Add New Item in PR -->
<div class="modal fade custom-modal-backdrop" id="viewItemList"
tabindex="-1" aria-labelledby="addItemLabel" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-xl">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header">
<h2 class="modal-title" id="addItemLabel">Item List</h2>
</div>
<div class="modal-body p-4">
<div style="margin-bottom:5px">
<span class="fw-semibold">Selected Items:</span>
<span id="totalSelectedItem" class="badge bg-danger ms-2">0</span>
</div>
<table id="ItemTable" class="row-border" cellspacing="0" width="100%">
<thead>
<tr>
<th>All</th>
<th>ItemNo</th>
<th>ItemName</th>
<th>ItemSpecs</th>
<th>CategoryName</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="modal-footer bg-light border-0 p-3">
<button type="button" class="btn btn-outline-secondary px-4" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-2"></i>Cancel
</button>
<button type="button" id="btnConfirmUpdate" onclick="postItemInPR()" class="btn btn-success px-4">
<i class="bi bi-check-circle me-2"></i>Submit
</button>
</div>
</div>
</div>
</div>
<!-- Modal addRemarksUpdate -->
<div class="modal fade custom-modal-backdrop" id="addRemarksUpdate"
tabindex="-1" aria-labelledby="approveLabel" aria-hidden="true" data-bs-backdrop="static">
@ -76,7 +116,7 @@
<link href="~/css/pr/ButtonStyleV2.css" rel="stylesheet" />
<link href="~/css/pr/PRTabs.css" rel="stylesheet" />
@await Html.PartialAsync("PagesView/PR/_PRTracking")
<script src="~/JsFunctions/PR/PRV8.js"></script>
<script src="~/JsFunctions/PR/PRV9.js"></script>
<script src="~/JsFunctions/PR/PRTabs.js"></script>
@await Html.PartialAsync("PagesView/PR/_PRScripts")
</body>

View File

@ -24,7 +24,7 @@
<script src="~/jsfunctions/po/POViewV4.js"></script>
<script src="~/jsfunctions/po/PopulateDopdownV4.js"></script>
<script src="~/jsfunctions/po/POPutPostV3.js"></script>
<script src="~/jsfunctions/po/rowCallBackV4.js"></script>
<script src="~/jsfunctions/po/rowCallBackV5.js"></script>
<script src="~/jsfunctions/utilities/NewStyle.js"></script>
<script src="~/jsfunctions/utilities/utilsV3.js"></script>

View File

@ -4,11 +4,11 @@
<link href="~/css/common/rowhighlighter.css" rel="stylesheet" />
<script src="~/jsfunctions/pr/PRColumnV8.js"></script>
<script src="~/jsfunctions/pr/PRViewV7.js"></script>
<script src="~/jsfunctions/pr/PRPutPost.js"></script>
<script src="~/jsfunctions/pr/PRViewV8.js"></script>
<script src="~/jsfunctions/pr/PRPostPut.js"></script>
<script src="~/jsfunctions/pr/PRButtonv3.js"></script>
<script src="~/jsfunctions/pr/PRVarV3.js"></script>
<script src="~/jsfunctions/pr/Configv5.js"></script>
<script src="~/jsfunctions/pr/Configv6.js"></script>
<script src="~/jsfunctions/pr/populatedropdown.js"></script>
<script src="~/jsfunctions/pr/prRowCallbackV3.js"></script>
<script src="~/jsfunctions/utilities/columnstyle.js"></script>

View File

@ -327,6 +327,7 @@
</div>
</div>
</div>
<!-- Modal viewPRDetails -->
<div class="modal fade custom-modal-backdrop" id="viewPRDetails"
tabindex="-1" aria-labelledby="ModalLabel" data-bs-backdrop="static">
@ -388,6 +389,13 @@
</div>
</div>
<button type="button"
id="btnAddNewItem"
onclick="viewItemList();"
class="btn btn-add">
Add Item
</button>
<!-- SELECTION SUMMARY -->
<div class="d-flex align-items-center mb-3 gap-2">
<div>
@ -499,6 +507,29 @@
</div>
</div>
</div>
<style>
.btn-add {
background: linear-gradient(135deg, #009688, #00bfa5);
color: #fff;
font-weight: 600;
border: none;
border-radius: 8px;
padding: 10px 20px;
margin-bottom:10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.btn-add:hover {
background: linear-gradient(135deg, #00bfa5, #00796b);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.btn-add:active {
transform: scale(0.97);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
</style>
<input hidden id="roleRights" value="@ViewBag.UserRoles" />