PR item addition in existing PRNo
This commit is contained in:
parent
84bcc7aaa7
commit
d23347c299
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
21
CPRNIMS.Infrastructure/Dto/SMTP/EmailValidationResult.cs
Normal file
21
CPRNIMS.Infrastructure/Dto/SMTP/EmailValidationResult.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
176
CPRNIMS.Infrastructure/Helper/EmailValidationService.cs
Normal file
176
CPRNIMS.Infrastructure/Helper/EmailValidationService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
14
CPRNIMS.Infrastructure/ViewModel/PR/PRItemListRequest.cs
Normal file
14
CPRNIMS.Infrastructure/ViewModel/PR/PRItemListRequest.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CPRNIMS.Infrastructure.ViewModel.PR
|
||||
{
|
||||
public class PRItemListRequest
|
||||
{
|
||||
public List<long>? ItemNo { get; set; }
|
||||
public List<decimal>? Qty { get; set; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
Loading…
Reference in New Issue
Block a user