user listing - editing - role assignment - operational messages

This commit is contained in:
Frede Hundewadt 2023-03-17 14:20:54 +01:00
parent 02fb20ca8a
commit efd7a8a330
18 changed files with 354 additions and 54 deletions

View file

@ -88,6 +88,10 @@
<th scope="col">Lev.Postnr By</th>
<td>@ReportItem.DlvZipCity</td>
</tr>
<tr>
<th scope="col">Email</th>
<td colspan="3">@ReportItem.Company.Email</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-striped table-bordered">

View file

@ -14,6 +14,7 @@
//
using System.Net.Mail;
using System.Text.RegularExpressions;
using Wonky.Client.Models;
namespace Wonky.Client.Helpers;
@ -23,6 +24,29 @@ namespace Wonky.Client.Helpers;
/// </summary>
public static class Utils
{
/// <summary>
/// Sanitize string by removing everything but digits
/// </summary>
/// <param name="digitString"></param>
/// <returns></returns>
public static string StringToDigits(string digitString)
{
if (string.IsNullOrWhiteSpace(digitString))
return "";
var regexObj = new Regex(@"[^\d]");
return regexObj.Replace(digitString, "");
}
/// <summary>
/// Validate string is only numbers
/// </summary>
/// <param name="check"></param>
/// <returns></returns>
private static bool IsDigitsOnly(string check)
{
return check.All(c => c is >= '0' and <= '9');
}
public static bool Validate(ValidateType validateType, string toValidate)
{
return validateType switch

View file

@ -32,7 +32,7 @@ public class VatUtils
if (string.IsNullOrWhiteSpace(vatNumber) || string.IsNullOrWhiteSpace(countryCode))
return false;
var sanitisedVat = SanitizeVatNumber(vatNumber);
var sanitisedVat = Utils.StringToDigits(vatNumber);
return countryCode.ToUpperInvariant() switch
{
@ -42,30 +42,7 @@ public class VatUtils
_ => false
};
}
/// <summary>
/// Sanitize Vat remove everything but digits
/// </summary>
/// <param name="vatNumber"></param>
/// <returns></returns>
public static string SanitizeVatNumber(string vatNumber)
{
if (string.IsNullOrWhiteSpace(vatNumber))
return "";
var regexObj = new Regex(@"[^\d]");
return regexObj.Replace(vatNumber, "");
}
/// <summary>
/// Validate string is only numbers
/// </summary>
/// <param name="check"></param>
/// <returns></returns>
private static bool IsDigitsOnly(string check)
{
return check.All(c => c is >= '0' and <= '9');
}
/// <summary>
/// Validate Danish Vat number format
/// </summary>

View file

@ -0,0 +1,33 @@
// Copyright (C) 2022 FCS Frede's Computer Services.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see [https://www.gnu.org/licenses/agpl-3.0.en.html]
//
using Wonky.Entity.DTO;
using Wonky.Entity.Views;
namespace Wonky.Client.HttpRepository;
/// <summary>
/// Interface for sending emai
/// </summary>
public interface ISystemSendSmsService
{
/// <summary>
/// Send Mail
/// </summary>
/// <param name="messageType"></param>
/// <param name="message"></param>
/// <returns></returns>
Task<ApiResponseView> SendSms(ShortMessage message);
}

View file

@ -0,0 +1,66 @@
// Copyright (C) 2022 FCS Frede's Computer Services.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see [https://www.gnu.org/licenses/agpl-3.0.en.html]
//
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using Wonky.Entity.Configuration;
using Wonky.Entity.DTO;
using Wonky.Entity.Views;
namespace Wonky.Client.HttpRepository;
public class SystemSendSmsService : ISystemSendSmsService
{
private readonly JsonSerializerOptions? _options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
private readonly NavigationManager _navigation;
private ILogger<SystemSendSmsService> _logger;
private readonly HttpClient _client;
private readonly ApiConfig _api;
public SystemSendSmsService(HttpClient client, ILogger<SystemSendSmsService> logger,
NavigationManager navigation, IOptions<ApiConfig> configuration)
{
_client = client;
_logger = logger;
_navigation = navigation;
_api = configuration.Value;
}
/// <summary>
/// Send Mail
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public async Task<ApiResponseView> SendSms(ShortMessage message)
{
var response = await _client.PostAsJsonAsync($"{_api.ServicesSms}", message, _options);
if (!response.IsSuccessStatusCode)
return new ApiResponseView
{
Code = (int)response.StatusCode,
Message = $"{response.ReasonPhrase}: {await response.Content.ReadAsStringAsync()}",
IsSuccess = false,
Id = ""
};
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ApiResponseView>(content, _options);
}
}

View file

@ -63,12 +63,12 @@
<label for="officeNote" class="col-form-label-sm col-sm-1">Ordre Note</label>
<div class="col-sm-5">
<textarea id="officeNote" class="form-control" value="@ReportItem.OfficeNote" readonly=""/>
<textarea id="officeNote" class="form-control" value="@ReportItem.OfficeNote" readonly/>
</div>
<label for="crmNote" class="col-form-label-sm col-sm-1">Crm Note</label>
<div class="col-sm-5">
<textarea id="crmNote" class="form-control" value="@ReportItem.CrmNote" readonly=""/>
<textarea id="crmNote" class="form-control" value="@ReportItem.CrmNote" readonly/>
</div>
<hr/>
<table class="table table-striped">

View file

@ -257,7 +257,7 @@ public partial class AdvisorActivityCreatePage : IDisposable
if (OrgWarning)
return;
OrgWarning = true;
if (Company.CountryCode.ToLower() == "se" && VatUtils.SanitizeVatNumber(Activity.VatNumber).Length < 10 &&
if (Company.CountryCode.ToLower() == "se" && Utils.StringToDigits(Activity.VatNumber).Length < 10 &&
Activity.ActivityStatusEnum == "order")
{
Toaster.ShowWarning("Org nummer er ufuldstændig. Skal opdateres før bestilling kan sendes. ", "ADVARSEL");
@ -573,7 +573,7 @@ public partial class AdvisorActivityCreatePage : IDisposable
if (Activity.ActivityStatusEnum.ToLower() is "order" or "quote"
&& Company.CountryCode.ToLower() == "se"
&& VatUtils.SanitizeVatNumber(Activity.VatNumber).Length < 10)
&& Utils.StringToDigits(Activity.VatNumber).Length < 10)
{
ShowOrgWarning();
PoFormInvalid = true;

View file

@ -101,6 +101,10 @@
<th scope="col">Lev.Postnr By</th>
<td>@ReportItem.DlvZipCity</td>
</tr>
<tr>
<th scope="col">Email</th>
<td colspan="3">@ReportItem.Company.Email</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-striped table-bordered">

View file

@ -0,0 +1,65 @@
@* Copyright (C) 2022 FCS Frede's Computer Services.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see [https://www.gnu.org/licenses/agpl-3.0.en.html]
*@
@using Microsoft.AspNetCore.Authorization
@using Wonky.Client.Helpers
@using System.Text.Json
@attribute [Authorize(Roles = "Admin")]
@page "/system/sms"
<div id="msg-box" style="display:@(MsgSent ? "none" : "block" )">
<div class="row">
<div class="h3 col-sm-10">
Drift Meddelelse (SMS)
</div>
<div class="col-sm-2 text-end">
<div class="busy-signal" style="display:@(Working ? "block" : "none")">
<div class="spinner-grow text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="row mb-5">
<div class="col-sm-12 d-grid mx-auto">
<textarea id="message" cols="50" rows="5" @bind-Value:event="oninput" @bind-Value="Message.Body"></textarea>
</div>
</div>
<div class="row ms-2">
@foreach (var recipient in Recipients)
{
<div class="form-check col-sm-2">
<input type="checkbox" id="@recipient.Mobile" class="form-check-input" @bind-Value:event="onchange" @bind-Value="recipient.Enabled"/>
<label class="form-check-label" for="@recipient.Mobile">@recipient.Name</label>
</div>
}
</div>
<div class="row">
<div class="col-sm-12 text-end">
<button class="btn btn-primary" type="button" @onclick="SendMessage">Send besked</button>
</div>
</div>
</div>
<div id="msg-box" style="display:@(MsgSent ? "block" : "none")">
<div class="row">
<div class="col-sm-2">
Status Kode: @SmsResponse.Code
</div>
<div class="col-sm-10">
Meddelelse: @SmsResponse.Message
</div>
</div>
</div>

View file

@ -0,0 +1,85 @@
using System.Text.Json;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Wonky.Client.Helpers;
using Wonky.Client.HttpInterceptors;
using Wonky.Client.HttpRepository;
using Wonky.Entity.DTO;
using Wonky.Entity.Views;
namespace Wonky.Client.Pages;
public partial class SystemMaintenanceMessage
{
[Inject] public HttpInterceptorService Interceptor { get; set; }
[Inject] public ISystemUserRepository UserRepo { get; set; }
[Inject] public ISystemSendSmsService SmsService { get; set; }
[Inject] public ILogger<SystemMaintenanceMessage> Logger { get; set; }
[Inject] public IToastService Toaster { get; set; }
private List<UserManagerListView> Users { get; set; }
private ShortMessage Message { get; set; } = new();
private List<Recipient> Recipients { get; set; } = new();
private ApiResponseView SmsResponse { get; set; } = new();
private bool Working { get; set; } = true;
private bool MsgSent { get; set; }
private readonly JsonSerializerOptions _options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
protected override async Task OnInitializedAsync()
{
Interceptor.RegisterEvent();
Interceptor.RegisterBeforeSendEvent();
Users = await UserRepo.GetUsers();
var allRecipients = Users.Where(x => Utils.StringToDigits(x.PhoneNumber) != string.Empty)
.Select(x => new Recipient
{
Mobile = Utils.StringToDigits(x.PhoneNumber),
Name = x.FullName
}).ToList();
Recipients = (from r in allRecipients select r).DistinctBy(x => x.Mobile).ToList();
Working = false;
}
private async Task SendMessage()
{
Working = true;
var recipients = (from recipient in Recipients where recipient.Enabled select recipient.Mobile).ToList();
if (!recipients.Any())
{
Toaster.ShowError("Der er ingen modtagere valgt!");
return;
}
if (string.IsNullOrWhiteSpace(Message.Body))
{
Toaster.ShowError("Meddelelsesfeltet er tomt!");
return;
}
Message.From = "Innotec IT";
Message.To = string.Join(",", recipients);
Logger.LogDebug("{}", JsonSerializer.Serialize(Message, _options));
SmsResponse = await SmsService.SendSms(Message);
if (SmsResponse.Code == 201)
{
MsgSent = true;
}
Working = false;
}
}
internal sealed class Recipient
{
public bool Enabled { get; set; }
public string Name { get; set; }
public string Mobile { get; set; }
}

View file

@ -15,7 +15,10 @@
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authorization.Infrastructure
@using Wonky.Client.Components
@using System.Security.Principal
@using Wonky.Client.Models
@attribute [Authorize(Roles = "Admin")]
@page "/system/users/{UserId}"
@ -27,7 +30,7 @@
</div>
</div>
<div class="card-body">
@if (!string.IsNullOrWhiteSpace(UserInfo.UserId))
@if (!string.IsNullOrWhiteSpace(UserData.UserId))
{
<EditForm EditContext="UserEditContext" OnValidSubmit="UpdateUser">
<DataAnnotationsValidator/>
@ -40,15 +43,15 @@
Fornavn
</th>
<td>
<InputText id="firstName" class="form-control" @bind-Value="UserInfo.FirstName" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserInfo.FirstName)"></ValidationMessage>
<InputText id="firstName" class="form-control" @bind-Value="UserData.FirstName" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserData.FirstName)"></ValidationMessage>
</td>
<th scope="col">
Efternavn
</th>
<td>
<InputText id="lastName" class="form-control" @bind-Value="UserInfo.LastName" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserInfo.LastName)"></ValidationMessage>
<InputText id="lastName" class="form-control" @bind-Value="UserData.LastName" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserData.LastName)"></ValidationMessage>
</td>
</tr>
<tr class="align-middle">
@ -56,15 +59,15 @@
Email
</th>
<td>
<InputText id="email" class="form-control" @bind-Value="UserInfo.Email" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserInfo.Email)"></ValidationMessage>
<InputText id="email" class="form-control" @bind-Value="UserData.Email" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserData.Email)"></ValidationMessage>
</td>
<th scope="col">
Mobilnummer
</th>
<td>
<InputText id="phoneNumber" class="form-control" @bind-Value="UserInfo.PhoneNumber" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserInfo.PhoneNumber)"></ValidationMessage>
<InputText id="phoneNumber" class="form-control" @bind-Value="UserData.PhoneNumber" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserData.PhoneNumber)"></ValidationMessage>
</td>
</tr>
<tr class="align-middle">
@ -72,24 +75,41 @@
Sælgernr.
</th>
<td>
<InputText id="salesRep" class="form-control" @bind-Value="UserInfo.SalesRep" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserInfo.SalesRep)"></ValidationMessage>
<InputText id="salesRep" class="form-control" @bind-Value="UserData.SalesRep" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserData.SalesRep)"></ValidationMessage>
</td>
<th scope="col">
Landekode
</th>
<td>
<InputText id="countryCode" class="form-control" @bind-Value="UserInfo.CountryCode" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserInfo.CountryCode)"></ValidationMessage>
<InputText id="countryCode" class="form-control" @bind-Value="UserData.CountryCode" readonly="@ReadOnly" />
<ValidationMessage For="@(() => UserData.CountryCode)"></ValidationMessage>
</td>
</tr>
<tr class="align-middle">
<th scope="col">
Spærret
</th>
<td colspan="3">
<InputCheckbox id="lockoutEnabled" class="form-check" @bind-Value="UserInfo.LockoutEnabled"/>
<ValidationMessage For="@(() => UserInfo.LockoutEnabled)"></ValidationMessage>
<td>
<InputCheckbox id="lockoutEnabled" class="form-check" @bind-Value="UserData.LockoutEnabled" disabled="@ReadOnly"/>
<ValidationMessage For="@(() => UserData.LockoutEnabled)"></ValidationMessage>
</td>
<th scope="col">Beskrivelse</th>
<td>
<InputText id="description" class="form-control" @bind-Value="@UserData.Description" readonly="@ReadOnly"/>
<ValidationMessage For="@(() => UserData.Description)"></ValidationMessage>
</td>
</tr>
<tr>
<th scope="col">Roller</th>
<td>
@foreach (var role in UserData.AssignedRoles)
{
<div class="form-check">
<InputCheckbox id="@(role.Name)" class="form-check-input" @bind-Value="role.Assigned" disabled="@ReadOnly" />
<label class="form-check-label">@role.Name</label>
</div>
}
</td>
</tr>
</tbody>
@ -111,10 +131,9 @@
<EditForm EditContext="PasswdContext" class="mt-5" >
<DataAnnotationsValidator />
<h3>NULSTIL ADGANGSKODE</h3>
<div class="alert-info">
<h4>Password politik</h4>
<p>Mindst 10 tegn bestående af store og små bogstaver samt tal.</p>
<p>Du kan teste pasword og danne stærke password på <a href="https://pw.nix.dk">pw.nix.dk</a></p>
<div class="alert alert-info">
<div class="h4">Password politik</div>
<div>Mindst 10 tegn bestående af store og små bogstaver samt tal. Kodeordsgenerator <a href="https://pw.nix.dk">pw.nix.dk</a></div>
</div>
<div class="row mb-3">
<label for="newPasswd" class="col-md-2 col-form-label">Ny</label>
@ -132,7 +151,7 @@
</div>
<div class="row">
<div class="col align-content-end">
<button class="btn btn-warning" @onclick="SetPassword" disabled="@PwInvalid">NULSTIL</button>
<button class="btn btn-danger" @onclick="SetPassword" disabled="@PwInvalid">NULSTIL</button>
</div>
</div>
</EditForm>

