diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/SignatoryInvitationInvocable.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/SignatoryInvitationInvocable.cs new file mode 100644 index 0000000..0c8a272 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Jobs/SignatoryInvitationInvocable.cs @@ -0,0 +1,45 @@ +using Coravel.Invocable; +using Coravel.Mailer.Mail.Interfaces; +using EnotaryoPH.Data; +using EnotaryoPH.Web.Common.Models; +using EnotaryoPH.Web.Mailables; + +namespace EnotaryoPH.Web.Common.Jobs +{ + public class SignatoryInvitationInvocable : IInvocable, IInvocableWithPayload + { + private readonly IMailer _mailer; + private readonly NotaryoDBContext _notaryoDBContext; + private readonly Settings _settings; + + public SignatoryInvitationInvocable(IMailer mailer, NotaryoDBContext notaryoDBContext, Settings settings) + { + _mailer = mailer; + _notaryoDBContext = notaryoDBContext; + _settings = settings; + } + + public Guid Payload { get; set; } + + public async Task Invoke() + { + var signatory = _notaryoDBContext + .TransactionSignatories + .Include(e => e.Transaction) + .ThenInclude(t => t.Principal) + .FirstOrDefault(e => e.TransactionSignatory_UID == Payload); + + var user = signatory.Transaction.Principal; + var name = $"{user.Firstname} {user.Lastname}".NullIfWhiteSpace() ?? user.Email; + var invitationCode = new Guid(signatory.InvitationCode).ToBase64String(); + + await _mailer.SendAsync(new SignatoryInvitationMailable(new SignatoryViewModel + { + Email = signatory.Email, + InvitationURL = $"{_settings.BaseUrl}/Principal/Registration/{invitationCode}", + MainPrincipalName = name, + SignatoryType = signatory.Type + })); + } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Models/Settings.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Models/Settings.cs new file mode 100644 index 0000000..379f300 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Models/Settings.cs @@ -0,0 +1,7 @@ +namespace EnotaryoPH.Web.Common.Models +{ + public class Settings + { + public string BaseUrl { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Mailables/SignatoryInvitationMailable.cs b/EnotaryoPH/EnotaryoPH.Web/Mailables/SignatoryInvitationMailable.cs new file mode 100644 index 0000000..f5f49c6 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Mailables/SignatoryInvitationMailable.cs @@ -0,0 +1,16 @@ +using Coravel.Mailer.Mail; + +namespace EnotaryoPH.Web.Mailables +{ + public class SignatoryInvitationMailable : Mailable + { + private readonly SignatoryViewModel _model; + + public SignatoryInvitationMailable(SignatoryViewModel model) => _model = model; + + public override void Build() => this + .To(_model.Email) + .From("noreply@enotaryoph.com") + .View("~/Views/Mail/SignatoryInvitation.cshtml", _model); + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Mailables/SignatoryViewModel.cs b/EnotaryoPH/EnotaryoPH.Web/Mailables/SignatoryViewModel.cs new file mode 100644 index 0000000..3b26f54 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Mailables/SignatoryViewModel.cs @@ -0,0 +1,10 @@ +namespace EnotaryoPH.Web.Mailables +{ + public class SignatoryViewModel + { + public string Email { get; set; } + public string InvitationURL { get; set; } + public string MainPrincipalName { get; set; } + public string SignatoryType { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/Registration/Index.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/Registration/Index.cshtml new file mode 100644 index 0000000..087fec1 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/Registration/Index.cshtml @@ -0,0 +1,4 @@ +@page "{InvitationCode?}" +@model EnotaryoPH.Web.Pages.Participant.Registration.IndexModel +@{ +} diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/Registration/Index.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/Registration/Index.cshtml.cs new file mode 100644 index 0000000..bc454c9 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/Registration/Index.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EnotaryoPH.Web.Pages.Participant.Registration +{ + public class IndexModel : PageModel + { + public IActionResult OnGet() => Page(); + + [BindProperty(SupportsGet = true)] + public string InvitationCode { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/ChooseNotary.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/ChooseNotary.cshtml new file mode 100644 index 0000000..4553aed --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/ChooseNotary.cshtml @@ -0,0 +1,94 @@ +@page "{Transaction_UID}" +@using EnotaryoPH.Web.Pages.Shared.Components.NotaryoSteps +@model EnotaryoPH.Web.Pages.Principal.NotaryoSteps.ChooseNotaryModel +@{ +} + +@section Head { + +} + +
+
+
+
+

Notaryo Steps

+
+ @await Component.InvokeAsync("NotaryoSteps", new { + NotaryoSteps = new List() { + new NotaryoStep { Name = "Upload Identification", Step = 1 }, + new NotaryoStep { Name = "Take Selfie", Step = 2 }, + new NotaryoStep { Name = "Upload Document", Step = 3 }, + new NotaryoStep { Name = "Choose Notary", Step = 4, IsActive = true }, + } + }) + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
Name of AttorneyOffice Location
Athony Co123 Fictional Road, Manila
Jude Romano342 Fake Road, Marikina
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+ + +@section Scripts { + +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/ChooseNotary.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/ChooseNotary.cshtml.cs new file mode 100644 index 0000000..4cb5f5f --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/ChooseNotary.cshtml.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EnotaryoPH.Web.Pages.Principal.NotaryoSteps +{ + public class ChooseNotaryModel : PageModel + { + public void OnGet() + { + } + + [BindProperty(SupportsGet = true)] + public Guid Transaction_UID { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/SignatoryViewModel.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/SignatoryViewModel.cs new file mode 100644 index 0000000..be2711a --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/SignatoryViewModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace EnotaryoPH.Web.Pages.Principal.NotaryoSteps +{ + public class SignatoryViewModel + { + [EmailAddress, Required, BindProperty] + public string Email { get; set; } + + [Required, BindProperty] + public string Type { get; set; } + + public Guid UID { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml index be607c9..852b7c5 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml @@ -4,6 +4,9 @@ @{ } +@section Head { + +}
@@ -22,7 +25,99 @@ })
+
+
+
+
+ + + @Html.ValidationMessageFor(x => x.DocumentFile) +
+ +
+ +
+
+ +
+ @Html.ValidationMessageFor(x => x.DocumentType) +
+
+
+
+
+ Email + + +
+
+ +
+
    +
+
+
+
+ Email + + +
+
+ +
+
    +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ @Html.ValidationMessageFor(x => x.IsConfirmed) +
+
+ + +
+
+ +
+
-
\ No newline at end of file + + + + +@section Scripts { + + +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.cs index caa1b00..1ce1d27 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.cs @@ -1,15 +1,144 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Coravel.Queuing.Interfaces; +using EnotaryoPH.Data; +using EnotaryoPH.Data.Entities; +using EnotaryoPH.Web.Common.Jobs; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; namespace EnotaryoPH.Web.Pages.Principal.NotaryoSteps { public class UploadDocumentModel : PageModel { - public void OnGet() + private readonly ICurrentUserService _currentUserService; + private readonly NotaryoDBContext _notaryoDBContext; + private readonly IQueue _queue; + + public UploadDocumentModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IQueue queue) { + _notaryoDBContext = notaryoDBContext; + _currentUserService = currentUserService; + _queue = queue; } + public IActionResult OnGet(Guid transaction_UID) + { + var _transaction = _notaryoDBContext.Transactions + .Include(t => t.TransactionDocument) + .Include(t => t.TransactionSignatories) + .AsNoTracking().FirstOrDefault(e => e.Transaction_UID == transaction_UID); + DocumentTypes = GetDocumentTypes(); + CurrentUserEmail = _currentUserService.GetEmail(); + var signatories = _transaction.TransactionSignatories.Select(ts => new SignatoryViewModel + { + Email = ts.Email, + Type = ts.Type, + UID = ts.TransactionSignatory_UID.GetValueOrDefault() + }).ToList(); + ParticipantsJson = JsonSerializer.Serialize(signatories); + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + DocumentTypes = GetDocumentTypes(); + return Page(); + } + + if (!IsConfirmed) + { + ModelState.AddModelError(nameof(IsConfirmed), "You must tick this box to continue."); + DocumentTypes = GetDocumentTypes(); + return Page(); + } + + var transaction = _notaryoDBContext.Transactions + .Include(t => t.TransactionSignatories) + .Include(t => t.TransactionDocument) + .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); + if (transaction == null) + { + return NotFound(); + } + + transaction.Status = nameof(TransactionStatus.DocumentUploaded); + transaction.IsRecorded = IsVideoConferenceRecorded; + + if (transaction.TransactionDocument == null) + { + transaction.TransactionDocument = new TransactionDocument + { + CreatedOn = DateTime.UtcNow, + Transaction = transaction, + TransactionDocument_UID = Guid.CreateVersion7(DateTime.UtcNow), + }; + } + + var stream = new MemoryStream((int)DocumentFile.Length); + DocumentFile.CopyTo(stream); + transaction.TransactionDocument.File = stream.ToArray(); + transaction.TransactionDocument.Filename = DocumentFile.FileName; + transaction.TransactionDocument.DocumentType = DocumentType; + transaction.TransactionDocument.UploadedOn = DateTime.UtcNow; + + var participants = JsonSerializer.Deserialize>(ParticipantsJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? []; + transaction.TransactionSignatories = participants.Select(p => new TransactionSignatory + { + CreatedOn = DateTime.UtcNow, + Email = p.Email, + Status = nameof(SignatoryStatus.New), + TransactionSignatory_UID = Guid.CreateVersion7(DateTime.UtcNow), + Type = p.Type, + InvitationCode = Guid.CreateVersion7(DateTime.UtcNow).ToString() + }).ToList(); + + _notaryoDBContext.Update(transaction); + _notaryoDBContext.SaveChanges(); + + foreach (var signatory in transaction.TransactionSignatories) + { + _queue.QueueInvocableWithPayload(signatory.TransactionSignatory_UID.GetValueOrDefault()); + } + + return Redirect($"/Principal/NotaryoSteps/ChooseNotary/{Transaction_UID}"); + } + + private List GetDocumentTypes() + { + var lookupIdentificationTypes = _notaryoDBContext.LookupData.AsNoTracking().Include(e => e.LookupDataValues).FirstOrDefault(e => e.Name == "Document Types"); + return lookupIdentificationTypes.LookupDataValues + .ConvertAll(m => new SelectListItem + { + Text = m.Title.DefaultIfEmpty(m.Value), + Value = m.Value + }); + } + + public string CurrentUserEmail { get; private set; } + + [BindProperty, Required] + public IFormFile DocumentFile { get; set; } + + [BindProperty, Required] + public string DocumentType { get; set; } + + public List DocumentTypes { get; set; } + + [BindProperty] + public bool IsConfirmed { get; set; } + + [BindProperty] + public bool IsVideoConferenceRecorded { get; set; } + + [BindProperty] + public string ParticipantsJson { get; set; } = "[]"; + [BindProperty(SupportsGet = true)] public Guid Transaction_UID { get; set; } } -} +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.js b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.js new file mode 100644 index 0000000..1ed7919 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.js @@ -0,0 +1,97 @@ +"use strict"; +(function () { + let + control_addAdditionalPrincipalButton = document.getElementById("AddAdditionalPrincipalButton"), + control_additionalPrincipalsList = document.getElementById("AdditionalPrincipalsList"), + control_additionalPrincipalsValidation = document.getElementById("AdditionalPrincipalsValidation"), + control_addWitnessButton = document.getElementById("AddWitnessButton"), + control_currentUserEmail = document.getElementById("CurrentUserEmail"), + control_documentFile = document.getElementById("DocumentFile"), + control_isConfirmed = document.getElementById("IsConfirmed"), + control_newPrincipalEmail = document.getElementById("NewPrincipalEmail"), + control_newWitnessEmail = document.getElementById("NewWitnessEmail"), + control_participantItemTemplate = document.getElementById("ParticipantItemTemplate"), + control_participantsJson = document.getElementById("ParticipantsJson"), + control_uploadDocumentForm = document.getElementById("UploadDocumentForm"), + control_witnessesList = document.getElementById("WitnessesList"), + control_witnessesValidation = document.getElementById("WitnessesValidation"); + + let signatories = []; + + function _addSignatory(control_email, signatoryType, control_signatoryList) { + let email = control_email.value; + + if (!email) { + return "Email is required."; + } + + if (control_currentUserEmail.value == email) { + return "Email must not be the same as the currently logged in user."; + } + + if (signatories.find(x => x.email === email)) { + return "Email already exists."; + } + + signatories.push({ + email: email, + type: signatoryType + }); + + let liTemplate = control_participantItemTemplate.cloneNode(true); + let li = liTemplate.content; + li.querySelector(".participantitem__li").setAttribute("data-uid", ""); + li.querySelector(".participantitem__email").textContent = email; + + let deleteButton = li.querySelector(".participantitem__delete"); + deleteButton.addEventListener("click", _deleteParticipant); + deleteButton.control_signatoryList = control_signatoryList; + control_signatoryList.appendChild(li); + + control_email.value = ""; + } + + function _deleteParticipant(sender) { + let li = sender.target.closest("li"); + let email = li.querySelector(".participantitem__email").textContent; + jfa.components.dialog.confirm({ + message: "Are you sure you want to remove this participant?", + callbackyes: function () { + signatories = signatories.filter(m => m.email != email); + } + }); + } + + function _bindEvents() { + control_addAdditionalPrincipalButton.addEventListener("click", () => { + let err = _addSignatory(control_newPrincipalEmail, 'Principal', control_additionalPrincipalsList); + control_additionalPrincipalsValidation.textContent = err; + }); + control_addWitnessButton.addEventListener("click", () => { + let err = _addSignatory(control_newWitnessEmail, 'Witness', control_witnessesList); + control_witnessesValidation.textContent = err; + }); + control_uploadDocumentForm.addEventListener("submit", _onSubmitForm); + } + + function _init() { + signatories = JSON.parse(control_participantsJson.value)?.map(p => ({ + email: p.Email, + type: p.Type, + uid: p.UID + })); + + _bindEvents(); + } + + function _onSubmitForm(event) { + control_participantsJson.value = JSON.stringify(signatories); + if (control_documentFile.files.length === 0 || !control_isConfirmed.checked) { + event.preventDefault(); + return false; + } + return true; + } + + _init(); +})(); \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Program.cs b/EnotaryoPH/EnotaryoPH.Web/Program.cs index 228e195..db64bf4 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Program.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Program.cs @@ -1,5 +1,8 @@ using System.Security.Principal; +using Coravel; using EnotaryoPH.Data; +using EnotaryoPH.Web.Common.Jobs; +using EnotaryoPH.Web.Common.Models; using Exadel.Compreface.Clients.CompreFaceClient; using Microsoft.AspNetCore.Authentication.Cookies; @@ -21,20 +24,28 @@ namespace EnotaryoPH.Web razorBuilder.AddRazorRuntimeCompilation(); #endif + var config = builder.Configuration; + builder.Services.AddDbContext(); builder.Services.AddHttpContextAccessor(); builder.Services.AddSession(options => options.IdleTimeout = TimeSpan.FromMinutes(120)); builder.Services.AddTransient(provider => provider.GetService()?.HttpContext?.User); + builder.Services.AddTransient(provider => new Settings + { + BaseUrl = config.GetValue("BaseUrl") ?? "" + }); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(provider => { - var config = provider.GetRequiredService(); var host = config.GetValue("CompreFaceConfig:Host"); var port = config.GetValue("CompreFaceConfig:Port"); return new CompreFaceClient(host, port); }); + builder.Services.AddQueue(); + builder.Services.AddMailer(config); + builder.Services.AddTransient(); var app = builder.Build(); diff --git a/EnotaryoPH/EnotaryoPH.Web/Views/Mail/SignatoryInvitation.cshtml b/EnotaryoPH/EnotaryoPH.Web/Views/Mail/SignatoryInvitation.cshtml new file mode 100644 index 0000000..c407048 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Views/Mail/SignatoryInvitation.cshtml @@ -0,0 +1,18 @@ +@using EnotaryoPH.Web.Mailables +@model SignatoryViewModel +@{ + ViewBag.Heading = "Welcome!"; + ViewBag.Preview = "Example Email"; +} + +

+ + Hello, you have been invited to enotaryo as a @Model.SignatoryType by @Model.MainPrincipalName. + + Please click here to get you started. @await Component.InvokeAsync("EmailLinkButton", new { text = "Registration Page", url = Model.InvitationURL }) +

+ +@section links +{ + Registration Page +} diff --git a/EnotaryoPH/EnotaryoPH.Web/Views/Mail/_ViewImports.cshtml b/EnotaryoPH/EnotaryoPH.Web/Views/Mail/_ViewImports.cshtml new file mode 100644 index 0000000..866bdb7 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Views/Mail/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using EnotaryoPH.Web +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Coravel.Mailer.ViewComponents diff --git a/EnotaryoPH/EnotaryoPH.Web/Views/Mail/_ViewStart.cshtml b/EnotaryoPH/EnotaryoPH.Web/Views/Mail/_ViewStart.cshtml new file mode 100644 index 0000000..1d54d45 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Views/Mail/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/Areas/Coravel/Pages/Mail/Template.cshtml"; +} diff --git a/EnotaryoPH/EnotaryoPH.Web/appsettings.json b/EnotaryoPH/EnotaryoPH.Web/appsettings.json index 68e0513..42f980d 100644 --- a/EnotaryoPH/EnotaryoPH.Web/appsettings.json +++ b/EnotaryoPH/EnotaryoPH.Web/appsettings.json @@ -13,5 +13,15 @@ "APIKey": "secret", "Host": "http://localhost", "Port": "8000" - } + }, + "Coravel": { + "Mail": { + "Driver": "SMTP", + "Host": "localhost", + "Port": 25, + "Username": "", + "Password": "" + } + }, + "BaseUrl": "https://localhost:7121" } \ No newline at end of file