Initial commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
using PowderCoating.Application.DTOs.Quote;
|
||||
|
||||
namespace PowderCoating.Web.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Shared request model posted to the AJAX pricing-calculation endpoint
|
||||
/// (<c>POST /Quotes/CalculatePricing</c> and <c>POST /Jobs/CalculatePricing</c>).
|
||||
/// <para>
|
||||
/// This DTO lives in the Web layer rather than the Application layer because it
|
||||
/// is specific to the MVC item-wizard UX; the Application layer's pricing service
|
||||
/// accepts a richer domain model internally. Keeping it here avoids polluting
|
||||
/// the Application layer with presentation concerns.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Both the Quotes and Jobs item wizards POST this model so that a single shared
|
||||
/// JavaScript wizard (<c>item-wizard.js</c>) can drive real-time price previews
|
||||
/// for both entity types without duplicating the AJAX call logic.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class PricingCalculationRequest
|
||||
{
|
||||
/// <summary>Line items to price (surfaces, coatings, prep services, etc.).</summary>
|
||||
public List<CreateQuoteItemDto> Items { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Optional customer ID; when provided the pricing engine applies any
|
||||
/// pricing-tier discount configured for that customer.
|
||||
/// </summary>
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>Tax percentage to apply after discounts (0–100 scale).</summary>
|
||||
public decimal TaxPercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Discount mode: <c>"None"</c>, <c>"Percent"</c>, or <c>"Flat"</c>.
|
||||
/// Defaults to <c>"None"</c> so omitting the field from the request body
|
||||
/// is safe.
|
||||
/// </summary>
|
||||
public string DiscountType { get; set; } = "None";
|
||||
|
||||
/// <summary>
|
||||
/// Discount magnitude — a percentage value when <see cref="DiscountType"/>
|
||||
/// is <c>"Percent"</c>, or a dollar amount when it is <c>"Flat"</c>.
|
||||
/// </summary>
|
||||
public decimal DiscountValue { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, a rush-job surcharge is added to the total.
|
||||
/// </summary>
|
||||
public bool IsRushJob { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The named oven (<c>OvenCost</c> record ID) used for scheduling.
|
||||
/// Drives per-cycle cost calculation when combined with <see cref="OvenBatches"/>
|
||||
/// and <see cref="OvenCycleMinutes"/>.
|
||||
/// </summary>
|
||||
public int? OvenCostId { get; set; }
|
||||
|
||||
/// <summary>Number of oven batch cycles required for this job. Defaults to 1.</summary>
|
||||
public int OvenBatches { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Override for the oven cycle duration in minutes. When <c>null</c> the
|
||||
/// pricing engine uses the default cycle time stored on the <c>OvenCost</c>
|
||||
/// record.
|
||||
/// </summary>
|
||||
public int? OvenCycleMinutes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PowderCoating.Web.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Static helper methods for input validation and sanitisation used across
|
||||
/// controllers and services.
|
||||
/// <para>
|
||||
/// All members are intentionally static so they can be called without DI from
|
||||
/// any layer. Each method is defensive-by-default: it returns a safe value
|
||||
/// (null / false / empty string) rather than throwing, so callers can treat a
|
||||
/// bad return as a "nothing to do" signal without try/catch overhead.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SecurityHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Hard upper bound on search term length. Anything longer is almost
|
||||
/// certainly not a legitimate search query and could indicate an attempt
|
||||
/// to probe buffer limits or trigger expensive LIKE queries.
|
||||
/// </summary>
|
||||
private const int MaxSearchTermLength = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Characters that have meaning in HTML, SQL, shell, and many injection
|
||||
/// contexts. Stripping them from search input provides defence-in-depth
|
||||
/// even though EF Core parameterises all DB queries independently.
|
||||
/// </summary>
|
||||
private static readonly Regex DangerousPatterns = new(@"[<>""';()&|]", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Collapses consecutive whitespace runs so that a search term passed to
|
||||
/// a LIKE query does not contain invisible padding that would silently
|
||||
/// change matching behaviour.
|
||||
/// </summary>
|
||||
private static readonly Regex MultipleSpaces = new(@"\s+", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Trims, length-caps, strips injection-risky characters, and normalises
|
||||
/// whitespace in a user-supplied search string.
|
||||
/// <para>
|
||||
/// Even though EF Core parameterises SQL queries (eliminating SQL injection
|
||||
/// risk at the DB layer), sanitising the input here provides additional
|
||||
/// defence against XSS if the raw term is ever echoed back into the view,
|
||||
/// and prevents excessively long strings from causing performance issues
|
||||
/// in LIKE queries or full-text search indexes.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="searchTerm">The raw search term from user input.</param>
|
||||
/// <returns>Sanitised search term, or <c>null</c> if the input was empty or
|
||||
/// reduced to nothing after stripping dangerous characters.</returns>
|
||||
public static string? SanitizeSearchTerm(string? searchTerm)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Trim and limit length
|
||||
searchTerm = searchTerm.Trim();
|
||||
if (searchTerm.Length > MaxSearchTermLength)
|
||||
{
|
||||
searchTerm = searchTerm[..MaxSearchTermLength];
|
||||
}
|
||||
|
||||
// Remove dangerous characters that could be used for injection
|
||||
searchTerm = DangerousPatterns.Replace(searchTerm, string.Empty);
|
||||
|
||||
// Normalize whitespace
|
||||
searchTerm = MultipleSpaces.Replace(searchTerm, " ");
|
||||
|
||||
// Return null if nothing left after sanitization
|
||||
return string.IsNullOrWhiteSpace(searchTerm) ? null : searchTerm.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the input contains only alphanumeric characters,
|
||||
/// hyphens, and underscores (plus optionally spaces).
|
||||
/// <para>
|
||||
/// Used to validate short codes, job numbers, and filenames before they are
|
||||
/// embedded in file paths or used as identifiers — characters outside this
|
||||
/// set could be used for path traversal or identifier injection.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="input">The input string to validate.</param>
|
||||
/// <param name="allowSpaces">Whether to allow space characters in the input.</param>
|
||||
/// <returns><c>true</c> if valid; <c>false</c> if <c>null</c>, empty, or contains unsafe characters.</returns>
|
||||
public static bool IsAlphanumericSafe(string? input, bool allowSpaces = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pattern = allowSpaces ? @"^[a-zA-Z0-9\-_\s]+$" : @"^[a-zA-Z0-9\-_]+$";
|
||||
return Regex.IsMatch(input, pattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the file extension extracted from <paramref name="fileName"/>
|
||||
/// is present in <paramref name="allowedExtensions"/> (case-insensitive).
|
||||
/// <para>
|
||||
/// Extension validation is the first line of defence against executable
|
||||
/// file uploads. Always combine with server-side MIME/magic-byte checks
|
||||
/// for sensitive upload endpoints.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="fileName">The filename (with or without path) to inspect.</param>
|
||||
/// <param name="allowedExtensions">
|
||||
/// Array of permitted lowercase extensions including the leading dot
|
||||
/// (e.g. <c>[".jpg", ".png"]</c>).
|
||||
/// </param>
|
||||
/// <returns><c>true</c> if the extension is in the allowed list; otherwise <c>false</c>.</returns>
|
||||
public static bool HasSafeFileExtension(string fileName, string[] allowedExtensions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return !string.IsNullOrEmpty(extension) && allowedExtensions.Contains(extension);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips OS-invalid filename characters and explicit path separators
|
||||
/// (<c>/</c> and <c>\</c>) from <paramref name="fileName"/>.
|
||||
/// <para>
|
||||
/// This prevents <em>path traversal</em> attacks where a malicious upload
|
||||
/// includes sequences like <c>../../etc/passwd</c> in the filename field.
|
||||
/// Note: only the <em>filename</em> component is sanitised here — callers
|
||||
/// must independently validate that the final storage path is within the
|
||||
/// expected base directory (see <see cref="IsPathWithinBase"/>).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="fileName">The raw filename from the upload or user input.</param>
|
||||
/// <returns>
|
||||
/// Sanitised filename with dangerous characters removed and leading/trailing
|
||||
/// whitespace trimmed. Returns an empty string if the input was null or empty.
|
||||
/// </returns>
|
||||
public static string SanitizeFileName(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Remove path separators and other dangerous characters
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
var sanitized = new string(fileName
|
||||
.Where(c => !invalidChars.Contains(c) && c != '/' && c != '\\')
|
||||
.ToArray());
|
||||
|
||||
return sanitized.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the fully-resolved <paramref name="filePath"/> is located
|
||||
/// inside <paramref name="basePath"/>, preventing directory traversal attacks.
|
||||
/// <para>
|
||||
/// Both paths are fully resolved with <see cref="Path.GetFullPath"/> before
|
||||
/// comparison so that relative segments (<c>..</c>) and symbolic links are
|
||||
/// normalised away. The comparison is case-insensitive to handle Windows
|
||||
/// filesystem semantics correctly.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Returns <c>false</c> on any exception (e.g. illegal path characters) so
|
||||
/// the caller can safely treat an unexpected input as unsafe.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="basePath">
|
||||
/// The root directory that <paramref name="filePath"/> must reside within.
|
||||
/// </param>
|
||||
/// <param name="filePath">The file path to validate (may be user-supplied).</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if <paramref name="filePath"/> is inside <paramref name="basePath"/>;
|
||||
/// <c>false</c> if traversal is detected or an error occurs during path resolution.
|
||||
/// </returns>
|
||||
public static bool IsPathWithinBase(string basePath, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
var fullBasePath = Path.GetFullPath(basePath);
|
||||
|
||||
return fullPath.StartsWith(fullBasePath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
fullPath.Equals(fullBasePath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Web.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Maps domain status values to PCL design-system chip kinds (ok/warn/bad/cool/ember/neutral).
|
||||
/// </summary>
|
||||
public static class StatusChipHelper
|
||||
{
|
||||
public static string EquipmentStatus(string status) => status switch
|
||||
{
|
||||
"Operational" => "ok",
|
||||
"NeedsMaintenance" => "warn",
|
||||
"UnderMaintenance" => "warn",
|
||||
"OutOfService" => "bad",
|
||||
"Retired" => "neutral",
|
||||
_ => "neutral"
|
||||
};
|
||||
|
||||
public static string MaintenanceStatus(string status) => status switch
|
||||
{
|
||||
"Scheduled" => "cool",
|
||||
"InProgress" => "warn",
|
||||
"Completed" => "ok",
|
||||
"Overdue" => "bad",
|
||||
"Cancelled" => "neutral",
|
||||
_ => "neutral"
|
||||
};
|
||||
|
||||
public static string MaintenancePriority(string priority) => priority switch
|
||||
{
|
||||
"Critical" => "bad",
|
||||
"High" => "warn",
|
||||
"Normal" => "cool",
|
||||
"Low" => "neutral",
|
||||
_ => "neutral"
|
||||
};
|
||||
|
||||
public static string InvoiceStatus(Core.Enums.InvoiceStatus status) => status switch
|
||||
{
|
||||
Core.Enums.InvoiceStatus.Draft => "neutral",
|
||||
Core.Enums.InvoiceStatus.Sent => "cool",
|
||||
Core.Enums.InvoiceStatus.PartiallyPaid => "warn",
|
||||
Core.Enums.InvoiceStatus.Paid => "ok",
|
||||
Core.Enums.InvoiceStatus.Overdue => "bad",
|
||||
Core.Enums.InvoiceStatus.Voided => "neutral",
|
||||
Core.Enums.InvoiceStatus.WrittenOff => "neutral",
|
||||
_ => "neutral"
|
||||
};
|
||||
|
||||
/// <param name="statusCode">Upper-case status code string from QuoteListDto.StatusCode (e.g. "SENT", "APPROVED").</param>
|
||||
public static string QuoteStatus(string? statusCode) => statusCode?.ToUpperInvariant() switch
|
||||
{
|
||||
"DRAFT" => "neutral",
|
||||
"PENDING" => "cool",
|
||||
"SENT" => "cool",
|
||||
"APPROVED" => "ok",
|
||||
"REJECTED" => "bad",
|
||||
"EXPIRED" => "neutral",
|
||||
"CONVERTED" => "ok",
|
||||
_ => "neutral"
|
||||
};
|
||||
|
||||
public static string Active(bool isActive) => isActive ? "ok" : "bad";
|
||||
|
||||
/// <param name="statusCode">Status code from JobListDto — may be PascalCase or UPPERCASE depending on source.</param>
|
||||
public static string JobStatus(string? statusCode) => statusCode?.ToUpperInvariant() switch
|
||||
{
|
||||
"PENDING" => "neutral",
|
||||
"QUOTED" => "cool",
|
||||
"APPROVED" => "ok",
|
||||
"INPREPARATION" => "cool",
|
||||
"SANDBLASTING" => "cool",
|
||||
"MASKINGTAPING" => "cool",
|
||||
"CLEANING" => "cool",
|
||||
"INOVEN" => "ember",
|
||||
"COATING" => "ember",
|
||||
"CURING" => "ember",
|
||||
"QUALITYCHECK" => "cool",
|
||||
"COMPLETED" => "ok",
|
||||
"READYFORPICKUP" => "ok",
|
||||
"DELIVERED" => "ok",
|
||||
"ONHOLD" => "warn",
|
||||
"CANCELLED" => "bad",
|
||||
_ => "neutral"
|
||||
};
|
||||
|
||||
/// <param name="priorityCode">Priority code from JobListDto — may be PascalCase or UPPERCASE.</param>
|
||||
public static string JobPriority(string? priorityCode) => priorityCode?.ToUpperInvariant() switch
|
||||
{
|
||||
"RUSH" => "bad",
|
||||
"URGENT" => "bad",
|
||||
"HIGH" => "warn",
|
||||
"NORMAL" => "neutral",
|
||||
"LOW" => "neutral",
|
||||
_ => "neutral"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Web.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods on <see cref="ITempDataDictionary"/> for storing toast
|
||||
/// notification messages that survive a POST→redirect→GET cycle.
|
||||
/// <para>
|
||||
/// TempData is used (rather than ViewBag or ViewData) because toast messages
|
||||
/// must survive an HTTP redirect: a controller sets the message before calling
|
||||
/// <c>RedirectToAction</c>, and the layout reads and renders it on the next GET.
|
||||
/// TempData is session-backed and automatically cleared after it is read.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The four well-known string keys (<c>Success</c>, <c>Error</c>, <c>Warning</c>,
|
||||
/// <c>Info</c>) match the JavaScript toast-renderer in <c>_Layout.cshtml</c>;
|
||||
/// changing them here requires a matching update in the layout script.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ToastHelper
|
||||
{
|
||||
/// <summary>TempData key read by the layout to render a green success toast.</summary>
|
||||
private const string SuccessKey = "Success";
|
||||
|
||||
/// <summary>TempData key read by the layout to render a red error toast.</summary>
|
||||
private const string ErrorKey = "Error";
|
||||
|
||||
/// <summary>TempData key read by the layout to render a yellow warning toast.</summary>
|
||||
private const string WarningKey = "Warning";
|
||||
|
||||
/// <summary>TempData key read by the layout to render a blue info toast.</summary>
|
||||
private const string InfoKey = "Info";
|
||||
|
||||
/// <summary>
|
||||
/// Stores a success (green) toast message in TempData for display on the
|
||||
/// next page render after a redirect.
|
||||
/// </summary>
|
||||
/// <param name="tempData">The TempData dictionary for the current request.</param>
|
||||
/// <param name="message">The human-readable success message.</param>
|
||||
public static void Success(this ITempDataDictionary tempData, string message)
|
||||
{
|
||||
tempData[SuccessKey] = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an error (red) toast message in TempData for display on the
|
||||
/// next page render after a redirect.
|
||||
/// </summary>
|
||||
/// <param name="tempData">The TempData dictionary for the current request.</param>
|
||||
/// <param name="message">The human-readable error message.</param>
|
||||
public static void Error(this ITempDataDictionary tempData, string message)
|
||||
{
|
||||
tempData[ErrorKey] = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a warning (yellow) toast message in TempData for display on the
|
||||
/// next page render after a redirect.
|
||||
/// </summary>
|
||||
/// <param name="tempData">The TempData dictionary for the current request.</param>
|
||||
/// <param name="message">The human-readable warning message.</param>
|
||||
public static void Warning(this ITempDataDictionary tempData, string message)
|
||||
{
|
||||
tempData[WarningKey] = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an informational (blue) toast message in TempData for display on
|
||||
/// the next page render after a redirect.
|
||||
/// </summary>
|
||||
/// <param name="tempData">The TempData dictionary for the current request.</param>
|
||||
/// <param name="message">The human-readable informational message.</param>
|
||||
public static void Info(this ITempDataDictionary tempData, string message)
|
||||
{
|
||||
tempData[InfoKey] = message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience extension methods on <see cref="Controller"/> that delegate to
|
||||
/// <see cref="ToastHelper"/> via the controller's own <c>TempData</c>.
|
||||
/// <para>
|
||||
/// These shorten controller code from <c>TempData.Success("…")</c> to the
|
||||
/// more readable <c>this.ToastSuccess("…")</c>, which reads closer to
|
||||
/// natural language and is consistent with other MVC helper conventions.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ControllerToastExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores a success (green) toast in the controller's TempData.
|
||||
/// Convenience wrapper around <see cref="ToastHelper.Success"/>.
|
||||
/// </summary>
|
||||
/// <param name="controller">The calling MVC controller.</param>
|
||||
/// <param name="message">The success message to display after redirect.</param>
|
||||
public static void ToastSuccess(this Controller controller, string message)
|
||||
{
|
||||
controller.TempData.Success(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an error (red) toast in the controller's TempData.
|
||||
/// Convenience wrapper around <see cref="ToastHelper.Error"/>.
|
||||
/// </summary>
|
||||
/// <param name="controller">The calling MVC controller.</param>
|
||||
/// <param name="message">The error message to display after redirect.</param>
|
||||
public static void ToastError(this Controller controller, string message)
|
||||
{
|
||||
controller.TempData.Error(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a warning (yellow) toast in the controller's TempData.
|
||||
/// Convenience wrapper around <see cref="ToastHelper.Warning"/>.
|
||||
/// </summary>
|
||||
/// <param name="controller">The calling MVC controller.</param>
|
||||
/// <param name="message">The warning message to display after redirect.</param>
|
||||
public static void ToastWarning(this Controller controller, string message)
|
||||
{
|
||||
controller.TempData.Warning(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an informational (blue) toast in the controller's TempData.
|
||||
/// Convenience wrapper around <see cref="ToastHelper.Info"/>.
|
||||
/// </summary>
|
||||
/// <param name="controller">The calling MVC controller.</param>
|
||||
/// <param name="message">The informational message to display after redirect.</param>
|
||||
public static void ToastInfo(this Controller controller, string message)
|
||||
{
|
||||
controller.TempData.Info(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates a <see cref="NotificationLog"/> outcome into the appropriate
|
||||
/// toast severity and message.
|
||||
/// <para>
|
||||
/// Sent notifications use Info (not Success) because the notification is a
|
||||
/// side effect of the main action — the main action already gets a Success
|
||||
/// toast. Skipped and Failed outcomes are Warning (not Error) because the
|
||||
/// primary operation succeeded even if the notification did not.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="controller">The calling MVC controller.</param>
|
||||
/// <param name="log">
|
||||
/// The most recent <see cref="NotificationLog"/> record, or <c>null</c>
|
||||
/// if no notification was attempted (in which case this method is a no-op).
|
||||
/// </param>
|
||||
public static void SetNotificationResultToast(this Controller controller, NotificationLog? log)
|
||||
{
|
||||
if (log == null) return;
|
||||
|
||||
var channel = log.Channel == NotificationChannel.Sms ? "SMS" : "Email";
|
||||
switch (log.Status)
|
||||
{
|
||||
case NotificationStatus.Sent:
|
||||
controller.ToastInfo($"{channel} sent to {log.RecipientName} ({log.Recipient}).");
|
||||
break;
|
||||
case NotificationStatus.Skipped:
|
||||
controller.ToastWarning(!string.IsNullOrEmpty(log.Message)
|
||||
? $"{channel} skipped: {log.Message}"
|
||||
: $"{channel} notification was skipped.");
|
||||
break;
|
||||
case NotificationStatus.Failed:
|
||||
controller.ToastWarning(!string.IsNullOrEmpty(log.ErrorMessage)
|
||||
? $"{channel} delivery failed: {log.ErrorMessage}"
|
||||
: $"{channel} notification failed.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user