View file

@ -35,7 +35,7 @@ public partial class SystemUserViewEditPage : IDisposable
[Inject] public ISystemUserRepository UserRepo { get; set; }
[Inject] public ILogger<SystemUserViewEditPage> Logger { get; set; }
[Inject] public IToastService Toaster { get; set; }
private UserManagerEditView UserInfo { get; set; } = new();
private UserManagerEditView UserData { get; set; } = new();
private EditContext UserEditContext { get; set; }
private ResetPasswordDto Passwords { get; set; } = new();
private EditContext PasswdContext { get; set; }
@ -53,9 +53,9 @@ public partial class SystemUserViewEditPage : IDisposable
Interceptor.RegisterEvent();
Interceptor.RegisterBeforeSendEvent();
UserInfo = await UserRepo.GetUserInfo(UserId);
UserData = await UserRepo.GetUserInfo(UserId);
UserEditContext = new EditContext(UserInfo);
UserEditContext = new EditContext(UserData);
PasswdContext = new EditContext(Passwords);
PasswdContext.OnFieldChanged += PwHandleFieldChanged!;
@ -68,7 +68,7 @@ public partial class SystemUserViewEditPage : IDisposable
ReadOnly = true;
Working = true;
Toaster.ShowInfo("Sender data til server ...");
await UserRepo.UpdateUserInfo(UserId, UserInfo);
await UserRepo.UpdateUserInfo(UserId, UserData);
Working = false;
Toaster.ShowInfo("Bruger er opdateret ...");
}

