using System.ComponentModel.DataAnnotations; using PlotLine.Models; namespace PlotLine.ViewModels; public sealed class ManageAccountViewModel { public AppUser User { get; set; } = new(); public UserSubscriptionDetail? Subscription { get; set; } public TrialVisibilityViewModel? Trial { get; set; } public bool HasPaidBillingRisk { get; set; } } public sealed class ManageSubscriptionViewModel { public AppUser User { get; set; } = new(); public UserSubscriptionDetail? CurrentSubscription { get; set; } public TrialVisibilityViewModel? Trial { get; set; } public SubscriptionUsageSummary? Usage { get; set; } public IReadOnlyList AvailableLevels { get; set; } = []; } public sealed class TrialVisibilityViewModel { public const int TrialDurationDays = 30; public string PlanName { get; init; } = string.Empty; public DateTime StartDateUtc { get; init; } public DateTime ExpiryDateUtc { get; init; } public int DaysRemaining { get; init; } public int DaysElapsed { get; init; } public int ProgressPercentage { get; init; } public bool IsExpired { get; init; } public int? WarningThresholdDays { get; init; } public string StatusLabel => IsExpired ? "Trial expired" : DaysRemaining == 1 ? "1 day remaining" : $"{DaysRemaining} days remaining"; public string WarningCssClass => WarningThresholdDays switch { <= 1 => "trial-status--one-day", <= 3 => "trial-status--three-days", <= 7 => "trial-status--seven-days", <= 14 => "trial-status--fourteen-days", _ => "trial-status--steady" }; public static TrialVisibilityViewModel? FromSubscription(UserSubscriptionDetail? subscription, DateTime utcNow) { if (subscription is null) { return null; } var isTrialPlan = subscription.IsTrial || (!subscription.RequiresStripeSubscription && string.IsNullOrWhiteSpace(subscription.StripeSubscriptionId)); if (!isTrialPlan) { return null; } var start = subscription.StartDateUTC; var expiry = subscription.TrialExpiryDateUTC ?? start.AddDays(TrialDurationDays); var totalDays = Math.Max(1, (int)Math.Ceiling((expiry - start).TotalDays)); var rawRemaining = (int)Math.Ceiling((expiry - utcNow).TotalDays); var daysRemaining = Math.Max(0, rawRemaining); var daysElapsed = Math.Clamp(totalDays - daysRemaining, 0, totalDays); var progress = Math.Clamp((int)Math.Round(daysElapsed * 100m / totalDays), 0, 100); return new TrialVisibilityViewModel { PlanName = subscription.DisplayName, StartDateUtc = start, ExpiryDateUtc = expiry, DaysRemaining = daysRemaining, DaysElapsed = daysElapsed, ProgressPercentage = progress, IsExpired = utcNow >= expiry, WarningThresholdDays = daysRemaining <= 1 ? 1 : daysRemaining <= 3 ? 3 : daysRemaining <= 7 ? 7 : daysRemaining <= 14 ? 14 : null }; } } public sealed class PricingViewModel { public IReadOnlyList Plans { get; set; } = []; public int? CurrentSubscriptionLevelID { get; set; } public bool IsSignedIn { get; set; } } public sealed class PublicPageViewModel { public string SupportEmail { get; set; } = "support@plotdirector.com"; public string LastUpdated { get; set; } = "June 7, 2026"; } public sealed class RegisterViewModel { [Required(ErrorMessage = "Enter your email address.")] [EmailAddress(ErrorMessage = "Enter a valid email address.")] [StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")] public string Email { get; set; } = string.Empty; [Required(ErrorMessage = "Enter a display name.")] [StringLength(200, ErrorMessage = "Display name must be 200 characters or fewer.")] public string DisplayName { get; set; } = string.Empty; [Required(ErrorMessage = "Enter a password.")] [StringLength(200, MinimumLength = 12, ErrorMessage = "Password must be at least 12 characters.")] [DataType(DataType.Password)] public string Password { get; set; } = string.Empty; [Required(ErrorMessage = "Confirm your password.")] [DataType(DataType.Password)] [Compare(nameof(Password), ErrorMessage = "Passwords do not match.")] public string ConfirmPassword { get; set; } = string.Empty; } public sealed class LoginViewModel { [Required(ErrorMessage = "Enter your email address.")] [EmailAddress(ErrorMessage = "Enter a valid email address.")] [StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")] public string Email { get; set; } = string.Empty; [Required(ErrorMessage = "Enter your password.")] [DataType(DataType.Password)] public string Password { get; set; } = string.Empty; public bool RememberMe { get; set; } public string? ReturnUrl { get; set; } public bool IsCompanionLogin { get; set; } } public sealed class ForgotPasswordViewModel { [Required(ErrorMessage = "Enter your email address.")] [EmailAddress(ErrorMessage = "Enter a valid email address.")] [StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")] public string Email { get; set; } = string.Empty; } public sealed class ResendVerificationEmailViewModel { [Required(ErrorMessage = "Enter your email address.")] [EmailAddress(ErrorMessage = "Enter a valid email address.")] [StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")] public string Email { get; set; } = string.Empty; } public sealed class ResetPasswordViewModel { [Required(ErrorMessage = "Password reset token is missing.")] public Guid Token { get; set; } [Required(ErrorMessage = "Enter a new password.")] [StringLength(200, MinimumLength = 12, ErrorMessage = "Password must be at least 12 characters.")] [DataType(DataType.Password)] public string Password { get; set; } = string.Empty; [Required(ErrorMessage = "Confirm your new password.")] [DataType(DataType.Password)] [Compare(nameof(Password), ErrorMessage = "Passwords do not match.")] public string ConfirmPassword { get; set; } = string.Empty; } public sealed class AccountStatusViewModel { public bool Success { get; set; } public string Title { get; set; } = string.Empty; public string Message { get; set; } = string.Empty; } public sealed class TwoFactorCodeViewModel { [Required(ErrorMessage = "Enter the code from your authenticator app.")] [StringLength(20, ErrorMessage = "Authentication code is too long.")] public string Code { get; set; } = string.Empty; public string? ReturnUrl { get; set; } } public sealed class TwoFactorRecoveryViewModel { [Required(ErrorMessage = "Enter a recovery code.")] [StringLength(20, ErrorMessage = "Recovery code is too long.")] public string RecoveryCode { get; set; } = string.Empty; public string? ReturnUrl { get; set; } } public sealed class EnableTwoFactorViewModel { [Required(ErrorMessage = "Authenticator secret is missing.")] public string SecretKey { get; set; } = string.Empty; public string QrCodeUri { get; set; } = string.Empty; public string? QrCodeImageDataUri { get; set; } [Required(ErrorMessage = "Enter the code from your authenticator app.")] [StringLength(20, ErrorMessage = "Authentication code is too long.")] public string Code { get; set; } = string.Empty; } public sealed class DisableTwoFactorViewModel { [Required(ErrorMessage = "Enter your current password.")] [DataType(DataType.Password)] public string CurrentPassword { get; set; } = string.Empty; } public sealed class RecoveryCodesViewModel { public IReadOnlyList RecoveryCodes { get; set; } = Array.Empty(); public bool HasNewCodes => RecoveryCodes.Any(); } public sealed class ChangePasswordViewModel { [Required(ErrorMessage = "Enter your current password.")] [DataType(DataType.Password)] public string CurrentPassword { get; set; } = string.Empty; [Required(ErrorMessage = "Enter a new password.")] [StringLength(200, MinimumLength = 12, ErrorMessage = "Password must be at least 12 characters.")] [DataType(DataType.Password)] public string NewPassword { get; set; } = string.Empty; [Required(ErrorMessage = "Confirm your new password.")] [DataType(DataType.Password)] [Compare(nameof(NewPassword), ErrorMessage = "Passwords do not match.")] public string ConfirmNewPassword { get; set; } = string.Empty; }