feat(sms): NotificationListForm adapter-gated Type selector + per-type recipients (S7)
This commit is contained in:
+99
-15
@@ -1,10 +1,13 @@
|
||||
@page "/notifications/lists/create"
|
||||
@page "/notifications/lists/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject INotificationRepository NotificationRepository
|
||||
@inject INotificationChannelCatalog ChannelCatalog
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
@@ -25,6 +28,19 @@
|
||||
<input type="text" class="form-control" @bind="_name" />
|
||||
</div>
|
||||
|
||||
@* Type is create-only: a list's channel fixes which contact field its
|
||||
recipients carry (email vs phone), so changing it after recipients exist
|
||||
would orphan them. Disabled on edit (Id.HasValue). *@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<select class="form-select" @bind="_type" disabled="@Id.HasValue">
|
||||
@foreach (var channel in ChannelCatalog.SupportedChannels)
|
||||
{
|
||||
<option value="@channel">@ChannelLabel(channel)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mb-2">@_formError</div>
|
||||
@@ -47,10 +63,23 @@
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" @bind="_recipientName" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" @bind="_recipientEmail" />
|
||||
</div>
|
||||
@* Per-type contact field: email lists collect an email address; SMS
|
||||
lists collect an E.164 phone number. *@
|
||||
@if (_type == NotificationType.Sms)
|
||||
{
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="tel" class="form-control" placeholder="+15551234567"
|
||||
@bind="_recipientPhone" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" @bind="_recipientEmail" />
|
||||
</div>
|
||||
}
|
||||
@if (_recipientFormError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_recipientFormError</div>
|
||||
@@ -65,7 +94,7 @@
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>@(_type == NotificationType.Sms ? "Phone" : "Email")</th>
|
||||
<th style="width:80px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -80,7 +109,7 @@
|
||||
{
|
||||
<tr>
|
||||
<td>@r.Name</td>
|
||||
<td>@r.EmailAddress</td>
|
||||
<td>@(_type == NotificationType.Sms ? r.PhoneNumber : r.EmailAddress)</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1" @onclick="() => DeleteRecipient(r)">Delete</button>
|
||||
</td>
|
||||
@@ -98,17 +127,29 @@
|
||||
|
||||
private bool _loading = true;
|
||||
private string _name = "";
|
||||
|
||||
// Delivery channel for the list. Selectable only on create (an empty Id); on edit it
|
||||
// reflects the persisted list's Type and the selector is disabled. Defaults to the
|
||||
// first supported channel so the create form never starts on an undeliverable type.
|
||||
private NotificationType _type = NotificationType.Email;
|
||||
private string? _formError;
|
||||
|
||||
private NotificationList? _existing;
|
||||
|
||||
// Recipients
|
||||
private List<NotificationRecipient> _recipients = new();
|
||||
private string _recipientName = "", _recipientEmail = "";
|
||||
private string _recipientName = "", _recipientEmail = "", _recipientPhone = "";
|
||||
private string? _recipientFormError;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Default the create-form selection to the first supported channel (catalog is
|
||||
// never empty on a correctly configured central node).
|
||||
if (!Id.HasValue && ChannelCatalog.SupportedChannels.Count > 0)
|
||||
{
|
||||
_type = ChannelCatalog.SupportedChannels[0];
|
||||
}
|
||||
|
||||
if (Id.HasValue)
|
||||
{
|
||||
try
|
||||
@@ -117,6 +158,7 @@
|
||||
if (_existing != null)
|
||||
{
|
||||
_name = _existing.Name;
|
||||
_type = _existing.Type;
|
||||
}
|
||||
_recipients = (await NotificationRepository.GetRecipientsByListIdAsync(Id.Value)).ToList();
|
||||
}
|
||||
@@ -125,6 +167,12 @@
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private static string ChannelLabel(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Sms => "SMS",
|
||||
_ => "Email"
|
||||
};
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
_formError = null;
|
||||
@@ -143,7 +191,10 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var nl = new NotificationList(_name.Trim());
|
||||
// Type is set at create time and never changes thereafter (the selector
|
||||
// is disabled on edit) so a list's recipients always carry the matching
|
||||
// contact field.
|
||||
var nl = new NotificationList(_name.Trim()) { Type = _type };
|
||||
await NotificationRepository.AddNotificationListAsync(nl);
|
||||
}
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
@@ -155,26 +206,59 @@
|
||||
private async Task SaveRecipient()
|
||||
{
|
||||
_recipientFormError = null;
|
||||
if (string.IsNullOrWhiteSpace(_recipientName) || string.IsNullOrWhiteSpace(_recipientEmail))
|
||||
if (string.IsNullOrWhiteSpace(_recipientName))
|
||||
{
|
||||
_recipientFormError = "Name and email required.";
|
||||
_recipientFormError = "Name required.";
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationRecipient r;
|
||||
if (_type == NotificationType.Sms)
|
||||
{
|
||||
var phone = _recipientPhone.Trim();
|
||||
if (!IsValidPhone(phone))
|
||||
{
|
||||
_recipientFormError = "A valid phone number is required (digits, optional leading +).";
|
||||
return;
|
||||
}
|
||||
r = NotificationRecipient.ForSms(_recipientName.Trim(), phone);
|
||||
}
|
||||
else
|
||||
{
|
||||
var email = _recipientEmail.Trim();
|
||||
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
|
||||
{
|
||||
_recipientFormError = "A valid email address is required.";
|
||||
return;
|
||||
}
|
||||
r = NotificationRecipient.ForEmail(_recipientName.Trim(), email);
|
||||
}
|
||||
r.NotificationListId = Id!.Value;
|
||||
|
||||
try
|
||||
{
|
||||
var r = new NotificationRecipient(_recipientName.Trim(), _recipientEmail.Trim())
|
||||
{
|
||||
NotificationListId = Id!.Value
|
||||
};
|
||||
await NotificationRepository.AddRecipientAsync(r);
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
_recipientName = _recipientEmail = string.Empty;
|
||||
_recipientName = _recipientEmail = _recipientPhone = string.Empty;
|
||||
_recipients = (await NotificationRepository.GetRecipientsByListIdAsync(Id.Value)).ToList();
|
||||
}
|
||||
catch (Exception ex) { _recipientFormError = ex.Message; }
|
||||
}
|
||||
|
||||
// E.164-ish: non-empty, an optional leading '+' then digits only. Kept permissive
|
||||
// (matches the SMS adapter's recipient handling) rather than enforcing a strict
|
||||
// regional format or length.
|
||||
private static bool IsValidPhone(string phone)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(phone))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var digits = phone.StartsWith('+') ? phone[1..] : phone;
|
||||
return digits.Length > 0 && digits.All(char.IsDigit);
|
||||
}
|
||||
|
||||
private async Task DeleteRecipient(NotificationRecipient r)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user