View file

@ -75,6 +75,7 @@ builder.Services.AddScoped<ISystemUserRepository, SystemUserRepository>();
builder.Services.AddScoped<IOrderProcessRepository, OrderProcessRepository>();
// mail service
builder.Services.AddScoped<ISystemSendMailService, SystemSendMailService>();
builder.Services.AddScoped<ISystemSendSmsService, SystemSendSmsService>();
// interceptor
builder.Services.AddScoped<HttpInterceptorService>();
// storage

View file

@ -36,6 +36,7 @@
"servicesGlsId": "",
"servicesGlsTrackUrl": "https://www.gls-group.eu/276-I-PORTAL-WEB/content/GLS/DK01/DA/5004.htm?txtAction=71000&txtRefNo=",
"servicesMail": "api/v2/services/sendmail",
"servicesSms": "api/v2/services/sms",
"servicesVatDk": "api/v2/services/virk",
"serviceVatEu": "api/v2/services/vies",
"servicesVatNo": "api/v2/services/brReg",

View file

@ -108,6 +108,10 @@ public class ApiConfig
/// </summary>
public string ServicesMail { get; set; } = "";
/// <summary>
/// url for sending Short Message
/// </summary>
public string ServicesSms { get; set; } = "";
/// <summary>
/// VAT registrar url Denmark
/// </summary>

