Files
PowderCoatingLogix/src/PowderCoating.Application/DTOs/Customer/CustomerDtos.cs
T
spouliot 2c353f2e7f Three small UX fixes: customer contact validation, pagination dropdown width
- Customer validation now accepts Mobile Phone as a valid contact method;
  previously only Email or Phone satisfied the requirement
- Create customer hint text updated to match the new validation rule
- Pagination page-size dropdown gets min-width: 5rem so the number has
  breathing room from the caret arrow across all paginated lists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:20:43 -04:00

224 lines
7.6 KiB
C#

using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Application.DTOs.Customer;
public class CustomerDto
{
public int Id { get; set; }
public string? CompanyName { get; set; }
public string? ContactFirstName { get; set; }
public string? ContactLastName { get; set; }
public string? Email { get; set; }
public string? BillingEmail { get; set; }
public string? Phone { get; set; }
public string? MobilePhone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? ZipCode { get; set; }
public string? Country { get; set; }
public bool IsCommercial { get; set; }
public string? TaxId { get; set; }
public decimal CreditLimit { get; set; }
public decimal CurrentBalance { get; set; }
public decimal CreditBalance { get; set; }
public string? PaymentTerms { get; set; }
public int? PricingTierId { get; set; }
public string? PricingTierName { get; set; }
public bool IsTaxExempt { get; set; }
public bool HasTaxExemptCertificate { get; set; }
public string? TaxExemptCertificateFileName { get; set; }
public string? GeneralNotes { get; set; }
public bool IsActive { get; set; }
public DateTime? LastContactDate { get; set; }
public DateTime CreatedAt { get; set; }
public bool NotifyByEmail { get; set; }
public bool NotifyBySms { get; set; }
public DateTime? SmsConsentedAt { get; set; }
public string? SmsConsentMethod { get; set; }
}
public class CreateCustomerDto : IValidatableObject
{
[Display(Name = "Company Name")]
[StringLength(200)]
public string? CompanyName { get; set; }
[Display(Name = "First Name")]
[StringLength(100)]
public string? ContactFirstName { get; set; }
[Display(Name = "Last Name")]
[StringLength(100)]
public string? ContactLastName { get; set; }
[Display(Name = "Email")]
[StringLength(1000)]
public string? Email { get; set; }
[Display(Name = "Billing / Accounting Email")]
[StringLength(1000)]
public string? BillingEmail { get; set; }
[Display(Name = "Phone")]
[Phone(ErrorMessage = "Please enter a valid phone number")]
[StringLength(20)]
public string? Phone { get; set; }
[Display(Name = "Mobile Phone")]
[Phone(ErrorMessage = "Please enter a valid mobile phone number")]
[StringLength(20)]
public string? MobilePhone { get; set; }
[Display(Name = "Street Address")]
[StringLength(500)]
public string? Address { get; set; }
[Display(Name = "City")]
[StringLength(100)]
public string? City { get; set; }
[Display(Name = "State")]
[StringLength(50)]
public string? State { get; set; }
[Display(Name = "Zip Code")]
[StringLength(20)]
public string? ZipCode { get; set; }
[Display(Name = "Country")]
[StringLength(100)]
public string? Country { get; set; } = "USA";
[Display(Name = "Customer Type")]
public bool IsCommercial { get; set; }
[Display(Name = "Tax ID / EIN")]
[StringLength(50)]
public string? TaxId { get; set; }
[Display(Name = "Credit Limit")]
[Range(0, 9999999.99)]
public decimal CreditLimit { get; set; }
[Display(Name = "Payment Terms")]
[StringLength(50)]
public string? PaymentTerms { get; set; }
[Display(Name = "Pricing Tier")]
public int? PricingTierId { get; set; }
[Display(Name = "Tax Exempt")]
public bool IsTaxExempt { get; set; }
[Display(Name = "General Notes")]
[StringLength(2000)]
public string? GeneralNotes { get; set; }
[Display(Name = "Notify by Email")]
public bool NotifyByEmail { get; set; } = true;
[Display(Name = "Notify by SMS")]
public bool NotifyBySms { get; set; } = false;
/// <summary>
/// Form-only flag: staff checks this to record that the customer gave verbal consent to receive SMS.
/// Not mapped to any entity field — the controller handles setting SmsConsentedAt and SmsConsentMethod.
/// </summary>
[Display(Name = "Customer has verbally consented to receive SMS notifications")]
public bool SmsConsentGranted { get; set; } = false;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// At least one name field is required (Company Name OR Contact First/Last Name)
if (string.IsNullOrWhiteSpace(CompanyName) &&
string.IsNullOrWhiteSpace(ContactFirstName) &&
string.IsNullOrWhiteSpace(ContactLastName))
{
yield return new ValidationResult(
"Please provide either a Company Name or a Contact Name (First and/or Last Name)",
new[] { nameof(CompanyName), nameof(ContactFirstName), nameof(ContactLastName) });
}
// At least one contact method is required (Email, Phone, or Mobile Phone)
if (string.IsNullOrWhiteSpace(Email) && string.IsNullOrWhiteSpace(Phone) && string.IsNullOrWhiteSpace(MobilePhone))
{
yield return new ValidationResult(
"Please provide at least one contact method (Email, Phone, or Mobile Phone)",
new[] { nameof(Email), nameof(Phone), nameof(MobilePhone) });
}
// Validate each address in comma-separated email fields
foreach (var addr in SplitEmails(Email))
{
if (!IsValidEmail(addr))
yield return new ValidationResult(
$"'{addr}' is not a valid email address.",
new[] { nameof(Email) });
}
foreach (var addr in SplitEmails(BillingEmail))
{
if (!IsValidEmail(addr))
yield return new ValidationResult(
$"'{addr}' is not a valid email address.",
new[] { nameof(BillingEmail) });
}
}
private static IEnumerable<string> SplitEmails(string? value) =>
string.IsNullOrWhiteSpace(value)
? []
: value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
private static bool IsValidEmail(string email)
{
try { _ = new System.Net.Mail.MailAddress(email); return true; }
catch { return false; }
}
}
public class UpdateCustomerDto : CreateCustomerDto
{
public int Id { get; set; }
public bool IsActive { get; set; }
public bool HasTaxExemptCertificate { get; set; }
public string? TaxExemptCertificateFileName { get; set; }
// Read from entity so the Edit view can display consent status
public DateTime? SmsConsentedAt { get; set; }
public string? SmsConsentMethod { get; set; }
}
public class CustomerListDto
{
public int Id { get; set; }
public string? CompanyName { get; set; }
public string ContactName { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Email { get; set; }
public bool IsCommercial { get; set; }
public decimal CurrentBalance { get; set; }
public bool IsActive { get; set; }
public DateTime? LastContactDate { get; set; }
public bool NotifyByEmail { get; set; }
public bool NotifyBySms { get; set; }
}
public class AddCreditDto
{
[Required]
[Range(0.01, 99999.99, ErrorMessage = "Amount must be between $0.01 and $99,999.99")]
public decimal Amount { get; set; }
[Required]
[StringLength(200, ErrorMessage = "Reason cannot exceed 200 characters")]
[Display(Name = "Reason")]
public string Reason { get; set; } = string.Empty;
[StringLength(1000)]
[Display(Name = "Notes")]
public string? Notes { get; set; }
[Display(Name = "Expiry Date (optional)")]
public DateTime? ExpiryDate { get; set; }
}