View file

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace Wonky.Entity.DTO;
public class ShortMessage
{
[Required] [MaxLength(11)] public string From { get; set; } = "";
[Required] [MaxLength(1000)] public string To { get; set; } = "";
[Required] [MaxLength(100)] public string Body { get; set; } = "";
}

View file

@ -31,7 +31,7 @@ public class UserManagerEditView
public bool LockoutEnabled { get; set; }
public string Passwd { get; set; } = "";
[Required(ErrorMessage = "Telefon nummer skal udfyldes")][MaxLength(20, ErrorMessage = "Der er afsat 20 tegn til telefon nummber")] public string PhoneNumber { get; set; } = "";
[Required(ErrorMessage = "Medarbejder ID skal udfyldes")][MaxLength(20, ErrorMessage = "Der er afsat 20 tegn til medarbejder ID")] public string SalesRep { get; set; } = "";
[MaxLength(20, ErrorMessage = "Der er afsat 20 tegn til Sælger No.")] public string SalesRep { get; set; } = "";
public string UserId { get; set; } = "";
public List<UserRoleAssignment> AssignedRoles { get; set; } = new();
public List<SubjectAssignment> AssignedSubjects { get; set; } = new();

View file

@ -54,4 +54,9 @@ public class ReportItemCustomer
/// Company address address line 2
/// </summary>
public string Address2 { get; set; } = "";
/// <summary>
/// Company email address
/// </summary>
public string Email { get; set; } = "";
}